| // Copyright 2022 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. |
| |
| // This is a sample chained plugin that supports multiple CNI versions. It |
| // parses prevResult according to the cniVersion |
| package main |
| |
| import ( |
| "fmt" |
| |
| "github.com/coreos/go-iptables/iptables" |
| |
| types100 "github.com/containernetworking/cni/pkg/types/100" |
| "github.com/containernetworking/plugins/pkg/utils" |
| ) |
| |
| func setupIngressPolicy(conf *FirewallNetConf, prevResult *types100.Result) error { |
| switch conf.IngressPolicy { |
| case "", IngressPolicyOpen: |
| // NOP |
| return nil |
| case IngressPolicySameBridge: |
| return setupIngressPolicyBridgeIsolation(conf, prevResult, false) |
| case IngressPolicyIsolated: |
| return setupIngressPolicyBridgeIsolation(conf, prevResult, true) |
| default: |
| return fmt.Errorf("unknown ingress policy: %q", conf.IngressPolicy) |
| } |
| } |
| |
| func setupIngressPolicyBridgeIsolation(conf *FirewallNetConf, prevResult *types100.Result, isolated bool) error { |
| if len(prevResult.Interfaces) == 0 { |
| return fmt.Errorf("interface needs to be set for ingress policy %q, make sure to chain \"firewall\" plugin with \"bridge\"", |
| conf.IngressPolicy) |
| } |
| intf := prevResult.Interfaces[0] |
| if intf == nil { |
| return fmt.Errorf("got nil interface") |
| } |
| bridgeName := intf.Name |
| if bridgeName == "" { |
| return fmt.Errorf("got empty bridge name") |
| } |
| for _, iptProto := range findProtos(conf) { |
| ipt, err := iptables.NewWithProtocol(iptProto) |
| if err != nil { |
| return err |
| } |
| if err := setupIsolationChains(ipt, bridgeName, isolated); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func teardownIngressPolicy(conf *FirewallNetConf) error { |
| switch conf.IngressPolicy { |
| case "", IngressPolicyOpen: |
| // NOP |
| return nil |
| case IngressPolicySameBridge, IngressPolicyIsolated: |
| // NOP |
| // |
| // We can't be sure whether conf.bridgeName is still in use by other containers. |
| // So we do not remove the iptable rules that are created per bridge. |
| return nil |
| default: |
| return fmt.Errorf("unknown ingress policy: %q", conf.IngressPolicy) |
| } |
| } |
| |
| const ( |
| filterTableName = "filter" // built-in |
| forwardChainName = "FORWARD" // built-in |
| ) |
| |
| // setupIsolationChains executes the following iptables commands for isolating networks: |
| // ``` |
| // iptables -N CNI-ISOLATION-STAGE-1 |
| // iptables -N CNI-ISOLATION-STAGE-2 |
| // # NOTE: "-j CNI-ISOLATION-STAGE-1" needs to be before "CNI-FORWARD" chain. So we use -I here. |
| // iptables -I FORWARD -j CNI-ISOLATION-STAGE-1 |
| // iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} ! -o ${bridgeName} -j CNI-ISOLATION-STAGE-2 |
| // [isolated=true] iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} -o ${bridgeName} -j DROP |
| // iptables -A CNI-ISOLATION-STAGE-1 -j RETURN |
| // iptables -A CNI-ISOLATION-STAGE-2 -o ${bridgeName} -j DROP |
| // iptables -A CNI-ISOLATION-STAGE-2 -j RETURN |
| // ``` |
| func setupIsolationChains(ipt *iptables.IPTables, bridgeName string, isolated bool) error { |
| const ( |
| // Future version may support custom chain names |
| stage1Chain = "CNI-ISOLATION-STAGE-1" |
| stage2Chain = "CNI-ISOLATION-STAGE-2" |
| ) |
| |
| ingressPolicyName := "same-bridge" |
| if isolated { |
| ingressPolicyName = "isolated" |
| } |
| |
| withPolicyComment := withPolicyComment(ingressPolicyName) |
| // Commands: |
| // ``` |
| // iptables -N CNI-ISOLATION-STAGE-1 |
| // iptables -N CNI-ISOLATION-STAGE-2 |
| // ``` |
| for _, chain := range []string{stage1Chain, stage2Chain} { |
| if err := utils.EnsureChain(ipt, filterTableName, chain); err != nil { |
| return err |
| } |
| } |
| |
| // Commands: |
| // ``` |
| // iptables -I FORWARD -j CNI-ISOLATION-STAGE-1 |
| // ``` |
| jumpToStage1 := withDefaultComment([]string{"-j", stage1Chain}) |
| // NOTE: "-j CNI-ISOLATION-STAGE-1" needs to be before "CNI-FORWARD" created by CNI firewall plugin. |
| // So we specify prepend = true . |
| const jumpToStage1Prepend = true |
| if err := utils.InsertUnique(ipt, filterTableName, forwardChainName, jumpToStage1Prepend, jumpToStage1); err != nil { |
| return err |
| } |
| |
| // Commands: |
| // ``` |
| // iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} ! -o ${bridgeName} -j CNI-ISOLATION-STAGE-2 |
| // [isolate=true] iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} -o ${bridgeName} -j DROP |
| // iptables -A CNI-ISOLATION-STAGE-1 -j RETURN |
| // ``` |
| stage1BridgeRule := withPolicyComment(isolationStage1BridgeRule(bridgeName, stage2Chain)) |
| stage1BridgeDropRule := withPolicyComment(isolationStage1BridgeDropRule(bridgeName)) |
| // prepend = true because this needs to be before "-j RETURN" |
| const stage1BridgePrepend = true |
| if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, stage1BridgePrepend, stage1BridgeRule); err != nil { |
| return err |
| } |
| if isolated { |
| if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, stage1BridgePrepend, stage1BridgeDropRule); err != nil { |
| return err |
| } |
| } |
| |
| stage1Return := withDefaultComment([]string{"-j", "RETURN"}) |
| if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, false, stage1Return); err != nil { |
| return err |
| } |
| |
| // Commands: |
| // ``` |
| // iptables -A CNI-ISOLATION-STAGE-2 -o ${bridgeName} -j DROP |
| // iptables -A CNI-ISOLATION-STAGE-2 -j RETURN |
| // ``` |
| stage2Bridge := withDefaultComment(isolationStage2BridgeRule(bridgeName)) |
| // prepend = true because this needs to be before "-j RETURN" |
| const stage2BridgePrepend = true |
| if err := utils.InsertUnique(ipt, filterTableName, stage2Chain, stage2BridgePrepend, stage2Bridge); err != nil { |
| return err |
| } |
| stage2Return := withDefaultComment([]string{"-j", "RETURN"}) |
| return utils.InsertUnique(ipt, filterTableName, stage2Chain, false, stage2Return) |
| } |
| |
| func isolationStage1BridgeRule(bridgeName, stage2Chain string) []string { |
| return []string{"-i", bridgeName, "!", "-o", bridgeName, "-j", stage2Chain} |
| } |
| |
| func isolationStage1BridgeDropRule(bridgeName string) []string { |
| return []string{"-i", bridgeName, "-o", bridgeName, "-j", "DROP"} |
| } |
| |
| func isolationStage2BridgeRule(bridgeName string) []string { |
| return []string{"-o", bridgeName, "-j", "DROP"} |
| } |
| |
| func withPolicyComment(policy string) func([]string) []string { |
| return func(rule []string) []string { |
| comment := fmt.Sprintf("CNI firewall plugin rules (ingressPolicy: %s)", policy) |
| |
| return withComment(rule, comment) |
| } |
| } |
| |
| func withDefaultComment(rule []string) []string { |
| defaultComment := "CNI firewall plugin rules (ingressPolicy: same-bridge|isolated)" |
| return withComment(rule, defaultComment) |
| } |
| |
| func withComment(rule []string, comment string) []string { |
| return append(rule, []string{"-m", "comment", "--comment", comment}...) |
| } |