package trace import ( "strings" "testing" "housecarl/internal/api" ) // Lines nft 1.1.3's `nft monitor trace` printed for a connection to a // blocked port, and for one that loopback accepted. const sample = `trace id 38ff3662 inet housecarl-trace pre packet: iif "wan" ip saddr 198.51.100.7 ip daddr 203.0.113.10 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 44978 ip protocol 6 ip length 60 tcp sport 43168 tcp dport 9999 tcp flags == syn tcp window 65495 trace id 38ff3662 inet housecarl-trace pre rule ip saddr 198.51.100.7 meta nftrace set 1 (verdict continue) trace id 38ff3662 inet housecarl-trace pre policy accept trace id 38ff3662 inet housecarl input packet: iif "wan" ip saddr 198.51.100.7 ip daddr 203.0.113.10 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 44978 ip protocol 6 ip length 60 tcp sport 43168 tcp dport 9999 tcp flags == syn tcp window 65495 trace id 38ff3662 inet housecarl input rule ct state established,related counter packets 0 bytes 0 accept comment "hc:b-established" (verdict continue) trace id 38ff3662 inet housecarl input rule tcp dport 9999 counter packets 1 bytes 60 drop comment "hc:r12 block 9999" (verdict drop) trace id a232d9a0 inet housecarl output packet: oif "lo" ip saddr 127.0.0.1 ip daddr 127.0.0.1 ip protocol 6 tcp sport 43168 tcp dport 22 tcp flags == syn trace id a232d9a0 inet housecarl output policy accept trace id a232d9a0 ip other input rule ip saddr 127.0.0.1 accept (verdict accept) trace id 77 inet housecarl forward packet: iif "lan" oif "wan" ip saddr 192.168.1.20 ip daddr 198.51.100.10 tcp sport 3500 tcp dport 7000 tcp flags == syn trace id 77 inet housecarl forward rule ip saddr @trusted_v4 counter accept comment "hc:r40 trusted" (verdict accept) trace id 77 inet housecarl srcnat rule oifname "wan" counter masquerade comment "hc:s91" (verdict accept)` func TestTracer(t *testing.T) { var tr Tracer for _, l := range strings.Split(sample, "\n") { tr.Line(l) } ps := tr.Packets() if len(ps) != 3 { t.Fatalf("packets: %+v", ps) } p := ps[0] if p.In != "wan" || p.Source != "198.51.100.7" || p.DestinationPort != 9999 || p.Protocol != "tcp" || p.TCPFlags != "syn" { t.Errorf("headers: %+v", p) } if len(p.Steps) != 2 || p.Verdict != "dropped" || p.DecidedBy.Mark != "r12" || p.DecidedBy.Chain != "input" { t.Errorf("steps: %+v", p) } if q := ps[1]; q.Verdict != "accepted" || q.Out != "lo" || len(q.Steps) != 2 || q.DecidedBy.Kind != "policy" { t.Errorf("second: %+v", q) } // Accepted by the forward rule, not by source NAT, which came after. if q := ps[2]; q.Verdict != "accepted" || q.DecidedBy.Mark != "r40" { t.Errorf("third: decided by %+v", q.DecidedBy) } } // The kernel reuses trace ids: a connection's next packet can come with // the same one. Entering a chain again starts another packet. func TestReusedID(t *testing.T) { var tr Tracer for _, l := range strings.Split(`trace id 5 inet housecarl output packet: oif "lo" ip saddr 127.0.0.1 ip daddr 127.0.0.1 tcp sport 49078 tcp dport 8070 tcp flags == syn trace id 5 inet housecarl output policy accept trace id 5 inet housecarl srcnat packet: oif "lo" ip saddr 127.0.0.1 ip daddr 127.0.0.1 tcp sport 49078 tcp dport 8070 tcp flags == syn trace id 5 inet housecarl srcnat policy accept trace id 5 inet housecarl output packet: oif "lo" ip saddr 127.0.0.1 ip daddr 127.0.0.1 tcp sport 49078 tcp dport 8070 tcp flags == ack trace id 5 inet housecarl output policy accept`, "\n") { tr.Line(l) } ps := tr.Packets() if len(ps) != 2 || len(ps[0].Steps) != 2 || len(ps[1].Steps) != 1 || ps[1].TCPFlags != "ack" { t.Errorf("packets: %+v", ps) } } func TestScript(t *testing.T) { s, err := Script(api.TraceInput{Address: "192.168.1.50", Port: 443}) if err != nil { t.Fatal(err) } for _, want := range []string{ "table inet housecarl-trace {}\ndelete table inet housecarl-trace\n", "type filter hook prerouting priority -301; policy accept;", "ip saddr 192.168.1.50/32 meta l4proto { tcp, udp, sctp } th dport 443 limit rate 20/second meta nftrace set 1", "ip daddr 192.168.1.50/32 meta l4proto { tcp, udp, sctp } th sport 443 limit rate 20/second meta nftrace set 1", } { if !strings.Contains(s, want) { t.Errorf("missing %q in\n%s", want, s) } } if _, err := Script(api.TraceInput{}); err == nil { t.Error("tracing everything") } if _, err := Script(api.TraceInput{Port: 22, Protocol: "icmp"}); err == nil { t.Error("ICMP with a port") } } // In shadow mode Housecarl's rules accept what they would drop: the // packet's fate is the other table's, and the would-be drop is noted. func TestShadowTrace(t *testing.T) { var tr Tracer for _, l := range strings.Split(`trace id 9 ip main input packet: iif "wan" ip saddr 198.51.100.7 ip daddr 203.0.113.10 tcp sport 4000 tcp dport 9999 tcp flags == syn trace id 9 ip main input rule tcp dport 9999 accept (verdict accept) trace id 9 inet housecarl input rule tcp dport 9999 counter packets 1 bytes 60 accept comment "hc:r12 [would drop] block 9999" (verdict accept) trace id 10 ip main input packet: iif "wan" ip saddr 198.51.100.7 ip daddr 203.0.113.10 tcp sport 4001 tcp dport 7000 tcp flags == syn trace id 10 ip main input rule tcp dport 7000 accept (verdict accept) trace id 10 inet housecarl input rule counter packets 1 bytes 60 comment "hc:p-input [would drop]" (verdict continue) trace id 10 inet housecarl input policy accept trace id 11 ip main input packet: iif "wan" ip saddr 198.51.100.7 ip daddr 203.0.113.10 tcp sport 4002 tcp dport 22 tcp flags == syn trace id 11 ip main input rule tcp dport 22 accept (verdict accept) trace id 11 inet housecarl input rule tcp dport 22 counter packets 1 bytes 60 accept comment "hc:r3 ssh" (verdict accept)`, "\n") { tr.Line(l) } ps := tr.Packets() if len(ps) != 3 { t.Fatalf("packets: %+v", ps) } if p := ps[0]; p.Verdict != "accepted" || p.WouldDrop == nil || p.WouldDrop.Mark != "r12" || p.DecidedBy.Table != "ip main" { t.Errorf("first: %+v", p) } if p := ps[1]; p.Verdict != "accepted" || p.WouldDrop == nil || p.WouldDrop.Mark != "p-input" { t.Errorf("second: %+v", p) } if p := ps[2]; p.WouldDrop != nil || p.DecidedBy.Mark != "r3" { t.Errorf("third: %+v", p) } }