From e48682c362717828c979b43b82de5cf2e0efe52a Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Sat, 22 Jul 2023 16:54:40 -0400 Subject: [PATCH] Add nftables backend to portmap Signed-off-by: Dan Winship --- plugins/meta/portmap/main.go | 78 ++++ plugins/meta/portmap/portmap_nftables.go | 344 ++++++++++++++++++ plugins/meta/portmap/portmap_nftables_test.go | 135 +++++++ plugins/meta/portmap/portmap_test.go | 101 ++++- 4 files changed, 647 insertions(+), 11 deletions(-) create mode 100644 plugins/meta/portmap/portmap_nftables.go create mode 100644 plugins/meta/portmap/portmap_nftables_test.go diff --git a/plugins/meta/portmap/main.go b/plugins/meta/portmap/main.go index ea6ba1ea1..48bf8a5f6 100644 --- a/plugins/meta/portmap/main.go +++ b/plugins/meta/portmap/main.go @@ -37,6 +37,7 @@ import ( "github.com/containernetworking/cni/pkg/types" current "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/utils" bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) @@ -46,6 +47,12 @@ type PortMapper interface { unforwardPorts(config *PortMapConf) error } +// These are vars rather than consts so we can "&" them +var ( + iptablesBackend = "iptables" + nftablesBackend = "nftables" +) + // PortMapEntry corresponds to a single entry in the port_mappings argument, // see CONVENTIONS.md type PortMapEntry struct { @@ -61,6 +68,7 @@ type PortMapConf struct { mapper PortMapper // Generic config + Backend *string `json:"backend,omitempty"` SNAT *bool `json:"snat,omitempty"` ConditionsV4 *[]string `json:"conditionsV4"` ConditionsV6 *[]string `json:"conditionsV6"` @@ -234,6 +242,21 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") } + err := validateBackend(&conf) + if err != nil { + return nil, nil, err + } + switch *conf.Backend { + case iptablesBackend: + conf.mapper = &portMapperIPTables{} + + case nftablesBackend: + conf.mapper = &portMapperNFTables{} + + default: + return nil, nil, fmt.Errorf("unrecognized backend %q", *conf.Backend) + } + // Reject invalid port numbers for _, pm := range conf.RuntimeConfig.PortMaps { if pm.ContainerPort <= 0 { @@ -273,3 +296,58 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er return &conf, result, nil } + +// validateBackend validates and/or sets conf.Backend +func validateBackend(conf *PortMapConf) error { + backendConfig := make(map[string][]string) + + if conf.ExternalSetMarkChain != nil { + backendConfig[iptablesBackend] = append(backendConfig[iptablesBackend], "externalSetMarkChain") + } + if conditionsBackend := detectBackendOfConditions(conf.ConditionsV4); conditionsBackend != "" { + backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV4") + } + if conditionsBackend := detectBackendOfConditions(conf.ConditionsV6); conditionsBackend != "" { + backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV6") + } + + // If backend wasn't requested explicitly, default to iptables, unless it is not + // available (and nftables is). FIXME: flip this default at some point. + if conf.Backend == nil { + if !utils.SupportsIPTables() && utils.SupportsNFTables() { + conf.Backend = &nftablesBackend + } else { + conf.Backend = &iptablesBackend + } + } + + // Make sure we dont have config for the wrong backend + var wrongBackend string + if *conf.Backend == iptablesBackend { + wrongBackend = nftablesBackend + } else { + wrongBackend = iptablesBackend + } + if len(backendConfig[wrongBackend]) > 0 { + return fmt.Errorf("%s backend was requested but configuration contains %s-specific options %v", *conf.Backend, wrongBackend, backendConfig[wrongBackend]) + } + + // OK + return nil +} + +// detectBackendOfConditions returns "iptables" if conditions contains iptables +// conditions, "nftables" if it contains nftables conditions, and "" if it is empty. +func detectBackendOfConditions(conditions *[]string) string { + if conditions == nil || len(*conditions) == 0 || (*conditions)[0] == "" { + return "" + } + + // The first token of any iptables condition would start with a hyphen (e.g. "-d", + // "--sport", "-m"). No nftables condition would start that way. (An nftables + // condition might include a negative number, but not as the first token.) + if (*conditions)[0][0] == '-' { + return iptablesBackend + } + return nftablesBackend +} diff --git a/plugins/meta/portmap/portmap_nftables.go b/plugins/meta/portmap/portmap_nftables.go new file mode 100644 index 000000000..f0f209efc --- /dev/null +++ b/plugins/meta/portmap/portmap_nftables.go @@ -0,0 +1,344 @@ +// Copyright 2023 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "net" + + "sigs.k8s.io/knftables" +) + +const ( + tableName = "cni_hostport" + + hostIPHostPortsChain = "hostip_hostports" + hostPortsChain = "hostports" + masqueradingChain = "masquerading" +) + +// The nftables portmap implementation is fairly similar to the iptables implementation: +// we add a rule for each mapping, with a comment containing a hash of the container ID, +// so that we can later reliably delete the rules we want. (This is important because in +// edge cases, it's possible the plugin might see "ADD container A with IP 192.168.1.3", +// followed by "ADD container B with IP 192.168.1.3" followed by "DEL container A with IP +// 192.168.1.3", and we need to make sure that the DEL causes us to delete the rule for +// container A, and not the rule for container B.) This iptables implementation actually +// uses a separate chain per container but there's not really any need for that... +// +// As with pkg/ip/ipmasq_nftables_linux.go, it would be more nftables-y to have a chain +// with a single rule doing a lookup against a map with an element per mapping, rather +// than having a chain with a rule per mapping. But there's no easy, non-racy way to say +// "delete the element 192.168.1.3 from the map, but only if it was added for container A, +// not if it was added for container B". + +type portMapperNFTables struct { + ipv4 knftables.Interface + ipv6 knftables.Interface +} + +// getPortMapNFT creates an nftables.Interface for port mapping for the IP family of ipn +func (pmNFT *portMapperNFTables) getPortMapNFT(ipv6 bool) (knftables.Interface, error) { + var err error + if ipv6 { + if pmNFT.ipv6 == nil { + pmNFT.ipv6, err = knftables.New(knftables.IPv6Family, tableName) + if err != nil { + return nil, err + } + } + return pmNFT.ipv6, nil + } + + if pmNFT.ipv4 == nil { + pmNFT.ipv4, err = knftables.New(knftables.IPv4Family, tableName) + if err != nil { + return nil, err + } + } + return pmNFT.ipv4, err +} + +// forwardPorts establishes port forwarding to a given container IP. +// containerNet.IP can be either v4 or v6. +func (pmNFT *portMapperNFTables) forwardPorts(config *PortMapConf, containerNet net.IPNet) error { + isV6 := (containerNet.IP.To4() == nil) + nft, err := pmNFT.getPortMapNFT(isV6) + if err != nil { + return err + } + + var ipX string + var conditions []string + if isV6 { + ipX = "ip6" + if config.ConditionsV6 != nil { + conditions = *config.ConditionsV6 + } + } else if !isV6 { + ipX = "ip" + if config.ConditionsV4 != nil { + conditions = *config.ConditionsV4 + } + } + + tx := nft.NewTransaction() + + // Ensure basic rule structure + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("CNI portmap plugin"), + }) + + tx.Add(&knftables.Chain{ + Name: "hostports", + }) + tx.Add(&knftables.Chain{ + Name: "hostip_hostports", + }) + + tx.Add(&knftables.Chain{ + Name: "prerouting", + Type: knftables.PtrTo(knftables.NATType), + Hook: knftables.PtrTo(knftables.PreroutingHook), + Priority: knftables.PtrTo(knftables.DNATPriority), + }) + tx.Flush(&knftables.Chain{ + Name: "prerouting", + }) + tx.Add(&knftables.Rule{ + Chain: "prerouting", + Rule: knftables.Concat( + conditions, + "jump", hostIPHostPortsChain, + ), + }) + tx.Add(&knftables.Rule{ + Chain: "prerouting", + Rule: knftables.Concat( + conditions, + "jump", hostPortsChain, + ), + }) + + tx.Add(&knftables.Chain{ + Name: "output", + Type: knftables.PtrTo(knftables.NATType), + Hook: knftables.PtrTo(knftables.OutputHook), + + // DNATPriority is not allowed on OutputHook, even though the "dnat" + // command is. Specify the numeric value instead... + Priority: knftables.PtrTo(knftables.BaseChainPriority("-100")), + }) + tx.Flush(&knftables.Chain{ + Name: "output", + }) + tx.Add(&knftables.Rule{ + Chain: "output", + Rule: knftables.Concat( + conditions, + "jump", hostIPHostPortsChain, + ), + }) + tx.Add(&knftables.Rule{ + Chain: "output", + Rule: knftables.Concat( + conditions, + "fib daddr type local", + "jump", hostPortsChain, + ), + }) + + if *config.SNAT { + tx.Add(&knftables.Chain{ + Name: masqueradingChain, + Type: knftables.PtrTo(knftables.NATType), + Hook: knftables.PtrTo(knftables.PostroutingHook), + Priority: knftables.PtrTo(knftables.SNATPriority), + }) + } + + // Set up this container + for _, e := range config.RuntimeConfig.PortMaps { + useHostIP := false + if e.HostIP != "" { + hostIP := net.ParseIP(e.HostIP) + isHostV6 := (hostIP.To4() == nil) + // Ignore wrong-IP-family HostIPs + if isV6 != isHostV6 { + continue + } + + // Unspecified addresses cannot be used as destination + useHostIP = !hostIP.IsUnspecified() + } + + if useHostIP { + tx.Add(&knftables.Rule{ + Chain: hostIPHostPortsChain, + Rule: knftables.Concat( + ipX, "daddr", e.HostIP, + ipX, "protocol", e.Protocol, + "th dport", e.HostPort, + "dnat", ipX, "addr . port", "to", containerNet.IP, ".", e.ContainerPort, + ), + Comment: &config.ContainerID, + }) + } else { + tx.Add(&knftables.Rule{ + Chain: hostPortsChain, + Rule: knftables.Concat( + ipX, "protocol", e.Protocol, + "th dport", e.HostPort, + "dnat", ipX, "addr . port", "to", containerNet.IP, ".", e.ContainerPort, + ), + Comment: &config.ContainerID, + }) + } + } + + if *config.SNAT { + // Add mark-to-masquerade rules for hairpin and localhost + // In theory we should validate that the original dst IP and port are as + // expected, but *any* traffic matching one of these patterns would need + // to be masqueraded to be able to work correctly anyway. + tx.Add(&knftables.Rule{ + Chain: masqueradingChain, + Rule: knftables.Concat( + ipX, "saddr", containerNet.IP, + ipX, "daddr", containerNet.IP, + "masquerade", + ), + Comment: &config.ContainerID, + }) + if !isV6 { + tx.Add(&knftables.Rule{ + Chain: masqueradingChain, + Rule: knftables.Concat( + ipX, "saddr 127.0.0.1", + ipX, "daddr", containerNet.IP, + "masquerade", + ), + Comment: &config.ContainerID, + }) + } + } + + err = nft.Run(context.TODO(), tx) + if err != nil { + return fmt.Errorf("unable to set up nftables rules for port mappings: %v", err) + } + + return nil +} + +func (pmNFT *portMapperNFTables) checkPorts(config *PortMapConf, containerNet net.IPNet) error { + isV6 := (containerNet.IP.To4() == nil) + + var hostPorts, hostIPHostPorts, masqueradings int + for _, e := range config.RuntimeConfig.PortMaps { + if e.HostIP != "" { + hostIPHostPorts++ + } else { + hostPorts++ + } + } + if *config.SNAT { + masqueradings = len(config.RuntimeConfig.PortMaps) + if isV6 { + masqueradings *= 2 + } + } + + nft, err := pmNFT.getPortMapNFT(isV6) + if err != nil { + return err + } + if hostPorts > 0 { + err := checkPortsAgainstRules(nft, hostPortsChain, config.ContainerID, hostPorts) + if err != nil { + return err + } + } + if hostIPHostPorts > 0 { + err := checkPortsAgainstRules(nft, hostIPHostPortsChain, config.ContainerID, hostIPHostPorts) + if err != nil { + return err + } + } + if masqueradings > 0 { + err := checkPortsAgainstRules(nft, masqueradingChain, config.ContainerID, masqueradings) + if err != nil { + return err + } + } + + return nil +} + +func checkPortsAgainstRules(nft knftables.Interface, chain, comment string, nPorts int) error { + rules, err := nft.ListRules(context.TODO(), chain) + if err != nil { + return err + } + + found := 0 + for _, r := range rules { + if r.Comment != nil && *r.Comment == comment { + found++ + } + } + if found < nPorts { + return fmt.Errorf("missing hostport rules in %q chain", chain) + } + + return nil +} + +// unforwardPorts deletes any nftables rules created by this plugin. +// It should be idempotent - it will not error if the chain does not exist. +func (pmNFT *portMapperNFTables) unforwardPorts(config *PortMapConf) error { + // Always clear both IPv4 and IPv6, just to be sure + for _, family := range []knftables.Family{knftables.IPv4Family, knftables.IPv6Family} { + nft, err := pmNFT.getPortMapNFT(family == knftables.IPv6Family) + if err != nil { + continue + } + + tx := nft.NewTransaction() + for _, chain := range []string{hostPortsChain, hostIPHostPortsChain, masqueradingChain} { + rules, err := nft.ListRules(context.TODO(), chain) + if err != nil { + if knftables.IsNotFound(err) { + continue + } + return fmt.Errorf("could not list rules in table %s: %w", tableName, err) + } + + for _, r := range rules { + if r.Comment != nil && *r.Comment == config.ContainerID { + tx.Delete(r) + } + } + } + + err = nft.Run(context.TODO(), tx) + if err != nil { + return fmt.Errorf("error deleting nftables rules: %w", err) + } + } + + return nil +} diff --git a/plugins/meta/portmap/portmap_nftables_test.go b/plugins/meta/portmap/portmap_nftables_test.go new file mode 100644 index 000000000..04a49dd1f --- /dev/null +++ b/plugins/meta/portmap/portmap_nftables_test.go @@ -0,0 +1,135 @@ +// Copyright 2023 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/knftables" + + "github.com/containernetworking/cni/pkg/types" +) + +var _ = Describe("portmapping configuration (nftables)", func() { + containerID := "icee6giejonei6so" + + for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} { + // Redefine ver inside for scope so real value is picked up by each dynamically defined It() + // See Gingkgo's "Patterns for dynamically generating tests" documentation. + ver := ver + + Describe("nftables rules", func() { + var pmNFT *portMapperNFTables + var ipv4Fake, ipv6Fake *knftables.Fake + BeforeEach(func() { + ipv4Fake = knftables.NewFake(knftables.IPv4Family, tableName) + ipv6Fake = knftables.NewFake(knftables.IPv6Family, tableName) + pmNFT = &portMapperNFTables{ + ipv4: ipv4Fake, + ipv6: ipv6Fake, + } + }) + + It(fmt.Sprintf("[%s] generates correct rules on ADD", ver), func() { + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s", + "backend": "nftables", + "runtimeConfig": { + "portMappings": [ + { "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}, + { "hostPort": 8081, "containerPort": 80, "protocol": "tcp"}, + { "hostPort": 8080, "containerPort": 81, "protocol": "udp"}, + { "hostPort": 8082, "containerPort": 82, "protocol": "udp"}, + { "hostPort": 8083, "containerPort": 83, "protocol": "tcp", "hostIP": "192.168.0.2"}, + { "hostPort": 8084, "containerPort": 84, "protocol": "tcp", "hostIP": "0.0.0.0"}, + { "hostPort": 8085, "containerPort": 85, "protocol": "tcp", "hostIP": "2001:db8:a::1"}, + { "hostPort": 8086, "containerPort": 86, "protocol": "tcp", "hostIP": "::"} + ] + }, + "snat": true, + "conditionsV4": ["a", "b"], + "conditionsV6": ["c", "d"] + }`, ver)) + + conf, _, err := parseConfig(configBytes, "foo") + Expect(err).NotTo(HaveOccurred()) + conf.ContainerID = containerID + + containerNet, err := types.ParseCIDR("10.0.0.2/24") + Expect(err).NotTo(HaveOccurred()) + + err = pmNFT.forwardPorts(conf, *containerNet) + Expect(err).NotTo(HaveOccurred()) + + expectedRules := strings.TrimSpace(` +add table ip cni_hostport { comment "CNI portmap plugin" ; } +add chain ip cni_hostport hostip_hostports +add chain ip cni_hostport hostports +add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; } +add chain ip cni_hostport output { type nat hook output priority -100 ; } +add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; } +add rule ip cni_hostport hostip_hostports ip daddr 192.168.0.2 ip protocol tcp th dport 8083 dnat ip addr . port to 10.0.0.2 . 83 comment "icee6giejonei6so" +add rule ip cni_hostport hostports ip protocol tcp th dport 8080 dnat ip addr . port to 10.0.0.2 . 80 comment "icee6giejonei6so" +add rule ip cni_hostport hostports ip protocol tcp th dport 8081 dnat ip addr . port to 10.0.0.2 . 80 comment "icee6giejonei6so" +add rule ip cni_hostport hostports ip protocol udp th dport 8080 dnat ip addr . port to 10.0.0.2 . 81 comment "icee6giejonei6so" +add rule ip cni_hostport hostports ip protocol udp th dport 8082 dnat ip addr . port to 10.0.0.2 . 82 comment "icee6giejonei6so" +add rule ip cni_hostport hostports ip protocol tcp th dport 8084 dnat ip addr . port to 10.0.0.2 . 84 comment "icee6giejonei6so" +add rule ip cni_hostport masquerading ip saddr 10.0.0.2 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so" +add rule ip cni_hostport masquerading ip saddr 127.0.0.1 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so" +add rule ip cni_hostport output a b jump hostip_hostports +add rule ip cni_hostport output a b fib daddr type local jump hostports +add rule ip cni_hostport prerouting a b jump hostip_hostports +add rule ip cni_hostport prerouting a b jump hostports +`) + actualRules := strings.TrimSpace(ipv4Fake.Dump()) + Expect(actualRules).To(Equal(expectedRules)) + + // Disable snat, generate IPv6 rules + *conf.SNAT = false + containerNet, err = types.ParseCIDR("2001:db8::2/64") + Expect(err).NotTo(HaveOccurred()) + + err = pmNFT.forwardPorts(conf, *containerNet) + Expect(err).NotTo(HaveOccurred()) + + expectedRules = strings.TrimSpace(` +add table ip6 cni_hostport { comment "CNI portmap plugin" ; } +add chain ip6 cni_hostport hostip_hostports +add chain ip6 cni_hostport hostports +add chain ip6 cni_hostport output { type nat hook output priority -100 ; } +add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; } +add rule ip6 cni_hostport hostip_hostports ip6 daddr 2001:db8:a::1 ip6 protocol tcp th dport 8085 dnat ip6 addr . port to 2001:db8::2 . 85 comment "icee6giejonei6so" +add rule ip6 cni_hostport hostports ip6 protocol tcp th dport 8080 dnat ip6 addr . port to 2001:db8::2 . 80 comment "icee6giejonei6so" +add rule ip6 cni_hostport hostports ip6 protocol tcp th dport 8081 dnat ip6 addr . port to 2001:db8::2 . 80 comment "icee6giejonei6so" +add rule ip6 cni_hostport hostports ip6 protocol udp th dport 8080 dnat ip6 addr . port to 2001:db8::2 . 81 comment "icee6giejonei6so" +add rule ip6 cni_hostport hostports ip6 protocol udp th dport 8082 dnat ip6 addr . port to 2001:db8::2 . 82 comment "icee6giejonei6so" +add rule ip6 cni_hostport hostports ip6 protocol tcp th dport 8086 dnat ip6 addr . port to 2001:db8::2 . 86 comment "icee6giejonei6so" +add rule ip6 cni_hostport output c d jump hostip_hostports +add rule ip6 cni_hostport output c d fib daddr type local jump hostports +add rule ip6 cni_hostport prerouting c d jump hostip_hostports +add rule ip6 cni_hostport prerouting c d jump hostports +`) + actualRules = strings.TrimSpace(ipv6Fake.Dump()) + Expect(actualRules).To(Equal(expectedRules)) + }) + }) + } +}) diff --git a/plugins/meta/portmap/portmap_test.go b/plugins/meta/portmap/portmap_test.go index fbb69b394..d09e238e4 100644 --- a/plugins/meta/portmap/portmap_test.go +++ b/plugins/meta/portmap/portmap_test.go @@ -35,6 +35,7 @@ var _ = Describe("portmapping configuration", func() { "name": "test", "type": "portmap", "cniVersion": "%s", + "backend": "iptables", "runtimeConfig": { "portMappings": [ { "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}, @@ -42,8 +43,8 @@ var _ = Describe("portmapping configuration", func() { ] }, "snat": false, - "conditionsV4": ["a", "b"], - "conditionsV6": ["c", "d"], + "conditionsV4": ["-a", "b"], + "conditionsV6": ["-c", "d"], "prevResult": { "interfaces": [ {"name": "host"}, @@ -74,8 +75,8 @@ var _ = Describe("portmapping configuration", func() { c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal(ver)) - Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) - Expect(c.ConditionsV6).To(Equal(&[]string{"c", "d"})) + Expect(c.ConditionsV4).To(Equal(&[]string{"-a", "b"})) + Expect(c.ConditionsV6).To(Equal(&[]string{"-c", "d"})) fvar := false Expect(c.SNAT).To(Equal(&fvar)) Expect(c.Name).To(Equal("test")) @@ -94,15 +95,16 @@ var _ = Describe("portmapping configuration", func() { "name": "test", "type": "portmap", "cniVersion": "%s", + "backend": "iptables", "snat": false, - "conditionsV4": ["a", "b"], - "conditionsV6": ["c", "d"] + "conditionsV4": ["-a", "b"], + "conditionsV6": ["-c", "d"] }`, ver)) c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal(ver)) - Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) - Expect(c.ConditionsV6).To(Equal(&[]string{"c", "d"})) + Expect(c.ConditionsV4).To(Equal(&[]string{"-a", "b"})) + Expect(c.ConditionsV6).To(Equal(&[]string{"-c", "d"})) fvar := false Expect(c.SNAT).To(Equal(&fvar)) Expect(c.Name).To(Equal("test")) @@ -113,9 +115,10 @@ var _ = Describe("portmapping configuration", func() { "name": "test", "type": "portmap", "cniVersion": "%s", + "backend": "iptables", "snat": false, - "conditionsV4": ["a", "b"], - "conditionsV6": ["c", "d"], + "conditionsV4": ["-a", "b"], + "conditionsV6": ["-c", "d"], "runtimeConfig": { "portMappings": [ { "hostPort": 0, "containerPort": 80, "protocol": "tcp"} @@ -126,6 +129,82 @@ var _ = Describe("portmapping configuration", func() { Expect(err).To(MatchError("Invalid host port number: 0")) }) + It(fmt.Sprintf("[%s] defaults to iptables when backend is not specified", ver), func() { + // "defaults to iptables" is only true if iptables is installed + // (or if neither iptables nor nftables is installed), but the + // other unit tests would fail if iptables wasn't installed, so + // we know it must be. + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s" + }`, ver)) + c, _, err := parseConfig(configBytes, "container") + Expect(err).NotTo(HaveOccurred()) + Expect(c.CNIVersion).To(Equal(ver)) + Expect(c.Backend).To(Equal(&iptablesBackend)) + Expect(c.Name).To(Equal("test")) + }) + + It(fmt.Sprintf("[%s] uses nftables if requested", ver), func() { + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s", + "backend": "nftables" + }`, ver)) + c, _, err := parseConfig(configBytes, "container") + Expect(err).NotTo(HaveOccurred()) + Expect(c.CNIVersion).To(Equal(ver)) + Expect(c.Backend).To(Equal(&nftablesBackend)) + Expect(c.Name).To(Equal("test")) + }) + + It(fmt.Sprintf("[%s] allows nftables conditions if nftables is requested", ver), func() { + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s", + "backend": "nftables", + "conditionsV4": ["aaa", "bbbb"], + "conditionsV6": ["ccc"] + }`, ver)) + c, _, err := parseConfig(configBytes, "container") + Expect(err).NotTo(HaveOccurred()) + Expect(c.CNIVersion).To(Equal(ver)) + Expect(c.Backend).To(Equal(&nftablesBackend)) + Expect(c.ConditionsV4).To(Equal(&[]string{"aaa", "bbbb"})) + Expect(c.ConditionsV6).To(Equal(&[]string{"ccc"})) + Expect(c.Name).To(Equal("test")) + }) + + It(fmt.Sprintf("[%s] rejects nftables options with 'backend: iptables'", ver), func() { + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s", + "backend": "iptables", + "conditionsV4": ["aaa", "bbbb"], + "conditionsV6": ["ccc"] + }`, ver)) + _, _, err := parseConfig(configBytes, "container") + Expect(err).To(MatchError("iptables backend was requested but configuration contains nftables-specific options [conditionsV4 conditionsV6]")) + }) + + It(fmt.Sprintf("[%s] rejects iptables options with 'backend: nftables'", ver), func() { + configBytes := []byte(fmt.Sprintf(`{ + "name": "test", + "type": "portmap", + "cniVersion": "%s", + "backend": "nftables", + "externalSetMarkChain": "KUBE-MARK-MASQ", + "conditionsV4": ["-a", "b"], + "conditionsV6": ["-c", "d"] + }`, ver)) + _, _, err := parseConfig(configBytes, "container") + Expect(err).To(MatchError("nftables backend was requested but configuration contains iptables-specific options [externalSetMarkChain conditionsV4 conditionsV6]")) + }) + It(fmt.Sprintf("[%s] does not fail on missing prevResult interface index", ver), func() { configBytes := []byte(fmt.Sprintf(`{ "name": "test", @@ -136,7 +215,7 @@ var _ = Describe("portmapping configuration", func() { { "hostPort": 8080, "containerPort": 80, "protocol": "tcp"} ] }, - "conditionsV4": ["a", "b"], + "conditionsV4": ["-a", "b"], "prevResult": { "interfaces": [ {"name": "host"}