hyp/hypd/server/packet.go
Steven Polley ead7578544 change pcap snaplen to 126 bytes
We really only care getting as far as the UDP header and can discard the rest.  This should reduce load, and perhaps enable full pcap with ports on the BPF filter

UDP header = 8 bytes
IPv4 max size = 60 bytes
IPv6 fixed size = 40 bytes
Ethernet header size = 18 bytes
2024-04-11 15:21:48 -06:00

167 lines
5.7 KiB
Go

/*
Copyright © 2024 Steven Polley <himself@stevenpolley.net>
*/
package server
import (
"encoding/binary"
"fmt"
"log"
"os"
"os/exec"
"time"
"deadbeef.codes/steven/hyp/otphyp"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
)
// 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
LastUpdated time.Time // The last time the client sent a correct packet in the sequence
}
// 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
}
var (
clients map[string]*Client // Contains a map of clients
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
)
// 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(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{}
// Open pcap handle on device
handle, err := pcap.OpenLive(captureDevice, 126, true, pcap.BlockForever)
if err != nil {
return fmt.Errorf("failed to open pcap on capture device: %w", err)
}
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
// Setup a goroutine to periodically rotate the authentic knock sequence
go rotateSequence(handle)
// Read from the pcap handle until we exit
for packet := range packetSource.Packets() {
handlePacket(packet) // Do something with a packet here.
}
return nil
}
// 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()
client, ok := clients[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 port == knockSequence.PortSequence[0] {
// Create the client and mark the knock sequence as used
clients[srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
knockSequences[i].Used = true
}
}
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
// 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.
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)
handleSuccess(srcip) // The magic function, the knock is completed
return
}
}
// Used to rotate the authentic port knock sequence
func rotateSequence(handle *pcap.Handle) {
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)
// Set BPF filter
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)))
// pop first value, next iteration pushes new value
knockSequences = knockSequences[1:]
}
}
// Given a pcap handle and list of authentic port knock sequences, configures a BPF filter
func setPacketFilter(handle *pcap.Handle) error {
filter := "udp && ("
for i, knockSequence := range knockSequences {
for j, port := range knockSequence.PortSequence {
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
}
// 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)
}
}