Add nftables backend to portmap
Signed-off-by: Dan Winship <danwinship@redhat.com>
This commit is contained in:
parent
3d1968c152
commit
01a94e17c7
@ -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
|
||||||
|
}
|
||||||
|
341
plugins/meta/portmap/portmap_nftables.go
Normal file
341
plugins/meta/portmap/portmap_nftables.go
Normal 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
|
||||||
|
}
|
134
plugins/meta/portmap/portmap_nftables_test.go
Normal file
134
plugins/meta/portmap/portmap_nftables_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user