Reject ORIGDEST on forwarded rules and mixed-family lists
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:18:48 +10:00
parent 34cf6dc9ab
commit ff5a52b9e2
2 changed files with 41 additions and 9 deletions
+15 -6
View File
@@ -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 {
+26 -3
View File
@@ -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)
}
}