Add nftables backend to portmap

Signed-off-by: Dan Winship <danwinship@redhat.com>
This commit is contained in:
Dan Winship 2023-07-22 16:54:40 -04:00 committed by Casey Callendrello
parent 3d1968c152
commit 01a94e17c7
4 changed files with 632 additions and 0 deletions

View File

@ -37,6 +37,7 @@ import (
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100" current "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion" bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
) )
@ -46,6 +47,12 @@ type PortMapper interface {
unforwardPorts(config *PortMapConf) error 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, // PortMapEntry corresponds to a single entry in the port_mappings argument,
// see CONVENTIONS.md // see CONVENTIONS.md
type PortMapEntry struct { type PortMapEntry struct {
@ -61,6 +68,7 @@ type PortMapConf struct {
mapper PortMapper mapper PortMapper
// Generic config // Generic config
Backend *string `json:"backend,omitempty"`
SNAT *bool `json:"snat,omitempty"` SNAT *bool `json:"snat,omitempty"`
ConditionsV4 *[]string `json:"conditionsV4"` ConditionsV4 *[]string `json:"conditionsV4"`
ConditionsV6 *[]string `json:"conditionsV6"` ConditionsV6 *[]string `json:"conditionsV6"`
@ -240,6 +248,21 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31")
} }
err := ensureBackend(&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 // Reject invalid port numbers
for _, pm := range conf.RuntimeConfig.PortMaps { for _, pm := range conf.RuntimeConfig.PortMaps {
if pm.ContainerPort <= 0 { if pm.ContainerPort <= 0 {
@ -279,3 +302,58 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
return &conf, result, nil return &conf, result, nil
} }
// ensureBackend validates and/or sets conf.Backend
func ensureBackend(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
}

View File

@ -0,0 +1,341 @@
// 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: "input",
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.InputHook),
Priority: knftables.PtrTo(knftables.DNATPriority),
})
tx.Flush(&knftables.Chain{
Name: "input",
})
tx.Add(&knftables.Rule{
Chain: "input",
Rule: knftables.Concat(
conditions,
"jump", hostIPHostPortsChain,
),
})
tx.Add(&knftables.Rule{
Chain: "input",
Rule: knftables.Concat(
conditions,
"jump", hostPortsChain,
),
})
tx.Add(&knftables.Chain{
Name: "output",
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.OutputHook),
Priority: knftables.PtrTo(knftables.DNATPriority),
})
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
}

View File

@ -0,0 +1,134 @@
// 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 input { type nat hook input priority -100 ; }
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 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 input a b jump hostip_hostports
add rule ip cni_hostport input a b jump hostports
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
`)
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 input { type nat hook input priority -100 ; }
add chain ip6 cni_hostport output { type nat hook output 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 input c d jump hostip_hostports
add rule ip6 cni_hostport input c d jump hostports
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
`)
actualRules = strings.TrimSpace(ipv6Fake.Dump())
Expect(actualRules).To(Equal(expectedRules))
})
})
}
})

View File

@ -35,6 +35,7 @@ var _ = Describe("portmapping configuration", func() {
"name": "test", "name": "test",
"type": "portmap", "type": "portmap",
"cniVersion": "%s", "cniVersion": "%s",
"backend": "iptables",
"runtimeConfig": { "runtimeConfig": {
"portMappings": [ "portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}, { "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
@ -94,6 +95,7 @@ var _ = Describe("portmapping configuration", func() {
"name": "test", "name": "test",
"type": "portmap", "type": "portmap",
"cniVersion": "%s", "cniVersion": "%s",
"backend": "iptables",
"snat": false, "snat": false,
"conditionsV4": ["-s", "1.2.3.4"], "conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"] "conditionsV6": ["-s", "12::34"]
@ -113,6 +115,7 @@ var _ = Describe("portmapping configuration", func() {
"name": "test", "name": "test",
"type": "portmap", "type": "portmap",
"cniVersion": "%s", "cniVersion": "%s",
"backend": "iptables",
"snat": false, "snat": false,
"conditionsV4": ["-s", "1.2.3.4"], "conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"], "conditionsV6": ["-s", "12::34"],
@ -126,6 +129,82 @@ var _ = Describe("portmapping configuration", func() {
Expect(err).To(MatchError("Invalid host port number: 0")) 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": ["ip", "saddr", "1.2.3.4"],
"conditionsV6": ["ip6", "saddr", "12::34"]
}`, 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{"ip", "saddr", "1.2.3.4"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"ip6", "saddr", "12::34"}))
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": ["ip", "saddr", "1.2.3.4"],
"conditionsV6": ["ip6", "saddr", "12::34"]
}`, 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": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"]
}`, 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() { It(fmt.Sprintf("[%s] does not fail on missing prevResult interface index", ver), func() {
configBytes := []byte(fmt.Sprintf(`{ configBytes := []byte(fmt.Sprintf(`{
"name": "test", "name": "test",