hyp/hypd/server/packet.go

214 lines
7.0 KiB
Go
Raw Normal View History

/*
Copyright © 2024 Steven Polley <himself@stevenpolley.net>
*/
package server
2024-04-07 03:59:13 +00:00
import (
2024-04-14 03:22:22 +00:00
"bytes"
2024-04-07 03:59:13 +00:00
"encoding/binary"
2024-04-14 03:22:22 +00:00
"errors"
2024-04-07 03:59:13 +00:00
"fmt"
"log"
2024-04-14 03:22:22 +00:00
"net"
"os"
"os/exec"
2024-04-07 03:59:13 +00:00
"time"
2024-04-18 01:12:01 +00:00
"deadbeef.codes/steven/hyp/hypd/configuration"
2024-04-07 03:59:13 +00:00
"deadbeef.codes/steven/hyp/otphyp"
2024-04-14 03:22:22 +00:00
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
2024-04-07 03:59:13 +00:00
)
2024-04-14 03:22:22 +00:00
//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
2024-04-07 03:59:13 +00:00
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
2024-04-07 03:59:13 +00:00
}
// 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 {
2024-04-08 03:33:13 +00:00
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
)
2024-04-07 03:59:13 +00:00
var (
2024-04-14 03:22:22 +00:00
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
2024-04-08 03:33:13 +00:00
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
2024-04-07 03:59:13 +00:00
)
// PacketServer is the main function when operating in server mode
2024-04-08 03:33:13 +00:00
// 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)
2024-04-14 03:22:22 +00:00
if err != nil {
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
2024-04-14 03:22:22 +00:00
}
secretBytes, err := os.ReadFile("hyp.secret")
if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err)
}
sharedSecret = string(secretBytes)
2024-04-14 03:22:22 +00:00
clients = make(map[uint32]*Client, 0)
knockSequences = []KnockSequence{}
2024-04-07 03:59:13 +00:00
2024-04-14 03:22:22 +00:00
// 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,
})
2024-04-07 03:59:13 +00:00
if err != nil {
2024-04-14 03:22:22 +00:00
log.Fatalf("could not attach XDP program: %v", err)
2024-04-07 03:59:13 +00:00
}
2024-04-14 03:22:22 +00:00
defer l.Close()
2024-04-07 03:59:13 +00:00
2024-04-14 03:22:22 +00:00
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")
2024-04-08 03:33:13 +00:00
2024-04-14 03:22:22 +00:00
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)
2024-04-07 03:59:13 +00:00
}
2024-04-14 03:22:22 +00:00
}
// 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
2024-04-07 03:59:13 +00:00
}
// packets that match the BPF filter get passed to handlePacket
2024-04-14 03:22:22 +00:00
func handleKnock(knockEvent hyp_bpfKnockData) {
2024-04-08 03:33:13 +00:00
2024-04-14 03:22:22 +00:00
client, ok := clients[knockEvent.Srcip]
2024-04-08 03:33:13 +00:00
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
}
2024-04-14 03:22:22 +00:00
if knockEvent.Dstport == knockSequence.PortSequence[0] {
2024-04-08 03:33:13 +00:00
// Create the client and mark the knock sequence as used
2024-04-14 03:22:22 +00:00
clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
knockSequences[i].Used = true
go timeoutKnockSequence(knockEvent.Srcip)
2024-04-07 03:59:13 +00:00
}
}
return
}
// if it's wrong, reset progress
2024-04-14 03:22:22 +00:00
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)
2024-04-07 03:59:13 +00:00
return
}
// Client increases progress through sequence and checks if sequence is completed
client.Progress++
if client.Progress >= len(client.Sequence) {
2024-04-14 03:22:22 +00:00
delete(clients, knockEvent.Srcip)
handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
2024-04-07 03:59:13 +00:00
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)
}
}
2024-04-07 03:59:13 +00:00
// Used to rotate the authentic port knock sequence
2024-04-14 03:22:22 +00:00
func rotateSequence() {
2024-04-07 03:59:13 +00:00
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)
2024-04-07 03:59:13 +00:00
}
knockSequence := KnockSequence{PortSequence: portSequence}
knockSequences = append(knockSequences, knockSequence)
2024-04-07 03:59:13 +00:00
}
fmt.Println("New sequences:", knockSequences)
2024-04-07 03:59:13 +00:00
// 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:]
2024-04-07 03:59:13 +00:00
}
}
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
// the configured success action is ran
2024-04-14 03:22:22 +00:00
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)
}
}