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"
"os/exec"
2024-04-07 03:59:13 +00:00
"time"
"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-15 00:14:24 +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
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
sharedSecret string // base32 encoded shared secret used for totp
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-11 03:42:38 +00:00
func PacketServer ( captureDevice string ) error {
2024-04-14 03:22:22 +00:00
iface , err := net . InterfaceByName ( captureDevice )
if err != nil {
log . Fatalf ( "lookup network iface %q: %v" , captureDevice , err )
}
2024-04-11 03:42:38 +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 )
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
}
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
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-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 }
2024-04-07 13:59:23 +00:00
knockSequences [ i ] . Used = true
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
// TBD: vulnerable to sweep attack - this won't be triggered if a wrong packet doesn't match BPF filter
2024-04-08 03:33:13 +00:00
// TBD: make the sweep attack fix on by default, but configurable to be off to allow for limited BPF filter for extremely low overhead as compromise.
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
}
}
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.
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 {
2024-04-07 13:59:23 +00:00
// 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
}
2024-04-07 13:59:23 +00:00
knockSequence := KnockSequence { PortSequence : portSequence }
knockSequences = append ( knockSequences , knockSequence )
2024-04-07 03:59:13 +00:00
}
2024-04-07 13:59:23 +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 ) ) )
2024-04-07 13:59:23 +00:00
// pop first value, next iteration pushes new value
knockSequences = knockSequences [ 1 : ]
2024-04-07 03:59:13 +00:00
}
}
2024-04-11 03:42:38 +00:00
// TBD: Implement - this is a temporary routine to demonstrate an application
2024-04-14 03:22:22 +00:00
func handleSuccess ( srcip net . IP ) {
2024-04-11 03:42:38 +00:00
fmt . Println ( "Success for " , srcip )
2024-04-14 03:22:22 +00:00
cmd := exec . Command ( "iptables" , "-A" , "INPUT" , "-p" , "tcp" , "-s" , fmt . Sprint ( srcip ) , "--dport" , "22" , "-j" , "ACCEPT" )
2024-04-11 03:42:38 +00:00
err := cmd . Run ( )
if err != nil {
log . Printf ( "failed to execute iptables command for '%s': %v" , srcip , err )
}
}