- 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>
52 lines
2.1 KiB
Go
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)
|
|
}
|
|
}
|