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 (
"encoding/binary"
"fmt"
"log"
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"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
)
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-11 03:42:38 +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
LastUpdated time . Time // The last time the client sent a correct packet in the sequence
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-07 03:59:13 +00:00
var (
2024-04-07 13:59:23 +00:00
clients map [ string ] * Client // Contains a map of clients
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-07 13:59:23 +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 {
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 [ string ] * Client , 0 )
knockSequences = [ ] KnockSequence { }
2024-04-07 03:59:13 +00:00
2024-04-08 03:33:13 +00:00
// Open pcap handle on device
2024-04-07 03:59:13 +00:00
handle , err := pcap . OpenLive ( captureDevice , 1600 , true , pcap . BlockForever )
if err != nil {
2024-04-11 03:42:38 +00:00
return fmt . Errorf ( "failed to open pcap on capture device: %w" , err )
2024-04-07 03:59:13 +00:00
}
packetSource := gopacket . NewPacketSource ( handle , handle . LinkType ( ) )
2024-04-08 03:33:13 +00:00
// Setup a goroutine to periodically rotate the authentic knock sequence
2024-04-07 03:59:13 +00:00
go rotateSequence ( handle )
2024-04-08 03:33:13 +00:00
// Read from the pcap handle until we exit
2024-04-07 03:59:13 +00:00
for packet := range packetSource . Packets ( ) {
handlePacket ( packet ) // Do something with a packet here.
}
2024-04-11 03:42:38 +00:00
return nil
2024-04-07 03:59:13 +00:00
}
// packets that match the BPF filter get passed to handlePacket
func handlePacket ( packet gopacket . Packet ) {
port := binary . BigEndian . Uint16 ( packet . TransportLayer ( ) . TransportFlow ( ) . Dst ( ) . Raw ( ) )
srcip := packet . NetworkLayer ( ) . NetworkFlow ( ) . Src ( ) . String ( )
2024-04-08 03:33:13 +00:00
2024-04-07 03:59:13 +00:00
client , ok := clients [ 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
}
if port == knockSequence . PortSequence [ 0 ] {
2024-04-08 03:33:13 +00:00
// Create the client and mark the knock sequence as used
2024-04-07 13:59:23 +00:00
clients [ srcip ] = & Client { Progress : 1 , Sequence : knockSequence . PortSequence }
knockSequences [ i ] . Used = true
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-07 03:59:13 +00:00
if port != client . Sequence [ client . Progress ] {
delete ( clients , srcip )
fmt . Printf ( "port '%d' is in sequence, but came at unexpected order - resetting progress" , port )
return
}
// Client increases progress through sequence and checks if sequence is completed
client . Progress ++
if client . Progress >= len ( client . Sequence ) {
delete ( clients , srcip )
2024-04-08 03:33:13 +00:00
handleSuccess ( srcip ) // The magic function, the knock is completed
2024-04-07 03:59:13 +00:00
return
}
}
// Used to rotate the authentic port knock sequence
func rotateSequence ( handle * pcap . Handle ) {
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
2024-04-07 13:59:23 +00:00
// Set BPF filter
2024-04-07 03:59:13 +00:00
err := setPacketFilter ( handle )
if err != nil {
log . Printf ( "failed to change packet filter: %v" , err )
}
// 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
}
}
// Given a pcap handle and list of authentic port knock sequences, configures a BPF filter
func setPacketFilter ( handle * pcap . Handle ) error {
filter := "udp && ("
2024-04-07 13:59:23 +00:00
for i , knockSequence := range knockSequences {
for j , port := range knockSequence . PortSequence {
2024-04-07 03:59:13 +00:00
if i == 0 && j == 0 {
filter += fmt . Sprint ( "port " , port )
} else {
filter += fmt . Sprint ( " || port " , port )
}
}
}
filter += ")"
err := handle . SetBPFFilter ( filter )
if err != nil {
return fmt . Errorf ( "failed to set BPF filter '%s': %v" , filter , err )
}
return nil
}
2024-04-11 03:42:38 +00:00
// TBD: Implement - this is a temporary routine to demonstrate an application
func handleSuccess ( srcip string ) {
fmt . Println ( "Success for " , srcip )
cmd := exec . Command ( "iptables" , "-A" , "INPUT" , "-p" , "tcp" , "-s" , srcip , "--dport" , "22" , "-j" , "ACCEPT" )
err := cmd . Run ( )
if err != nil {
log . Printf ( "failed to execute iptables command for '%s': %v" , srcip , err )
}
}