Skip to content

Commit 42268a4

Browse files
authored
Merge pull request #584 from AkihiroSuda/isolation-firewall
firewall: support ingressPolicy=(open|same-bridge) for isolating bridges as in Docker
2 parents f531419 + 22dd6c5 commit 42268a4

File tree

5 files changed

+425
-20
lines changed

5 files changed

+425
-20
lines changed

pkg/utils/iptables.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,21 @@ func ClearChain(ipt *iptables.IPTables, table, chain string) error {
119119
return err
120120
}
121121
}
122+
123+
// InsertUnique will add a rule to a chain if it does not already exist.
124+
// By default the rule is appended, unless prepend is true.
125+
func InsertUnique(ipt *iptables.IPTables, table, chain string, prepend bool, rule []string) error {
126+
exists, err := ipt.Exists(table, chain, rule...)
127+
if err != nil {
128+
return err
129+
}
130+
if exists {
131+
return nil
132+
}
133+
134+
if prepend {
135+
return ipt.Insert(table, chain, 1, rule...)
136+
} else {
137+
return ipt.Append(table, chain, rule...)
138+
}
139+
}

plugins/meta/firewall/firewall.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,27 @@ type FirewallNetConf struct {
4646
// the firewalld backend is used but the zone is not given, it defaults
4747
// to 'trusted'
4848
FirewalldZone string `json:"firewalldZone,omitempty"`
49+
50+
// IngressPolicy is an optional ingress policy.
51+
// Defaults to "open".
52+
IngressPolicy IngressPolicy `json:"ingressPolicy,omitempty"`
4953
}
5054

55+
// IngressPolicy is an ingress policy string.
56+
type IngressPolicy = string
57+
58+
const (
59+
// IngressPolicyOpen ("open"): all inbound connections to the container are accepted.
60+
// IngressPolicyOpen is the default ingress policy.
61+
IngressPolicyOpen IngressPolicy = "open"
62+
63+
// IngressPolicySameBridge ("same-bridge"): connections from the same bridge are accepted, others are blocked.
64+
// This is similar to how Docker libnetwork works.
65+
// IngressPolicySameBridge executes `iptables` regardless to the value of `Backend`.
66+
// IngressPolicySameBridge may not work as expected for non-bridge networks.
67+
IngressPolicySameBridge IngressPolicy = "same-bridge"
68+
)
69+
5170
type FirewallBackend interface {
5271
Add(*FirewallNetConf, *current.Result) error
5372
Del(*FirewallNetConf, *current.Result) error
@@ -129,6 +148,10 @@ func cmdAdd(args *skel.CmdArgs) error {
129148
return err
130149
}
131150

151+
if err := setupIngressPolicy(conf, result); err != nil {
152+
return err
153+
}
154+
132155
if result == nil {
133156
result = &current.Result{
134157
CNIVersion: current.ImplementedSpecVersion,
@@ -153,6 +176,10 @@ func cmdDel(args *skel.CmdArgs) error {
153176
return err
154177
}
155178

179+
if err := teardownIngressPolicy(conf, result); err != nil {
180+
return err
181+
}
182+
156183
return nil
157184
}
158185

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2022 CNI authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
"os/exec"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/containernetworking/cni/libcni"
26+
types100 "github.com/containernetworking/cni/pkg/types/100"
27+
"github.com/containernetworking/plugins/pkg/ns"
28+
"github.com/containernetworking/plugins/pkg/testutils"
29+
. "github.com/onsi/ginkgo"
30+
. "github.com/onsi/gomega"
31+
)
32+
33+
// The integration tests expect the "firewall" binary to be present in $PATH.
34+
// To run test, e.g, : go test -exec "sudo -E PATH=$(pwd):/opt/cni/bin:$PATH" -v -ginkgo.v
35+
var _ = Describe("firewall integration tests (ingressPolicy: same-bridge)", func() {
36+
// ns0: foo (10.88.3.0/24)
37+
// ns1: foo (10.88.3.0/24)
38+
// ns2: bar (10.88.4.0/24)
39+
//
40+
// ns0@foo can talk to ns1@foo, but cannot talk to ns2@bar
41+
const nsCount = 3
42+
var (
43+
configListFoo *libcni.NetworkConfigList // "foo", 10.88.3.0/24
44+
configListBar *libcni.NetworkConfigList // "bar", 10.88.4.0/24
45+
cniConf *libcni.CNIConfig
46+
namespaces [nsCount]ns.NetNS
47+
)
48+
49+
BeforeEach(func() {
50+
var err error
51+
rawConfigFoo := `
52+
{
53+
"cniVersion": "1.0.0",
54+
"name": "foo",
55+
"plugins": [
56+
{
57+
"type": "bridge",
58+
"bridge": "foo",
59+
"isGateway": true,
60+
"ipMasq": true,
61+
"hairpinMode": true,
62+
"ipam": {
63+
"type": "host-local",
64+
"routes": [
65+
{
66+
"dst": "0.0.0.0/0"
67+
}
68+
],
69+
"ranges": [
70+
[
71+
{
72+
"subnet": "10.88.3.0/24",
73+
"gateway": "10.88.3.1"
74+
}
75+
]
76+
]
77+
}
78+
},
79+
{
80+
"type": "firewall",
81+
"backend": "iptables",
82+
"ingressPolicy": "same-bridge"
83+
}
84+
]
85+
}
86+
`
87+
configListFoo, err = libcni.ConfListFromBytes([]byte(rawConfigFoo))
88+
Expect(err).NotTo(HaveOccurred())
89+
90+
rawConfigBar := strings.ReplaceAll(rawConfigFoo, "foo", "bar")
91+
rawConfigBar = strings.ReplaceAll(rawConfigBar, "10.88.3.", "10.88.4.")
92+
93+
configListBar, err = libcni.ConfListFromBytes([]byte(rawConfigBar))
94+
Expect(err).NotTo(HaveOccurred())
95+
96+
// turn PATH in to CNI_PATH.
97+
_, err = exec.LookPath("firewall")
98+
Expect(err).NotTo(HaveOccurred())
99+
dirs := filepath.SplitList(os.Getenv("PATH"))
100+
cniConf = &libcni.CNIConfig{Path: dirs}
101+
102+
for i := 0; i < nsCount; i++ {
103+
targetNS, err := testutils.NewNS()
104+
Expect(err).NotTo(HaveOccurred())
105+
fmt.Fprintf(GinkgoWriter, "namespace %d:%s\n", i, targetNS.Path())
106+
namespaces[i] = targetNS
107+
}
108+
})
109+
110+
AfterEach(func() {
111+
for _, targetNS := range namespaces {
112+
if targetNS != nil {
113+
targetNS.Close()
114+
}
115+
}
116+
})
117+
118+
Describe("Testing with network foo and bar", func() {
119+
It("should isolate foo from bar", func() {
120+
var results [nsCount]*types100.Result
121+
for i := 0; i < nsCount; i++ {
122+
runtimeConfig := libcni.RuntimeConf{
123+
ContainerID: fmt.Sprintf("test-cni-firewall-%d", i),
124+
NetNS: namespaces[i].Path(),
125+
IfName: "eth0",
126+
}
127+
128+
configList := configListFoo
129+
switch i {
130+
case 0, 1:
131+
// leave foo
132+
default:
133+
configList = configListBar
134+
}
135+
136+
// Clean up garbages produced during past failed executions
137+
_ = cniConf.DelNetworkList(context.TODO(), configList, &runtimeConfig)
138+
139+
// Make delete idempotent, so we can clean up on failure
140+
netDeleted := false
141+
deleteNetwork := func() error {
142+
if netDeleted {
143+
return nil
144+
}
145+
netDeleted = true
146+
return cniConf.DelNetworkList(context.TODO(), configList, &runtimeConfig)
147+
}
148+
// Create the network
149+
res, err := cniConf.AddNetworkList(context.TODO(), configList, &runtimeConfig)
150+
Expect(err).NotTo(HaveOccurred())
151+
// nolint: errcheck
152+
defer deleteNetwork()
153+
154+
results[i], err = types100.NewResultFromResult(res)
155+
Expect(err).NotTo(HaveOccurred())
156+
fmt.Fprintf(GinkgoWriter, "results[%d]: %+v\n", i, results[i])
157+
}
158+
ping := func(src, dst int) error {
159+
return namespaces[src].Do(func(ns.NetNS) error {
160+
defer GinkgoRecover()
161+
saddr := results[src].IPs[0].Address.IP.String()
162+
daddr := results[dst].IPs[0].Address.IP.String()
163+
srcNetName := results[src].Interfaces[0].Name
164+
dstNetName := results[dst].Interfaces[0].Name
165+
166+
fmt.Fprintf(GinkgoWriter, "ping %s (ns%d@%s) -> %s (ns%d@%s)...",
167+
saddr, src, srcNetName, daddr, dst, dstNetName)
168+
timeoutSec := 1
169+
if err := testutils.Ping(saddr, daddr, timeoutSec); err != nil {
170+
fmt.Fprintln(GinkgoWriter, "unpingable")
171+
return err
172+
}
173+
fmt.Fprintln(GinkgoWriter, "pingable")
174+
return nil
175+
})
176+
}
177+
178+
// ns0@foo can ping to ns1@foo
179+
err := ping(0, 1)
180+
Expect(err).NotTo(HaveOccurred())
181+
182+
// ns1@foo can ping to ns0@foo
183+
err = ping(1, 0)
184+
Expect(err).NotTo(HaveOccurred())
185+
186+
// ns0@foo cannot ping to ns2@bar
187+
err = ping(0, 2)
188+
Expect(err).To(HaveOccurred())
189+
190+
// ns1@foo cannot ping to ns2@bar
191+
err = ping(1, 2)
192+
Expect(err).To(HaveOccurred())
193+
194+
// ns2@bar cannot ping to ns0@foo
195+
err = ping(2, 0)
196+
Expect(err).To(HaveOccurred())
197+
198+
// ns2@bar cannot ping to ns1@foo
199+
err = ping(2, 1)
200+
Expect(err).To(HaveOccurred())
201+
})
202+
})
203+
})

0 commit comments

Comments
 (0)