diff --git a/internal/nftables/compiler.go b/internal/nftables/compiler.go index 52ad4eb..7e300c6 100644 --- a/internal/nftables/compiler.go +++ b/internal/nftables/compiler.go @@ -423,6 +423,10 @@ func (c *Compiler) compileZonePair(state *FirewallState, tag, srcZone, srcAddr, srcIfaces := c.resolveZoneInterfaces(srcZone) dstIfaces := c.resolveZoneInterfaces(dstZone) chain := c.selectChain(srcZone, dstZone, fwZone) + // ponytail: forward daddr is post-DNAT; lift with `ct original daddr` (expr.Ct Direction, google/nftables v0.3.0). + if origDest != "" && chain == "forward" { + return fmt.Errorf("origdest: ORIGDEST on forwarded rules is not supported yet") + } for _, srcIface := range srcIfaces { for _, dstIface := range dstIfaces { @@ -430,7 +434,6 @@ func (c *Compiler) compileZonePair(state *FirewallState, tag, srcZone, srcAddr, if err != nil { return err } - // ponytail: matches daddr, so a forward rule never sees a pre-DNAT address; needs `ct original daddr` (google/nftables Ct.Direction, >v0.2.0). if origDest != "" { od, err := matchOrigDest(origDest) if err != nil { @@ -1410,11 +1413,17 @@ func matchDestCIDR(cidr string) ([]expr.Any, error) { // matchOrigDest guards the daddr match with the address's nfproto so it is family-correct in the inet table. func matchOrigDest(addr string) ([]expr.Any, error) { - first, _, _ := strings.Cut(strings.TrimPrefix(addr, "!"), ",") - first, _, _ = strings.Cut(first, "/") - proto := byte(unix.NFPROTO_IPV6) - if ip := net.ParseIP(first); ip != nil && ip.To4() != nil { - proto = unix.NFPROTO_IPV4 + var proto byte + for i, a := range strings.Split(strings.TrimPrefix(addr, "!"), ",") { + a, _, _ = strings.Cut(a, "/") + p := byte(unix.NFPROTO_IPV6) + if ip := net.ParseIP(a); ip != nil && ip.To4() != nil { + p = unix.NFPROTO_IPV4 + } + if i > 0 && p != proto { + return nil, fmt.Errorf("%q mixes IPv4 and IPv6 addresses", addr) + } + proto = p } dst, err := matchDestCIDR(addr) if err != nil { diff --git a/internal/nftables/compiler_test.go b/internal/nftables/compiler_test.go index 973917f..890cfc6 100644 --- a/internal/nftables/compiler_test.go +++ b/internal/nftables/compiler_test.go @@ -3,6 +3,7 @@ package nftables import ( "encoding/binary" "fmt" + "net" "reflect" "strings" "testing" @@ -1781,12 +1782,12 @@ func describeRule(r ManagedRule) string { parts = append(parts, "oif="+strings.TrimRight(string(cmp.Data), "\x00")) } case *expr.Payload: - if m.Base == expr.PayloadBaseNetworkHeader && m.Len == 4 { - name := map[uint32]string{12: "saddr", 16: "daddr"}[m.Offset] + if m.Base == expr.PayloadBaseNetworkHeader && (m.Len == 4 || m.Len == 16) { + name := map[uint32]string{12: "saddr", 16: "daddr", 8: "saddr", 24: "daddr"}[m.Offset] if cmp.Op == expr.CmpOpNeq { name = "!" + name } - parts = append(parts, fmt.Sprintf("%s=%d.%d.%d.%d", name, cmp.Data[0], cmp.Data[1], cmp.Data[2], cmp.Data[3])) + parts = append(parts, name+"="+net.IP(cmp.Data).String()) } } } @@ -1870,6 +1871,11 @@ func TestCompile_CommaZoneLists(t *testing.T) { rule: config.Rule{Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, OrigDest: "203.0.113.5"}, want: map[string][]string{"input": {"iif=eth0 daddr=203.0.113.5"}}, }, + { + name: "accept ipv6 origdest", + rule: config.Rule{Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, OrigDest: "2001:db8::5"}, + want: map[string][]string{"input": {"iif=eth0 daddr=2001:db8::5"}}, + }, { name: "blrule zone list", blrule: &config.BlruleRule{Action: config.BlruleDrop, Source: "net,anycast", Dest: "fw"}, @@ -2290,4 +2296,21 @@ func TestMatchOrigDest_FamilyGuard(t *testing.T) { t.Errorf("%s: missing nfproto %d guard: %v", addr, want, e[:2]) } } + if _, err := matchOrigDest("!203.0.113.5,2001:db8::5"); err == nil { + t.Error("mixed IPv4/IPv6 origdest: want error") + } +} + +func TestCompile_OrigDestForwardRejected(t *testing.T) { + cfg := &config.Config{ + Settings: config.Settings{TableName: "test", AddressFamily: config.FamilyINET}, + Zones: map[string]config.Zone{"fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "svr": {Type: config.ZoneIP}}, + Interfaces: []config.Interface{{Zone: "net", Interface: "eth0"}, {Zone: "svr", Interface: "eth2"}}, + Rules: []config.Rule{{Action: config.RuleAccept, Source: "net", Dest: "svr", Proto: "tcp", DPort: config.PortSpec{"80"}, OrigDest: "203.0.113.5"}}, + PortGroups: make(map[string]config.PortGroup), + } + _, err := NewCompiler(cfg).Compile() + if err == nil || !strings.Contains(err.Error(), "not supported yet") { + t.Fatalf("Compile() error = %v, want forwarded ORIGDEST rejection", err) + } }