// Copyright 2021 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 link_test import ( "fmt" "github.com/networkplumbing/go-nft/nft" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/containernetworking/plugins/pkg/link" ) var _ = Describe("spoofcheck", func() { iface := "net0" mac := "02:00:00:00:12:34" id := "container99-net1" Context("setup", func() { It("succeeds", func() { c := configurerStub{} sc := link.NewSpoofCheckerWithConfigurer(iface, mac, id, &c) Expect(sc.Setup()).To(Succeed()) assertExpectedTableAndChainsInSetupConfig(c) assertExpectedRulesInSetupConfig(c) }) It("fails to setup config when 1st apply is unsuccessful (declare table and chains)", func() { c := &configurerStub{failFirstApplyConfig: true} sc := link.NewSpoofCheckerWithConfigurer(iface, mac, id, c) Expect(sc.Setup()).To(MatchError("failed to setup spoof-check: " + errorFirstApplyText)) }) It("fails to setup config when 2nd apply is unsuccessful (flush and add the rules)", func() { c := &configurerStub{failSecondApplyConfig: true} sc := link.NewSpoofCheckerWithConfigurer(iface, mac, id, c) Expect(sc.Setup()).To(MatchError("failed to setup spoof-check: " + errorSecondApplyText)) }) }) Context("teardown", func() { It("succeeds", func() { existingConfig := nft.NewConfig() existingConfig.FromJSON([]byte(rowConfigWithRulesOnly())) c := configurerStub{readConfig: existingConfig} sc := link.NewSpoofCheckerWithConfigurer("", "", id, &c) Expect(sc.Teardown()).To(Succeed()) assertExpectedBaseChainRuleDeletionInTeardownConfig(c) assertExpectedRegularChainsDeletionInTeardownConfig(c) }) It("fails, 1st apply is unsuccessful (delete iface match rule)", func() { config := nft.NewConfig() config.FromJSON([]byte(rowConfigWithRulesOnly())) c := &configurerStub{applyConfig: []*nft.Config{config}, readConfig: config, failFirstApplyConfig: true} sc := link.NewSpoofCheckerWithConfigurer("", "", id, c) Expect(sc.Teardown()).To(MatchError(fmt.Sprintf( "failed to teardown spoof-check: failed to delete iface match rule: %s, ", errorFirstApplyText, ))) }) It("fails, read current config is unsuccessful", func() { config := nft.NewConfig() config.FromJSON([]byte(rowConfigWithRulesOnly())) c := &configurerStub{applyConfig: []*nft.Config{config}, readConfig: config, failReadConfig: true} sc := link.NewSpoofCheckerWithConfigurer("", "", id, c) Expect(sc.Teardown()).To(MatchError(fmt.Sprintf( "failed to teardown spoof-check: %s, ", errorReadText, ))) }) It("fails, 2nd apply is unsuccessful (delete the regular chains)", func() { config := nft.NewConfig() config.FromJSON([]byte(rowConfigWithRulesOnly())) c := &configurerStub{applyConfig: []*nft.Config{config}, readConfig: config, failSecondApplyConfig: true} sc := link.NewSpoofCheckerWithConfigurer("", "", id, c) Expect(sc.Teardown()).To(MatchError(fmt.Sprintf( "failed to teardown spoof-check: , failed to delete regular chains: %s", errorSecondApplyText, ))) }) It("fails, both applies are unsuccessful", func() { config := nft.NewConfig() config.FromJSON([]byte(rowConfigWithRulesOnly())) c := &configurerStub{ applyConfig: []*nft.Config{config}, readConfig: config, failFirstApplyConfig: true, failSecondApplyConfig: true, } sc := link.NewSpoofCheckerWithConfigurer("", "", id, c) Expect(sc.Teardown()).To(MatchError(fmt.Sprintf( "failed to teardown spoof-check: "+ "failed to delete iface match rule: %s, "+ "failed to delete regular chains: %s", errorFirstApplyText, errorSecondApplyText, ))) }) }) }) func assertExpectedRegularChainsDeletionInTeardownConfig(action configurerStub) { deleteRegularChainRulesJSONConfig, err := action.applyConfig[1].ToJSON() ExpectWithOffset(1, err).NotTo(HaveOccurred()) expectedDeleteRegularChainRulesJSONConfig := ` {"nftables": [ {"delete": {"chain": { "family": "bridge", "table": "nat", "name": "cni-br-iface-container99-net1" }}}, {"delete": {"chain": { "family": "bridge", "table": "nat", "name": "cni-br-iface-container99-net1-mac" }}} ]}` ExpectWithOffset(1, string(deleteRegularChainRulesJSONConfig)).To(MatchJSON(expectedDeleteRegularChainRulesJSONConfig)) } func assertExpectedBaseChainRuleDeletionInTeardownConfig(action configurerStub) { deleteBaseChainRuleJSONConfig, err := action.applyConfig[0].ToJSON() Expect(err).NotTo(HaveOccurred()) expectedDeleteIfaceMatchRuleJSONConfig := ` {"nftables": [ {"delete": {"rule": { "family": "bridge", "table": "nat", "chain": "PREROUTING", "expr": [ {"match": { "op": "==", "left": {"meta": {"key": "iifname"}}, "right": "net0" }}, {"jump": {"target": "cni-br-iface-container99-net1"}} ], "comment": "macspoofchk-container99-net1" }}} ]}` Expect(string(deleteBaseChainRuleJSONConfig)).To(MatchJSON(expectedDeleteIfaceMatchRuleJSONConfig)) } func rowConfigWithRulesOnly() string { return ` {"nftables":[ {"rule":{"family":"bridge","table":"nat","chain":"PREROUTING", "expr":[ {"match":{"op":"==","left":{"meta":{"key":"iifname"}},"right":"net0"}}, {"jump":{"target":"cni-br-iface-container99-net1"}} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1", "expr":[ {"jump":{"target":"cni-br-iface-container99-net1-mac"}} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1-mac", "expr":[ {"match":{ "op":"==", "left":{"payload":{"protocol":"ether","field":"saddr"}}, "right":"02:00:00:00:12:34" }}, {"return":null} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1-mac", "expr":[{"drop":null}], "index":0, "comment":"macspoofchk-container99-net1"}} ]}` } func assertExpectedTableAndChainsInSetupConfig(c configurerStub) { config := c.applyConfig[0] jsonConfig, err := config.ToJSON() ExpectWithOffset(1, err).NotTo(HaveOccurred()) expectedConfig := ` {"nftables": [ {"table": {"family": "bridge", "name": "nat"}}, {"chain": { "family": "bridge", "table": "nat", "name": "PREROUTING", "type": "filter", "hook": "prerouting", "prio": -300, "policy": "accept" }}, {"chain": { "family": "bridge", "table": "nat", "name": "cni-br-iface-container99-net1" }}, {"chain": { "family": "bridge", "table": "nat", "name": "cni-br-iface-container99-net1-mac" }} ]}` ExpectWithOffset(1, string(jsonConfig)).To(MatchJSON(expectedConfig)) } func assertExpectedRulesInSetupConfig(c configurerStub) { config := c.applyConfig[1] jsonConfig, err := config.ToJSON() ExpectWithOffset(1, err).NotTo(HaveOccurred()) expectedConfig := ` {"nftables":[ {"flush":{"chain":{"family":"bridge","table":"nat","name":"cni-br-iface-container99-net1"}}}, {"flush":{"chain":{"family":"bridge","table":"nat","name":"cni-br-iface-container99-net1-mac"}}}, {"rule":{"family":"bridge","table":"nat","chain":"PREROUTING", "expr":[ {"match":{"op":"==","left":{"meta":{"key":"iifname"}},"right":"net0"}}, {"jump":{"target":"cni-br-iface-container99-net1"}} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1", "expr":[ {"jump":{"target":"cni-br-iface-container99-net1-mac"}} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1-mac", "expr":[ {"match":{ "op":"==", "left":{"payload":{"protocol":"ether","field":"saddr"}}, "right":"02:00:00:00:12:34" }}, {"return":null} ], "comment":"macspoofchk-container99-net1"}}, {"rule":{"family":"bridge","table":"nat","chain":"cni-br-iface-container99-net1-mac", "expr":[{"drop":null}], "index":0, "comment":"macspoofchk-container99-net1"}} ]}` ExpectWithOffset(1, string(jsonConfig)).To(MatchJSON(expectedConfig)) } const ( errorFirstApplyText = "1st apply failed" errorSecondApplyText = "2nd apply failed" errorReadText = "read failed" ) type configurerStub struct { applyConfig []*nft.Config readConfig *nft.Config applyCounter int failFirstApplyConfig bool failSecondApplyConfig bool failReadConfig bool } func (a *configurerStub) Apply(c *nft.Config) error { a.applyCounter++ if a.failFirstApplyConfig && a.applyCounter == 1 { return fmt.Errorf(errorFirstApplyText) } if a.failSecondApplyConfig && a.applyCounter == 2 { return fmt.Errorf(errorSecondApplyText) } a.applyConfig = append(a.applyConfig, c) return nil } func (a *configurerStub) Read() (*nft.Config, error) { if a.failReadConfig { return nil, fmt.Errorf(errorReadText) } return a.readConfig, nil }