Match source/dest zones and addresses on conntrack rules
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

This commit is contained in:
2026-10-03 22:56:26 +10:00
parent c9adff32f6
commit 3c5f1cacd4
2 changed files with 99 additions and 16 deletions
+46 -16
View File
@@ -213,35 +213,65 @@ func (c *Compiler) compileBlrules(state *FirewallState) error {
}
func (c *Compiler) compileConntrack(state *FirewallState) error {
fwZone := c.cfg.FirewallZone()
for i, ct := range c.cfg.Conntrack {
tag := fmt.Sprintf("conntrack:%d", i)
chains := []string{"raw_prerouting"}
switch ct.Chain {
case config.ConntrackOutput:
chains = []string{"raw_output"}
case config.ConntrackBoth:
chains = []string{"raw_prerouting", "raw_output"}
srcs, dsts := zoneSpecs(ct.Source), zoneSpecs(ct.Dest)
if len(srcs) == 0 {
srcs = []config.ZoneSpec{{}}
}
if len(dsts) == 0 {
dsts = []config.ZoneSpec{{}}
}
matches, err := l4Matches(ct.Proto, ct.DPort, nil)
if err != nil {
return fmt.Errorf("conntrack[%d]: %w", i, err)
for _, src := range srcs {
chains := []string{"raw_prerouting"}
switch {
case ct.Chain == config.ConntrackOutput, ct.Chain == "" && src.Zone == fwZone:
chains = []string{"raw_output"}
case ct.Chain == config.ConntrackBoth:
chains = []string{"raw_prerouting", "raw_output"}
}
for _, srcAddr := range splitAddrs(src.Addr) {
for _, dst := range dsts {
for _, dstAddr := range splitAddrs(dst.Addr) {
for _, chain := range chains {
if err := c.compileConntrackPair(state, tag, chain, ct, src.Zone, srcAddr, dst.Zone, dstAddr); err != nil {
return fmt.Errorf("conntrack[%d]: %w", i, err)
}
}
}
}
}
}
}
return nil
}
for _, chain := range chains {
// compileConntrackPair matches iif of the source zone in raw_prerouting and oif of the dest zone in raw_output.
func (c *Compiler) compileConntrackPair(state *FirewallState, tag, chain string, ct config.ConntrackRule,
srcZone, srcAddr, dstZone, dstAddr string) error {
if ct.Action == config.ConntrackHelper {
return nil
}
srcIfaces, dstIfaces := c.resolveZoneInterfaces(srcZone, srcAddr), []string{""}
if chain == "raw_output" {
srcIfaces, dstIfaces = []string{""}, c.resolveZoneInterfaces(dstZone, dstAddr)
}
for _, srcIface := range srcIfaces {
for _, dstIface := range dstIfaces {
matches, err := c.buildMatchExprs(srcIface, dstIface, chain, ct.Proto, ct.DPort, ct.SPort, srcAddr, dstAddr)
if err != nil {
return err
}
for _, m := range matches {
exprs := append([]expr.Any{}, m.exprs...)
exprs := m.exprs
switch ct.Action {
case config.ConntrackNoTrack:
exprs = append(exprs, &expr.Notrack{})
case config.ConntrackHelper:
continue
case config.ConntrackDrop:
exprs = append(exprs, &expr.Verdict{Kind: expr.VerdictDrop})
}
state.Rules[chain] = append(state.Rules[chain], ManagedRule{
Chain: chain,
Exprs: exprs,
+53
View File
@@ -2416,3 +2416,56 @@ func TestCompile_OrigDestForwardRejected(t *testing.T) {
t.Fatalf("Compile() error = %v, want forwarded ORIGDEST rejection", err)
}
}
func TestCompile_ConntrackZones(t *testing.T) {
tests := []struct {
name string
ct config.ConntrackRule
want map[string][]string
}{
{
name: "source zone matches iif",
ct: config.ConntrackRule{Action: config.ConntrackNoTrack, Source: "net", Dest: "fw", Proto: "udp", DPort: config.PortSpec{"53"}},
want: map[string][]string{"raw_prerouting": {"iif=eth0"}},
},
{
name: "source and dest addresses",
ct: config.ConntrackRule{Action: config.ConntrackDrop, Source: "net:192.0.2.1,198.51.100.1", Dest: "fw:203.0.113.1"},
want: map[string][]string{"raw_prerouting": {"iif=eth0 saddr=192.0.2.1 daddr=203.0.113.1", "iif=eth0 saddr=198.51.100.1 daddr=203.0.113.1"}},
},
{
name: "fw source goes to raw_output with dest oif",
ct: config.ConntrackRule{Action: config.ConntrackNoTrack, Source: "fw", Dest: "net,lan"},
want: map[string][]string{"raw_output": {"oif=eth0", "oif=eth1"}},
},
{
name: "all matches no interface",
ct: config.ConntrackRule{Action: config.ConntrackNoTrack, Source: "all", Dest: "fw:192.0.2.53"},
want: map[string][]string{"raw_prerouting": {"daddr=192.0.2.53"}},
},
{
name: "interface-less zone fails closed",
ct: config.ConntrackRule{Action: config.ConntrackNoTrack, Source: "dmz", Dest: "fw"},
want: map[string][]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := mustCompile(t, listCfg(func(cfg *config.Config) {
cfg.Zones["lan"] = config.Zone{Type: config.ZoneIP}
cfg.Zones["dmz"] = config.Zone{Type: config.ZoneIP}
cfg.Interfaces = append(cfg.Interfaces, config.Interface{Zone: "lan", Interface: "eth1"})
cfg.Conntrack = []config.ConntrackRule{tt.ct}
}))
got := map[string][]string{}
for _, chain := range []string{"raw_prerouting", "raw_output"} {
for _, r := range taggedRules(state, chain, "conntrack:0:"+chain) {
got[chain] = append(got[chain], describeRule(r))
}
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("rules = %v, want %v", got, tt.want)
}
})
}
}