diff --git a/internal/nftables/compiler.go b/internal/nftables/compiler.go index 2fc1e4d..9bd47eb 100644 --- a/internal/nftables/compiler.go +++ b/internal/nftables/compiler.go @@ -972,7 +972,19 @@ func l4Matches(proto string, dports, sports config.PortSpec) ([]l4Match, error) if proto != "" && p == "" { return nil, fmt.Errorf("empty element in proto list %q", proto) } - isICMP := strings.EqualFold(p, "icmp") || strings.EqualFold(p, "icmpv6") || strings.EqualFold(p, "ipv6-icmp") + var pm []expr.Any + isICMP := false + if p != "" { + n, err := protoNumber(p) + if err != nil { + return nil, err + } + isICMP = n == unix.IPPROTO_ICMP || n == unix.IPPROTO_ICMPV6 + if (len(dports) > 0 && !isICMP || len(sports) > 0) && !hasPorts(n) { + return nil, fmt.Errorf("protocol %q does not support ports", p) + } + pm = matchProtoNum(n) + } parseD := parsePortOrRange if isICMP { if len(protos) > 1 && len(dports) > 0 { @@ -980,14 +992,6 @@ func l4Matches(proto string, dports, sports config.PortSpec) ([]l4Match, error) } parseD = matchICMPType } - var pm []expr.Any - if p != "" { - m, err := matchProto(p) - if err != nil { - return nil, err - } - pm = m - } dalts, err := portAlternatives(dports, parseD) if err != nil { return nil, err @@ -1051,36 +1055,27 @@ func matchIfaceName(input bool, name string) []expr.Any { } } -func matchProto(proto string) ([]expr.Any, error) { - var protoNum byte - switch strings.ToLower(proto) { - case "tcp": - protoNum = unix.IPPROTO_TCP - case "udp": - protoNum = unix.IPPROTO_UDP - case "icmp": - protoNum = unix.IPPROTO_ICMP - case "icmpv6", "ipv6-icmp": - protoNum = unix.IPPROTO_ICMPV6 - case "gre": - protoNum = 47 - case "esp": - protoNum = 50 - case "ah": - protoNum = 51 - case "sctp": - protoNum = unix.IPPROTO_SCTP - default: - n, err := strconv.ParseUint(proto, 10, 8) - if err != nil { - return nil, fmt.Errorf("unknown protocol %q", proto) - } - protoNum = byte(n) +var protoNumbers = map[string]byte{ + "icmp": 1, "igmp": 2, "ipip": 4, "ipencap": 4, "tcp": 6, "udp": 17, + "gre": 47, "esp": 50, "ah": 51, "icmpv6": 58, "ipv6-icmp": 58, + "ospf": 89, "ospfigp": 89, "pim": 103, "vrrp": 112, "l2tp": 115, + "sctp": 132, "udplite": 136, +} + +// protoNumber resolves a common IANA protocol name or a 0-255 number. +func protoNumber(proto string) (byte, error) { + if n, ok := protoNumbers[strings.ToLower(proto)]; ok { + return n, nil } - return []expr.Any{ - &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, - &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{protoNum}}, - }, nil + n, err := strconv.ParseUint(proto, 10, 8) + if err != nil { + return 0, fmt.Errorf("unknown protocol %q", proto) + } + return byte(n), nil +} + +func hasPorts(proto byte) bool { + return proto == unix.IPPROTO_TCP || proto == unix.IPPROTO_UDP || proto == unix.IPPROTO_SCTP || proto == unix.IPPROTO_UDPLITE } func matchDPort(port uint16) []expr.Any { diff --git a/internal/nftables/compiler_test.go b/internal/nftables/compiler_test.go index 939c4a9..912ce65 100644 --- a/internal/nftables/compiler_test.go +++ b/internal/nftables/compiler_test.go @@ -1581,6 +1581,24 @@ func TestCompile_PortAndProtoLists(t *testing.T) { chain: "prerouting", want: []want{{6, "53"}, {17, "53"}}, }, + { + name: "protocol names", + rule: config.Rule{Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "ospf,OSPFIGP,igmp,gre,esp,ah,vrrp,pim,ipencap,ipv6-icmp"}, + chain: "input", + want: []want{{89, ""}, {89, ""}, {2, ""}, {47, ""}, {50, ""}, {51, ""}, {112, ""}, {103, ""}, {4, ""}, {58, ""}}, + }, + { + name: "protocol numbers", + rule: config.Rule{Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "0,89,255"}, + chain: "input", + want: []want{{0, ""}, {89, ""}, {255, ""}}, + }, + { + name: "numeric tcp with port", + rule: config.Rule{Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "6,udplite", DPort: config.PortSpec{"22"}}, + chain: "input", + want: []want{{6, "22"}, {136, "22"}}, + }, } for _, tt := range tests { @@ -1724,6 +1742,9 @@ func TestCompile_ListErrors(t *testing.T) { {"icmp code out of range", config.Rule{Proto: "icmp", DPort: config.PortSpec{"3/256"}}}, {"unknown proto in list", config.Rule{Proto: "tcp,udpp", DPort: config.PortSpec{"53"}}}, {"proto number out of range", config.Rule{Proto: "256"}}, + {"unknown proto name", config.Rule{Proto: "bogus"}}, + {"dport with ospf", config.Rule{Proto: "ospf", DPort: config.PortSpec{"80"}}}, + {"sport with gre", config.Rule{Proto: "gre", SPort: config.PortSpec{"80"}}}, {"dport with icmp in proto list", config.Rule{Proto: "icmp,tcp", DPort: config.PortSpec{"80"}}}, {"ratelimit with port list", config.Rule{Proto: "tcp", DPort: config.PortSpec{"80,443"}, RateLimit: "10/sec"}}, {"connlimit with proto list", config.Rule{Proto: "tcp,udp", DPort: config.PortSpec{"53"}, ConnLimit: "10"}},