Add nftables implementation of ipmasq

Signed-off-by: Dan Winship <danwinship@redhat.com>
This commit is contained in:
Dan Winship
2023-04-04 08:49:14 -04:00
committed by Casey Callendrello
parent 729dd23c40
commit 61d078645a
8 changed files with 780 additions and 133 deletions

View File

@ -35,7 +35,6 @@ import (
"github.com/containernetworking/plugins/pkg/ipam"
"github.com/containernetworking/plugins/pkg/link"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
"github.com/containernetworking/plugins/pkg/utils/sysctl"
)
@ -52,6 +51,7 @@ type NetConf struct {
IsDefaultGW bool `json:"isDefaultGateway"`
ForceAddress bool `json:"forceAddress"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
@ -673,10 +673,8 @@ func cmdAdd(args *skel.CmdArgs) error {
}
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
if err = ip.SetupIPMasqForNetwork(n.IPMasqBackend, &ipc.Address, n.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
@ -814,10 +812,8 @@ func cmdDel(args *skel.CmdArgs) error {
}
if isLayer3 && n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipn := range ipnets {
if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil {
if err := ip.TeardownIPMasqForNetwork(ipn, n.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}

View File

@ -15,6 +15,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
@ -27,6 +28,7 @@ import (
. "github.com/onsi/gomega"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"sigs.k8s.io/knftables"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
@ -77,6 +79,7 @@ type testCase struct {
vlanTrunk []*VlanTrunk
removeDefaultVlan bool
ipMasq bool
ipMasqBackend string
macspoofchk bool
disableContIface bool
@ -172,6 +175,9 @@ const (
ipMasqConfStr = `,
"ipMasq": %t`
ipMasqBackendConfStr = `,
"ipMasqBackend": "%s"`
// Single subnet configuration (legacy)
subnetConfStr = `,
"subnet": "%s"`
@ -243,6 +249,9 @@ func (tc testCase) netConfJSON(dataDir string) string {
if tc.ipMasq {
conf += tc.ipMasqConfig()
}
if tc.ipMasqBackend != "" {
conf += tc.ipMasqBackendConfig()
}
if tc.args.cni.mac != "" {
conf += fmt.Sprintf(argsFormat, tc.args.cni.mac)
}
@ -295,6 +304,11 @@ func (tc testCase) ipMasqConfig() string {
return conf
}
func (tc testCase) ipMasqBackendConfig() string {
conf := fmt.Sprintf(ipMasqBackendConfStr, tc.ipMasqBackend)
return conf
}
func (tc testCase) rangesConfig() string {
conf := rangesStartStr
for i, tcRange := range tc.ranges {
@ -2390,41 +2404,82 @@ var _ = Describe("bridge Operations", func() {
})
if testutils.SpecVersionHasChaining(ver) {
It(fmt.Sprintf("[%s] configures a bridge and ipMasq rules", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
tc := testCase{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
cniVersion: ver,
}
for _, tc := range []testCase{
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
cniVersion: ver,
},
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
ipMasqBackend: "iptables",
cniVersion: ver,
},
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
ipMasqBackend: "nftables",
cniVersion: ver,
},
} {
tc := tc
It(fmt.Sprintf("[%s] configures a bridge and ipMasq rules with ipMasqBackend %q", ver, tc.ipMasqBackend), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
args := tc.createCmdArgs(originalNS, dataDir)
r, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
args := tc.createCmdArgs(originalNS, dataDir)
r, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := types100.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).Should(HaveLen(1))
ip := result.IPs[0].Address.IP.String()
// Update this if the default ipmasq backend changes
switch tc.ipMasqBackend {
case "iptables", "":
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
Expect(err).NotTo(HaveOccurred())
rules, err := ipt.List("nat", "POSTROUTING")
Expect(err).NotTo(HaveOccurred())
Expect(rules).Should(ContainElement(ContainSubstring(ip)))
case "nftables":
nft, err := knftables.New(knftables.InetFamily, "cni_plugins_masquerade")
Expect(err).NotTo(HaveOccurred())
rules, err := nft.ListRules(context.TODO(), "masq_checks")
Expect(err).NotTo(HaveOccurred())
// FIXME: ListRules() doesn't return the actual rule strings,
// and we can't easily compute the ipmasq plugin's comment.
comments := 0
for _, r := range rules {
if r.Comment != nil {
comments++
break
}
}
Expect(comments).To(Equal(1), "expected to find exactly one Rule with a comment")
}
err = testutils.CmdDelWithArgs(args, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
result, err := types100.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).Should(HaveLen(1))
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
Expect(err).NotTo(HaveOccurred())
rules, err := ipt.List("nat", "POSTROUTING")
Expect(err).NotTo(HaveOccurred())
Expect(rules).Should(ContainElement(ContainSubstring(result.IPs[0].Address.IP.String())))
err = testutils.CmdDelWithArgs(args, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
})
}
for i, tc := range []testCase{
{

View File

@ -31,7 +31,6 @@ import (
"github.com/containernetworking/plugins/pkg/ip"
"github.com/containernetworking/plugins/pkg/ipam"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
@ -44,8 +43,9 @@ func init() {
type NetConf struct {
types.NetConf
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
}
func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
@ -229,10 +229,8 @@ func cmdAdd(args *skel.CmdArgs) error {
}
if conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
if err = ip.SetupIPMasqForNetwork(conf.IPMasqBackend, &ipc.Address, conf.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
@ -293,10 +291,8 @@ func cmdDel(args *skel.CmdArgs) error {
}
if len(ipnets) != 0 && conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
for _, ipn := range ipnets {
err = ip.TeardownIPMasq(ipn, chain, comment)
err = ip.TeardownIPMasqForNetwork(ipn, conf.Name, args.IfName, args.ContainerID)
}
}

View File

@ -39,6 +39,7 @@ type Net struct {
CNIVersion string `json:"cniVersion"`
Type string `json:"type,omitempty"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
IPAM *allocator.IPAMConfig `json:"ipam"`
DNS types.DNS `json:"dns"`
@ -368,6 +369,62 @@ var _ = Describe("ptp Operations", func() {
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a ptp link when specifying ipMasqBackend: iptables", ver), func() {
dnsConf := types.DNS{
Nameservers: []string{"10.1.2.123"},
Domain: "some.domain.test",
Search: []string{"search.test"},
Options: []string{"option1:foo"},
}
dnsConfBytes, err := json.Marshal(dnsConf)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "mynet",
"type": "ptp",
"ipMasq": true,
"ipMasqBackend": "iptables",
"mtu": 5000,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
},
"dns": %s
}`, ver, dataDir, string(dnsConfBytes))
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a ptp link when specifying ipMasqBackend: nftables", ver), func() {
dnsConf := types.DNS{
Nameservers: []string{"10.1.2.123"},
Domain: "some.domain.test",
Search: []string{"search.test"},
Options: []string{"option1:foo"},
}
dnsConfBytes, err := json.Marshal(dnsConf)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "mynet",
"type": "ptp",
"ipMasq": true,
"ipMasqBackend": "nftables",
"mtu": 5000,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
},
"dns": %s
}`, ver, dataDir, string(dnsConfBytes))
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a dual-stack ptp link + routes with ADD/DEL", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",