enter eBPF, libpcap and CGO = bloat
This commit is contained in:
@ -5,18 +5,24 @@ Copyright © 2024 Steven Polley <himself@stevenpolley.net>
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"deadbeef.codes/steven/hyp/otphyp"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/cilium/ebpf/ringbuf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
)
|
||||
|
||||
//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
|
||||
type Client struct {
|
||||
Progress int // index of current progress in sequence. Value of 1 means first port has been matched
|
||||
@ -31,7 +37,7 @@ type KnockSequence struct {
|
||||
}
|
||||
|
||||
var (
|
||||
clients map[string]*Client // Contains a map of clients
|
||||
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
|
||||
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
|
||||
)
|
||||
@ -40,46 +46,96 @@ var (
|
||||
// it sets up the pcap on the capture device and starts a goroutine
|
||||
// to rotate the knock sequence
|
||||
func PacketServer(captureDevice string) error {
|
||||
|
||||
iface, err := net.InterfaceByName(captureDevice)
|
||||
if err != nil {
|
||||
log.Fatalf("lookup network iface %q: %v", captureDevice, err)
|
||||
}
|
||||
|
||||
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)
|
||||
clients = make(map[uint32]*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)
|
||||
go rotateSequence()
|
||||
|
||||
// Read from the pcap handle until we exit
|
||||
for packet := range packetSource.Packets() {
|
||||
handlePacket(packet) // Do something with a packet here.
|
||||
////////////////////////////////////
|
||||
|
||||
// Allow the current process to lock memory for eBPF resources.
|
||||
if err := rlimit.RemoveMemlock(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
|
||||
// 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,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("could not attach XDP program: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()
|
||||
func handleKnock(knockEvent hyp_bpfKnockData) {
|
||||
|
||||
client, ok := clients[srcip]
|
||||
client, ok := clients[knockEvent.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] {
|
||||
if knockEvent.Dstport == knockSequence.PortSequence[0] {
|
||||
// Create the client and mark the knock sequence as used
|
||||
clients[srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
||||
clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
||||
knockSequences[i].Used = true
|
||||
}
|
||||
}
|
||||
@ -89,23 +145,23 @@ func handlePacket(packet gopacket.Packet) {
|
||||
// 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)
|
||||
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)
|
||||
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
|
||||
delete(clients, knockEvent.Srcip)
|
||||
handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Used to rotate the authentic port knock sequence
|
||||
func rotateSequence(handle *pcap.Handle) {
|
||||
func rotateSequence() {
|
||||
for {
|
||||
|
||||
// Generate new knock sequences with time skew support
|
||||
@ -120,12 +176,6 @@ func rotateSequence(handle *pcap.Handle) {
|
||||
}
|
||||
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)))
|
||||
|
||||
@ -134,31 +184,10 @@ func rotateSequence(handle *pcap.Handle) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func handleSuccess(srcip net.IP) {
|
||||
fmt.Println("Success for ", srcip)
|
||||
|
||||
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", srcip, "--dport", "22", "-j", "ACCEPT")
|
||||
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", fmt.Sprint(srcip), "--dport", "22", "-j", "ACCEPT")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("failed to execute iptables command for '%s': %v", srcip, err)
|
||||
|
Reference in New Issue
Block a user