Steven Polley
05e37193b4
Instead of using the hardcoded proof of concept for iptables, the success action is now read from the hypd configuration file and whatever is defined there is executed.
214 lines
7.0 KiB
Go
214 lines
7.0 KiB
Go
/*
|
|
Copyright © 2024 Steven Polley <himself@stevenpolley.net>
|
|
*/
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"deadbeef.codes/steven/hyp/hypd/configuration"
|
|
"deadbeef.codes/steven/hyp/otphyp"
|
|
"github.com/cilium/ebpf/link"
|
|
"github.com/cilium/ebpf/ringbuf"
|
|
"github.com/cilium/ebpf/rlimit"
|
|
)
|
|
|
|
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go --type knock_data hyp_bpf hyp_bpf.c
|
|
|
|
// Client is used to keep track of a client attempting to perform an authentic knock sequence
|
|
type Client struct {
|
|
Progress int // index of current progress in sequence. Value of 1 means first port has been matched
|
|
Sequence [4]uint16 // stores the knock sequence the current client is attempting. It's set and tracked here to prevent race conditions during a knock sequence being received and key rotations
|
|
}
|
|
|
|
// KnockSequence is used keep track of an ordered knock sequence and whether it's been marked for use (to prevent replay attacks)
|
|
type KnockSequence struct {
|
|
Used bool // If true, that means this knock sequence has already been used once. It may still be within the valid time window, but it can't be used again
|
|
PortSequence [4]uint16 // Each knock sequence is four ports long
|
|
}
|
|
|
|
const (
|
|
KnockSequenceTimeout = 3 // TBD: Make this a configurable value
|
|
)
|
|
|
|
var (
|
|
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
|
|
knockSequences []KnockSequence // We have 3 valid knock sequences at any time to account for clock skew
|
|
sharedSecret string // base32 encoded shared secret used for totp
|
|
serverConfig *configuration.HypdConfiguration
|
|
)
|
|
|
|
// PacketServer is the main function when operating in server mode
|
|
// it sets up the pcap on the capture device and starts a goroutine
|
|
// to rotate the knock sequence
|
|
func PacketServer(config *configuration.HypdConfiguration) error {
|
|
serverConfig = config
|
|
iface, err := net.InterfaceByName(serverConfig.NetworkInterface)
|
|
if err != nil {
|
|
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
|
|
}
|
|
|
|
secretBytes, err := os.ReadFile("hyp.secret")
|
|
if err != nil {
|
|
log.Fatalf("failed to read file 'hyp.secret': %v", err)
|
|
}
|
|
sharedSecret = string(secretBytes)
|
|
|
|
clients = make(map[uint32]*Client, 0)
|
|
knockSequences = []KnockSequence{}
|
|
|
|
// Setup a goroutine to periodically rotate the authentic knock sequence
|
|
go rotateSequence()
|
|
|
|
////////////////////////////////////
|
|
|
|
// Allow the current process to lock memory for eBPF resources.
|
|
if err := rlimit.RemoveMemlock(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Load pre-compiled programs into the kernel.
|
|
objs := hyp_bpfObjects{}
|
|
if err := loadHyp_bpfObjects(&objs, nil); err != nil {
|
|
log.Fatalf("loading objects: %v", err)
|
|
}
|
|
defer objs.Close()
|
|
|
|
// Attach the program.
|
|
l, err := link.AttachXDP(link.XDPOptions{
|
|
Program: objs.XdpProgFunc,
|
|
Interface: iface.Index,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("could not attach XDP program: %v", err)
|
|
}
|
|
defer l.Close()
|
|
|
|
log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index)
|
|
log.Printf("Press Ctrl-C to exit and remove the program")
|
|
|
|
rd, err := ringbuf.NewReader(objs.Rb)
|
|
if err != nil {
|
|
log.Fatalf("could not open ring buffer reader: %v", err)
|
|
}
|
|
defer rd.Close()
|
|
|
|
var event hyp_bpfKnockData
|
|
for {
|
|
record, err := rd.Read()
|
|
if err != nil {
|
|
if errors.Is(err, ringbuf.ErrClosed) {
|
|
log.Println("eBPF ring buffer closed, exiting...")
|
|
return nil
|
|
}
|
|
log.Printf("error reading from ring buffer reader: %v", err)
|
|
continue
|
|
}
|
|
|
|
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
|
|
log.Printf("error parsing ringbuf event: %v", err)
|
|
continue
|
|
}
|
|
handleKnock(event)
|
|
}
|
|
}
|
|
|
|
// intToIP converts IPv4 number to net.IP
|
|
func intToIP(ipNum uint32) net.IP {
|
|
ip := make(net.IP, 4)
|
|
binary.BigEndian.PutUint32(ip, ipNum)
|
|
return ip
|
|
}
|
|
|
|
// packets that match the BPF filter get passed to handlePacket
|
|
func handleKnock(knockEvent hyp_bpfKnockData) {
|
|
|
|
client, ok := clients[knockEvent.Srcip]
|
|
if !ok { // client doesn't exist yet
|
|
for i, knockSequence := range knockSequences { // identify which of the 3 authentic knock sequences is matched
|
|
if knockSequence.Used { // skip over sequences that are already used to prevent replay attack
|
|
continue
|
|
}
|
|
if knockEvent.Dstport == knockSequence.PortSequence[0] {
|
|
// Create the client and mark the knock sequence as used
|
|
clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
|
knockSequences[i].Used = true
|
|
go timeoutKnockSequence(knockEvent.Srcip)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// if it's wrong, reset progress
|
|
if knockEvent.Dstport != client.Sequence[client.Progress] {
|
|
delete(clients, knockEvent.Srcip)
|
|
fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", knockEvent.Dstport)
|
|
return
|
|
}
|
|
|
|
// Client increases progress through sequence and checks if sequence is completed
|
|
client.Progress++
|
|
if client.Progress >= len(client.Sequence) {
|
|
delete(clients, knockEvent.Srcip)
|
|
handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
|
|
return
|
|
}
|
|
}
|
|
|
|
// Remove the client after the timeout value has elapsed. This prevents a client from
|
|
// being indefinitely stuck part way through an old knock sequence. It's also helpful
|
|
// in preventing sweep attacks as the authentic knock sequence must be correctly entered
|
|
// within the timeout value from start to finish.
|
|
func timeoutKnockSequence(srcip uint32) {
|
|
time.Sleep(time.Second * KnockSequenceTimeout)
|
|
_, ok := clients[srcip]
|
|
if ok {
|
|
delete(clients, srcip)
|
|
}
|
|
}
|
|
|
|
// Used to rotate the authentic port knock sequence
|
|
func rotateSequence() {
|
|
for {
|
|
// Generate new knock sequences with time skew support
|
|
t := time.Now().Add(time.Second * -30)
|
|
for i := len(knockSequences); i < 3; i++ {
|
|
portSequence, err := otphyp.GeneratePorts(sharedSecret, t.Add((time.Second * 30 * time.Duration(i))))
|
|
if err != nil {
|
|
log.Fatalf("failed to generate port knock sequence: %v", err)
|
|
}
|
|
knockSequence := KnockSequence{PortSequence: portSequence}
|
|
knockSequences = append(knockSequences, knockSequence)
|
|
}
|
|
fmt.Println("New sequences:", knockSequences)
|
|
|
|
// Sleep until next 30 second offset
|
|
time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30)))
|
|
|
|
// pop first value, next iteration pushes new value
|
|
knockSequences = knockSequences[1:]
|
|
}
|
|
}
|
|
|
|
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
|
|
// the configured success action is ran
|
|
func handleSuccess(srcip net.IP) {
|
|
fmt.Println("Successful knock from:", srcip)
|
|
// Don't care about command injection, the configuration file providing the command literally NEEDS to be trusted
|
|
// TBD: Use template / substitution instead of string formatting directive - allows for srcip token to be used multiple times
|
|
cmd := exec.Command("sh", "-c", fmt.Sprintf(serverConfig.SuccessAction, srcip))
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
log.Printf("failed to execute success action command for '%s': %v", srcip, err)
|
|
}
|
|
}
|