Merge pull request #639 from EdDev/bridge-macspoofchk

bridge: Add macspoofchk support
This commit is contained in:
Matt Dupre
2021-10-06 08:39:10 -07:00
committed by GitHub
24 changed files with 2132 additions and 14 deletions

View File

@ -0,0 +1,27 @@
// 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 (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestIp(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "pkg/link")
}

245
pkg/link/spoofcheck.go Normal file
View File

@ -0,0 +1,245 @@
// 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
import (
"fmt"
"os"
"github.com/networkplumbing/go-nft/nft"
"github.com/networkplumbing/go-nft/nft/schema"
)
const (
natTableName = "nat"
preRoutingBaseChainName = "PREROUTING"
)
type NftConfigurer interface {
Apply(*nft.Config) error
Read() (*nft.Config, error)
}
type SpoofChecker struct {
iface string
macAddress string
refID string
configurer NftConfigurer
}
type defaultNftConfigurer struct{}
func (_ defaultNftConfigurer) Apply(cfg *nft.Config) error {
return nft.ApplyConfig(cfg)
}
func (_ defaultNftConfigurer) Read() (*nft.Config, error) {
return nft.ReadConfig()
}
func NewSpoofChecker(iface, macAddress, refID string) *SpoofChecker {
return NewSpoofCheckerWithConfigurer(iface, macAddress, refID, defaultNftConfigurer{})
}
func NewSpoofCheckerWithConfigurer(iface, macAddress, refID string, configurer NftConfigurer) *SpoofChecker {
return &SpoofChecker{iface, macAddress, refID, configurer}
}
// Setup applies nftables configuration to restrict traffic
// from the provided interface. Only traffic with the mentioned mac address
// is allowed to pass, all others are blocked.
// The configuration follows the format libvirt and ebtables implemented, allowing
// extensions to the rules in the future.
// refID is used to label the rules with a unique comment, identifying the rule-set.
//
// In order to take advantage of the nftables configuration change atomicity, the
// following steps are taken to apply the configuration:
// - Declare the table and chains (they will be created in case not present).
// - Apply the rules, while first flushing the iface/mac specific regular chain rules.
// Two transactions are used because the flush succeeds only if the table/chain it targets
// exists. This avoids the need to query the existing state and acting upon it (a raceful pattern).
// Although two transactions are taken place, only the 2nd one where the rules
// are added has a real impact on the system.
func (sc *SpoofChecker) Setup() error {
baseConfig := nft.NewConfig()
baseConfig.AddTable(&schema.Table{Family: schema.FamilyBridge, Name: natTableName})
baseConfig.AddChain(sc.baseChain())
ifaceChain := sc.ifaceChain()
baseConfig.AddChain(ifaceChain)
macChain := sc.macChain(ifaceChain.Name)
baseConfig.AddChain(macChain)
if err := sc.configurer.Apply(baseConfig); err != nil {
return fmt.Errorf("failed to setup spoof-check: %v", err)
}
rulesConfig := nft.NewConfig()
rulesConfig.FlushChain(ifaceChain)
rulesConfig.FlushChain(macChain)
rulesConfig.AddRule(sc.matchIfaceJumpToChainRule(preRoutingBaseChainName, ifaceChain.Name))
rulesConfig.AddRule(sc.jumpToChainRule(ifaceChain.Name, macChain.Name))
rulesConfig.AddRule(sc.matchMacRule(macChain.Name))
rulesConfig.AddRule(sc.dropRule(macChain.Name))
if err := sc.configurer.Apply(rulesConfig); err != nil {
return fmt.Errorf("failed to setup spoof-check: %v", err)
}
return nil
}
// Teardown removes the interface and mac-address specific chains and their rules.
// The table and base-chain are expected to survive while the base-chain rule that matches the
// interface is removed.
func (sc *SpoofChecker) Teardown() error {
ifaceChain := sc.ifaceChain()
currentConfig, ifaceMatchRuleErr := sc.configurer.Read()
if ifaceMatchRuleErr == nil {
expectedRuleToFind := sc.matchIfaceJumpToChainRule(preRoutingBaseChainName, ifaceChain.Name)
// It is safer to exclude the statement matching, avoiding cases where a current statement includes
// additional default entries (e.g. counters).
ruleToFindExcludingStatements := *expectedRuleToFind
ruleToFindExcludingStatements.Expr = nil
rules := currentConfig.LookupRule(&ruleToFindExcludingStatements)
if len(rules) > 0 {
c := nft.NewConfig()
for _, rule := range rules {
c.DeleteRule(rule)
}
if err := sc.configurer.Apply(c); err != nil {
ifaceMatchRuleErr = fmt.Errorf("failed to delete iface match rule: %v", err)
}
} else {
fmt.Fprintf(os.Stderr, "spoofcheck/teardown: unable to detect iface match rule for deletion: %+v", expectedRuleToFind)
}
}
regularChainsConfig := nft.NewConfig()
regularChainsConfig.DeleteChain(ifaceChain)
regularChainsConfig.DeleteChain(sc.macChain(ifaceChain.Name))
var regularChainsErr error
if err := sc.configurer.Apply(regularChainsConfig); err != nil {
regularChainsErr = fmt.Errorf("failed to delete regular chains: %v", err)
}
if ifaceMatchRuleErr != nil || regularChainsErr != nil {
return fmt.Errorf("failed to teardown spoof-check: %v, %v", ifaceMatchRuleErr, regularChainsErr)
}
return nil
}
func (sc *SpoofChecker) matchIfaceJumpToChainRule(chain, toChain string) *schema.Rule {
return &schema.Rule{
Family: schema.FamilyBridge,
Table: natTableName,
Chain: chain,
Expr: []schema.Statement{
{Match: &schema.Match{
Op: schema.OperEQ,
Left: schema.Expression{RowData: []byte(`{"meta":{"key":"iifname"}}`)},
Right: schema.Expression{String: &sc.iface},
}},
{Verdict: schema.Verdict{Jump: &schema.ToTarget{Target: toChain}}},
},
Comment: ruleComment(sc.refID),
}
}
func (sc *SpoofChecker) jumpToChainRule(chain, toChain string) *schema.Rule {
return &schema.Rule{
Family: schema.FamilyBridge,
Table: natTableName,
Chain: chain,
Expr: []schema.Statement{
{Verdict: schema.Verdict{Jump: &schema.ToTarget{Target: toChain}}},
},
Comment: ruleComment(sc.refID),
}
}
func (sc *SpoofChecker) matchMacRule(chain string) *schema.Rule {
return &schema.Rule{
Family: schema.FamilyBridge,
Table: natTableName,
Chain: chain,
Expr: []schema.Statement{
{Match: &schema.Match{
Op: schema.OperEQ,
Left: schema.Expression{Payload: &schema.Payload{
Protocol: schema.PayloadProtocolEther,
Field: schema.PayloadFieldEtherSAddr,
}},
Right: schema.Expression{String: &sc.macAddress},
}},
{Verdict: schema.Verdict{SimpleVerdict: schema.SimpleVerdict{Return: true}}},
},
Comment: ruleComment(sc.refID),
}
}
func (sc *SpoofChecker) dropRule(chain string) *schema.Rule {
macRulesIndex := nft.NewRuleIndex()
return &schema.Rule{
Family: schema.FamilyBridge,
Table: natTableName,
Chain: chain,
Index: macRulesIndex.Next(),
Expr: []schema.Statement{
{Verdict: schema.Verdict{SimpleVerdict: schema.SimpleVerdict{Drop: true}}},
},
Comment: ruleComment(sc.refID),
}
}
func (_ *SpoofChecker) baseChain() *schema.Chain {
chainPriority := -300
return &schema.Chain{
Family: schema.FamilyBridge,
Table: natTableName,
Name: preRoutingBaseChainName,
Type: schema.TypeFilter,
Hook: schema.HookPreRouting,
Prio: &chainPriority,
Policy: schema.PolicyAccept,
}
}
func (sc *SpoofChecker) ifaceChain() *schema.Chain {
ifaceChainName := "cni-br-iface-" + sc.refID
return &schema.Chain{
Family: schema.FamilyBridge,
Table: natTableName,
Name: ifaceChainName,
}
}
func (_ *SpoofChecker) macChain(ifaceChainName string) *schema.Chain {
macChainName := ifaceChainName + "-mac"
return &schema.Chain{
Family: schema.FamilyBridge,
Table: natTableName,
Name: macChainName,
}
}
func ruleComment(id string) string {
const refIDPrefix = "macspoofchk-"
return refIDPrefix + id
}

297
pkg/link/spoofcheck_test.go Normal file
View File

@ -0,0 +1,297 @@
// 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"
. "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, <nil>", 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, <nil>", 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: <nil>, 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
}