Akihiro Suda 22dd6c553d
firewall: support ingressPolicy=(open|same-bridge) for isolating bridges as in Docker
This commit adds a new parameter `ingressPolicy` (`string`) to the `firewall` plugin.
The supported values are `open` and `same-bridge`.

- `open` is the default and does NOP.

- `same-bridge` creates "CNI-ISOLATION-STAGE-1" and "CNI-ISOLATION-STAGE-2"
that are similar to Docker libnetwork's "DOCKER-ISOLATION-STAGE-1" and
"DOCKER-ISOLATION-STAGE-2" rules.

e.g., when `ns1` and `ns2` are connected to bridge `cni1`, and `ns3` is
connected to bridge `cni2`, the `same-bridge` ingress policy disallows
communications between `ns1` and `ns3`, while allowing communications
between `ns1` and `ns2`.

Please refer to the comment lines in `ingresspolicy.go` for the actual iptables rules.

The `same-bridge` ingress policy is expected to be used in conjunction
with `bridge` plugin. May not work as expected with other "main" plugins.

It should be also noted that the `same-bridge` ingress policy executes
raw `iptables` commands directly, even when the `backend` is set to `firewalld`.
We could potentially use the "direct" API of firewalld [1] to execute
iptables via firewalld, but it doesn't seem to have a clear benefit over just directly
executing raw iptables commands.
(Anyway, we have been already executing raw iptables commands in the `portmap` plugin)

[1] https://firewalld.org/documentation/direct/options.html

This commit replaces the `isolation` plugin proposal (issue 573, PR 574).
The design of `ingressPolicy` was discussed in the comments of the withdrawn PR 574 ,
but `same-network` was renamed to `same-bridge` then.

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
2022-02-03 15:49:43 +09:00

176 lines
5.8 KiB
Go

// 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"
types100 "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/plugins/pkg/utils"
"github.com/coreos/go-iptables/iptables"
)
func setupIngressPolicy(conf *FirewallNetConf, prevResult *types100.Result) error {
switch conf.IngressPolicy {
case "", IngressPolicyOpen:
// NOP
return nil
case IngressPolicySameBridge:
return setupIngressPolicySameBridge(conf, prevResult)
default:
return fmt.Errorf("unknown ingress policy: %q", conf.IngressPolicy)
}
}
func setupIngressPolicySameBridge(conf *FirewallNetConf, prevResult *types100.Result) 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); err != nil {
return err
}
}
return nil
}
func teardownIngressPolicy(conf *FirewallNetConf, prevResult *types100.Result) error {
switch conf.IngressPolicy {
case "", IngressPolicyOpen:
// NOP
return nil
case IngressPolicySameBridge:
// 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
// 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) error {
const (
// Future version may support custom chain names
stage1Chain = "CNI-ISOLATION-STAGE-1"
stage2Chain = "CNI-ISOLATION-STAGE-2"
)
// 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
// iptables -A CNI-ISOLATION-STAGE-1 -j RETURN
// ```
stage1Bridge := withDefaultComment(isolationStage1BridgeRule(bridgeName, stage2Chain))
// prepend = true because this needs to be before "-j RETURN"
const stage1BridgePrepend = true
if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, stage1BridgePrepend, stage1Bridge); 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"})
if err := utils.InsertUnique(ipt, filterTableName, stage2Chain, false, stage2Return); err != nil {
return err
}
return nil
}
func isolationStage1BridgeRule(bridgeName, stage2Chain string) []string {
return []string{"-i", bridgeName, "!", "-o", bridgeName, "-j", stage2Chain}
}
func isolationStage2BridgeRule(bridgeName string) []string {
return []string{"-o", bridgeName, "-j", "DROP"}
}
func withDefaultComment(rule []string) []string {
defaultComment := fmt.Sprintf("CNI firewall plugin rules (ingressPolicy: same-bridge)")
return withComment(rule, defaultComment)
}
func withComment(rule []string, comment string) []string {
return append(rule, []string{"-m", "comment", "--comment", comment}...)
}