// 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 ( "context" "fmt" "os" "time" "github.com/networkplumbing/go-nft/nft" "github.com/networkplumbing/go-nft/nft/schema" ) const ( natTableName = "nat" preRoutingBaseChainName = "PREROUTING" ) type NftConfigurer interface { Apply(*nft.Config) (*nft.Config, error) Read(filterCommands ...string) (*nft.Config, error) } type SpoofChecker struct { iface string macAddress string refID string configurer NftConfigurer rulestore *nft.Config } type defaultNftConfigurer struct{} func (dnc defaultNftConfigurer) Apply(cfg *nft.Config) (*nft.Config, error) { const timeout = 55 * time.Second ctxWithTimeout, cancelFunc := context.WithTimeout(context.Background(), timeout) defer cancelFunc() return nft.ApplyConfigEcho(ctxWithTimeout, cfg) } func (dnc defaultNftConfigurer) Read(filterCommands ...string) (*nft.Config, error) { const timeout = 55 * time.Second ctxWithTimeout, cancelFunc := context.WithTimeout(context.Background(), timeout) defer cancelFunc() return nft.ReadConfigContext(ctxWithTimeout, filterCommands...) } 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, nil} } // 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)) rulestore, err := sc.configurer.Apply(rulesConfig) if err != nil { return fmt.Errorf("failed to setup spoof-check: %v", err) } sc.rulestore = rulestore return nil } func (sc *SpoofChecker) findPreroutingRule(ruleToFind *schema.Rule) ([]*schema.Rule, error) { ruleset := sc.rulestore if ruleset == nil { chain, err := sc.configurer.Read(listChainBridgeNatPrerouting()...) if err != nil { return nil, err } ruleset = chain } return ruleset.LookupRule(ruleToFind), 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() 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, ifaceMatchRuleErr := sc.findPreroutingRule(&ruleToFindExcludingStatements) if ifaceMatchRuleErr == nil && 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) } // Drop the cache, it should contain deleted rule(s) now sc.rulestore = nil } 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 { return &schema.Rule{ Family: schema.FamilyBridge, Table: natTableName, Chain: chain, Expr: []schema.Statement{ {Verdict: schema.Verdict{SimpleVerdict: schema.SimpleVerdict{Drop: true}}}, }, Comment: ruleComment(sc.refID), } } func (sc *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 (sc *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 } func listChainBridgeNatPrerouting() []string { return []string{"chain", "bridge", natTableName, preRoutingBaseChainName} }