Files
abrown38andClaude Opus 5.5 a45c893830 Connections, "every packet" rules, and a packet tracer
- Connections: the kernel's tracked connections, read and closed over
  ctnetlink (internal/conntrack, raw netlink like the drop log), with
  their NAT and TCP state; closing one makes the rules check its next
  packet again (GET /connections, POST /connections/close; hc
  connections; Traffic → Connections).
- Rules can check every packet (every_packet, --every-packet): before
  established connections are accepted, so a drop or reject also stops
  connections already open. They can't accept.
- A packet tracer (internal/trace): a table of its own, inet
  housecarl-trace, marks the packets a filter picks, `nft monitor trace`
  reports every rule and policy they meet in every table, and the tracer
  names what accepted or dropped them, Housecarl's rules by name (POST,
  GET, DELETE /trace; hc trace; Traffic → Trace). One trace at a time,
  at most 200 packets and 20 a second; the table goes when it ends.

e2e: 157 checks, among them an upload a normal block doesn't stop and an
every-packet one does, and a trace naming the blocking rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:22:34 +01:00

52 lines
2.1 KiB
Go

package conntrack
import (
"encoding/binary"
"net/netip"
"testing"
)
func nested(typ uint16, parts ...[]byte) []byte {
var b []byte
for _, p := range parts {
b = append(b, p...)
}
return attr(typ|nlaNested, b)
}
func tupleAttr(typ uint16, src, dst string, proto uint8, sport, dport uint16) []byte {
s, d := netip.MustParseAddr(src), netip.MustParseAddr(dst)
return nested(typ,
nested(ctaTupleIP, attr(ctaIPv4Src, s.AsSlice()), attr(ctaIPv4Dst, d.AsSlice())),
nested(ctaTupleProto, attr(ctaProtoNum, []byte{proto}), attr(ctaProtoSrcPort, be16(sport)), attr(ctaProtoDstPort, be16(dport))))
}
// A port-forwarded TCP connection from the internet, as ctnetlink
// describes it: the reply comes from the inside host.
func TestParse(t *testing.T) {
status, timeout := make([]byte, 4), make([]byte, 4)
binary.BigEndian.PutUint32(status, statusAssured|statusDstNAT)
binary.BigEndian.PutUint32(timeout, 431999)
msg := []byte{2, 0, 0, 0} // nfgenmsg: AF_INET
msg = append(msg, tupleAttr(ctaTupleOrig, "198.51.100.7", "203.0.113.10", 6, 51432, 8443)...)
msg = append(msg, tupleAttr(ctaTupleReply, "192.168.1.10", "198.51.100.7", 6, 443, 51432)...)
msg = append(msg, attr(ctaStatus, status)...)
msg = append(msg, attr(ctaTimeout, timeout)...)
msg = append(msg, nested(ctaProtoinfo, nested(ctaProtoinfoTCP, attr(ctaProtoinfoTCPState, []byte{3})))...)
c, ok := parse(msg)
if !ok {
t.Fatal("not parsed")
}
if c.Protocol != "tcp" || c.Source != "198.51.100.7" || c.SourcePort != 51432 || c.Destination != "203.0.113.10" || c.DestinationPort != 8443 ||
c.DNATTo != "192.168.1.10" || c.DNATPort != 443 || c.SNATTo != "" || c.State != "established" || !c.Assured || c.Timeout != 431999 {
t.Errorf("parsed %+v", c)
}
// Masqueraded on the way out: the reply comes back to the WAN address.
msg = []byte{2, 0, 0, 0}
msg = append(msg, tupleAttr(ctaTupleOrig, "192.168.1.20", "198.51.100.9", 17, 5353, 53)...)
msg = append(msg, tupleAttr(ctaTupleReply, "198.51.100.9", "203.0.113.10", 17, 53, 5353)...)
if c, _ := parse(msg); c.SNATTo != "203.0.113.10" || c.DNATTo != "" || c.Protocol != "udp" {
t.Errorf("masqueraded: %+v", c)
}
}