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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-19 20:22:34 +01:00
1 parent 98e11a8bec
commit a45c893830
34 files changed
+1830 -41

No files matched your search

+7 -2
View File
@@ -99,7 +99,7 @@ type ruleFlags struct {
states, statuses, days []string
hours, limit, quota string
connLimit, groupV4, groupV6 int
log, disabled bool
log, disabled, everyPacket bool
before, after int64
first bool
}
@@ -122,6 +122,7 @@ func (r *ruleFlags) register(f *pflag.FlagSet) {
f.StringVar(&r.raw, "raw", "", "an nftables rule written by hand instead of the match flags")
f.StringVar(&r.comment, "comment", "", "a comment")
f.BoolVar(&r.disabled, "disabled", false, "keep the rule but don't use it")
f.BoolVar(&r.everyPacket, "every-packet", false, "check every packet, so a drop or reject also stops connections already open")
}
var amountRe = regexp.MustCompile(`^(\d+)\s*([a-zA-Z]*)$`)
@@ -244,7 +245,7 @@ func humanBytes(n int64) string {
func (r *ruleFlags) rule(chain string) (model.Rule, error) {
rule := model.Rule{Chain: chain, Match: r.match(), Action: r.action, RejectWith: r.rejectWith, CtState: r.states, CtStatus: r.statuses,
Days: r.days, ConnLimit: r.connLimit, Log: r.log, LogPrefix: r.logPrefix, Raw: r.raw, Comment: r.comment, Disabled: r.disabled}
Days: r.days, ConnLimit: r.connLimit, EveryPacket: r.everyPacket, Log: r.log, LogPrefix: r.logPrefix, Raw: r.raw, Comment: r.comment, Disabled: r.disabled}
if r.hours != "" {
from, to, ok := strings.Cut(r.hours, "-")
if !ok {
@@ -383,6 +384,7 @@ output (from it) or early (before connection tracking: cheap drops).
"status": {"ct_status", r.CtStatus}, "days": {"days", r.Days}, "conn-limit": {"conn_limit", r.ConnLimit},
"log": {"log", r.Log}, "log-prefix": {"log_prefix", r.LogPrefix}, "raw": {"raw", r.Raw}, "comment": {"comment", r.Comment},
"disabled": {"disabled", r.Disabled}, "group-v4": {"group_v4", r.GroupV4}, "group-v6": {"group_v6", r.GroupV6},
"every-packet": {"every_packet", r.EveryPacket},
} {
if fl.Changed(flag) {
p[field[0].(string)] = field[1]
@@ -507,6 +509,9 @@ func ruleExtras(r model.Rule) string {
if r.ConnLimit > 0 {
x = append(x, fmt.Sprintf("over %d connections", r.ConnLimit))
}
if r.EveryPacket {
x = append(x, "every packet")
}
if len(x) == 0 {
return ""
}