2024-04-11 03:42:38 +00:00
/ *
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"
2024-04-11 03:42:38 +00:00
"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
2024-04-07 13:59:23 +00:00
// 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 {
2024-04-20 21:41:26 +00:00
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
LastSuccess time . Time
2024-04-07 03:59:13 +00:00
}
2024-04-07 13:59:23 +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
2024-04-07 13:59:23 +00:00
}
2024-04-15 00:14:24 +00:00
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
2024-04-18 17:22:03 +00:00
serverConfig * configuration . HypdConfiguration
2024-04-20 04:04:00 +00:00
sharedSecrets [ ] [ ] byte // A slice of byte slices, each being a secret key
2024-04-07 03:59:13 +00:00
)
2024-04-11 21:21:48 +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
2024-04-07 13:59:23 +00:00
// to rotate the knock sequence
2024-04-20 04:04:00 +00:00
func PacketServer ( config * configuration . HypdConfiguration , secrets [ ] [ ] byte ) error {
2024-04-18 17:22:03 +00:00
serverConfig = config
2024-04-20 04:04:00 +00:00
sharedSecrets = secrets
2024-04-18 17:22:03 +00:00
iface , err := net . InterfaceByName ( serverConfig . NetworkInterface )
2024-04-14 03:22:22 +00:00
if err != nil {
2024-04-18 17:22:03 +00:00
log . Fatalf ( "lookup network iface %q: %v" , serverConfig . NetworkInterface , err )
2024-04-14 03:22:22 +00:00
}
clients = make ( map [ uint32 ] * Client , 0 )
2024-04-11 03:42:38 +00:00
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
}
2024-04-20 21:41:26 +00:00
go 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 ) {
client , ok := clients [ knockEvent . Srcip ]
2024-04-08 03:33:13 +00:00
if ! ok { // client doesn't exist yet
2024-04-20 21:41:26 +00:00
client = & Client { }
clients [ knockEvent . Srcip ] = client
}
if client . Progress == 0 {
for i , knockSequence := range knockSequences { // identify which of the authentic knock sequences is matched
2024-04-07 13:59:23 +00:00
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-20 21:41:26 +00:00
knockSequences [ i ] . Used = true // TBD: This is vulnerable to a DoS just by doing a full UDP port scan
client . Progress = 1
client . Sequence = knockSequence . PortSequence
2024-04-15 00:14:24 +00:00
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-20 21:41:26 +00:00
client . Progress = 0
client . LastSuccess = time . Now ( )
handleSuccess ( knockEvent . Srcip ) // The magic function, the knock is completed
2024-04-07 03:59:13 +00:00
return
}
}
2024-04-15 00:14:24 +00:00
// 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.
2024-04-20 21:41:26 +00:00
// Note: This is not related to handling the timeout / clsoe ports action after a client
// has successfully completed an authentic knock sequence
2024-04-15 00:14:24 +00:00
func timeoutKnockSequence ( srcip uint32 ) {
time . Sleep ( time . Second * KnockSequenceTimeout )
2024-04-20 21:41:26 +00:00
client , ok := clients [ srcip ]
2024-04-15 00:14:24 +00:00
if ok {
2024-04-20 21:41:26 +00:00
if client . LastSuccess . IsZero ( ) { // If they've never succeeded, just drop them from the map
delete ( clients , srcip )
} else { // If they have succeeded, just reset their progress to 0 but keep them in map. They will be cleaned in handleSuccess
client . Progress = 0
}
2024-04-15 00:14:24 +00:00
}
}
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 {
2024-04-07 13:59:23 +00:00
// Generate new knock sequences with time skew support
t := time . Now ( ) . Add ( time . Second * - 30 )
2024-04-20 19:27:00 +00:00
for i := len ( knockSequences ) / len ( sharedSecrets ) ; i < 3 ; i ++ {
2024-04-20 04:04:00 +00:00
for _ , secret := range sharedSecrets {
portSequence , err := otphyp . GeneratePorts ( secret , 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 )
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 ) ) )
2024-04-07 13:59:23 +00:00
// pop first value, next iteration pushes new value
2024-04-20 19:27:00 +00:00
knockSequences = knockSequences [ len ( sharedSecrets ) : ]
2024-04-07 03:59:13 +00:00
}
}
2024-04-18 17:22:03 +00:00
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
// the configured success action is ran
2024-04-20 21:41:26 +00:00
func handleSuccess ( srcip uint32 ) {
srcipf := intToIP ( srcip ) // formatted as net.IP
log . Printf ( "Successful knock from: %s" , srcipf )
client , ok := clients [ srcip ]
if ! ok {
log . Printf ( "failed to lookup %s in clients" , srcipf )
return
}
2024-04-18 17:22:03 +00:00
// 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
2024-04-20 21:41:26 +00:00
cmd := exec . Command ( "sh" , "-c" , fmt . Sprintf ( serverConfig . SuccessAction , srcipf ) )
2024-04-11 03:42:38 +00:00
err := cmd . Run ( )
if err != nil {
2024-04-20 21:41:26 +00:00
log . Printf ( "failed to execute success action command for '%s': %v" , srcipf , err )
}
// Handle timeout action
if serverConfig . TimeoutSeconds < 1 { // Timeout action is disabled
delete ( clients , srcip )
return
}
// Handle checks for client timeout
// TBD: Persistence / journaling state to disk? How to handle case if knock daemon is restarted - ports would remain open
lastSuccess := client . LastSuccess
time . Sleep ( time . Until ( client . LastSuccess . Add ( time . Duration ( serverConfig . TimeoutSeconds * int ( time . Second ) ) ) ) )
if client . LastSuccess . After ( lastSuccess ) { // The client has refreshed
return
}
// 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
log . Printf ( "Performing timeout action on: %s" , srcipf )
cmd = exec . Command ( "sh" , "-c" , fmt . Sprintf ( serverConfig . TimeoutAction , srcipf ) )
err = cmd . Run ( )
if err != nil {
log . Printf ( "failed to execute timeout action command for '%s': %v" , srcipf , err )
2024-04-11 03:42:38 +00:00
}
2024-04-20 21:41:26 +00:00
delete ( clients , srcip )
2024-04-11 03:42:38 +00:00
}