firewall: new plugin which allows a host interface to send/receive traffic
Distros often have additional rules in the their iptabvles 'filter' table that do things like: -A FORWARD -j REJECT --reject-with icmp-host-prohibited docker, for example, gets around this by adding explicit rules to the filter table's FORWARD chain to allow traffic from the docker0 interface. Do that for a given host interface too, as a chained plugin.
This commit is contained in:
parent
e9e1d37309
commit
d096a4df48
12
plugins/linux_only.txt
Normal file
12
plugins/linux_only.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
plugins/ipam/dhcp
|
||||||
|
plugins/main/bridge
|
||||||
|
plugins/main/host-device
|
||||||
|
plugins/main/ipvlan
|
||||||
|
plugins/main/loopback
|
||||||
|
plugins/main/macvlan
|
||||||
|
plugins/main/ptp
|
||||||
|
plugins/main/vlan
|
||||||
|
plugins/meta/portmap
|
||||||
|
plugins/meta/tuning
|
||||||
|
plugins/meta/bandwidth
|
||||||
|
plugins/meta/firewall
|
33
plugins/meta/firewall/README.md
Normal file
33
plugins/meta/firewall/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# firewall plugin
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This plugin creates firewall rules to allow traffic to/from the host network interface given by "ifName".
|
||||||
|
It does not create any network interfaces and therefore does not set up connectivity by itself.
|
||||||
|
It is only useful when used in addition to other plugins.
|
||||||
|
|
||||||
|
## Operation
|
||||||
|
The following network configuration file
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "firewall",
|
||||||
|
"ifName": "cni0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will allow the given interface to send/receive traffic via the host.
|
||||||
|
|
||||||
|
A successful result would simply be an empty result, unless a previous plugin passed a previous result, in which case this plugin will return that verbatim.
|
||||||
|
|
||||||
|
## Backends
|
||||||
|
|
||||||
|
This plugin supports multiple firewall backends that implement the desired functionality.
|
||||||
|
Available backends include `iptables` and `firewalld` and may be selected with the `backend` key.
|
||||||
|
If no `backend` key is given, the plugin will use firewalld if the service exists on the D-Bus system bus.
|
||||||
|
If no firewalld service is found, it will fall back to iptables.
|
||||||
|
|
||||||
|
When the `iptables` backend is used, the above example will create two new iptables chains in the `filter` table and add rules that allow the given interface to send/receive traffic.
|
||||||
|
When the `firewalld` backend is used, the above example will place the `cni0` interface into firewalld's `trusted` zone, allowing it to send/receive traffic.
|
||||||
|
|
||||||
|
|
162
plugins/meta/firewall/firewall.go
Normal file
162
plugins/meta/firewall/firewall.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright 2016 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 "meta-plugin". It reads in its own netconf, it does not create
|
||||||
|
// any network interface but just changes the network sysctl.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
"github.com/containernetworking/plugins/pkg/ns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FirewallNetConf represents the firewall configuration.
|
||||||
|
type FirewallNetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
|
||||||
|
// Backend is the firewall type to add rules to. Allowed values are
|
||||||
|
// 'iptables' and 'firewalld'.
|
||||||
|
Backend string `json:"backend"`
|
||||||
|
|
||||||
|
// IptablesAdminChainName is an optional name to use instead of the default
|
||||||
|
// admin rules override chain name that includes the interface name.
|
||||||
|
IptablesAdminChainName string `json:"iptablesAdminChainName,omitempty"`
|
||||||
|
|
||||||
|
// FirewalldZone is an optional firewalld zone to place the interface into. If
|
||||||
|
// the firewalld backend is used but the zone is not given, it defaults
|
||||||
|
// to 'trusted'
|
||||||
|
FirewalldZone string `json:"firewalldZone,omitempty"`
|
||||||
|
|
||||||
|
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||||
|
PrevResult *current.Result `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallBackend interface {
|
||||||
|
Add(*FirewallNetConf) error
|
||||||
|
Del(*FirewallNetConf) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipString(ip net.IPNet) string {
|
||||||
|
if ip.IP.To4() == nil {
|
||||||
|
return ip.IP.String() + "/128"
|
||||||
|
}
|
||||||
|
return ip.IP.String() + "/32"
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConf(data []byte) (*FirewallNetConf, error) {
|
||||||
|
conf := FirewallNetConf{}
|
||||||
|
if err := json.Unmarshal(data, &conf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default the firewalld zone to trusted
|
||||||
|
if conf.FirewalldZone == "" {
|
||||||
|
conf.FirewalldZone = "trusted"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse previous result.
|
||||||
|
if conf.RawPrevResult != nil {
|
||||||
|
resultBytes, err := json.Marshal(conf.RawPrevResult)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not serialize prevResult: %v", err)
|
||||||
|
}
|
||||||
|
res, err := version.NewResult(conf.CNIVersion, resultBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse prevResult: %v", err)
|
||||||
|
}
|
||||||
|
conf.RawPrevResult = nil
|
||||||
|
conf.PrevResult, err = current.NewResultFromResult(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not convert result to current version: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBackend(conf *FirewallNetConf) (FirewallBackend, error) {
|
||||||
|
switch conf.Backend {
|
||||||
|
case "iptables":
|
||||||
|
return newIptablesBackend(conf)
|
||||||
|
case "firewalld":
|
||||||
|
return newFirewalldBackend(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to firewalld if it's running
|
||||||
|
if isFirewalldRunning() {
|
||||||
|
return newFirewalldBackend(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise iptables
|
||||||
|
return newIptablesBackend(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
conf, err := parseConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := getBackend(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := backend.Add(conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := conf.PrevResult
|
||||||
|
if result == nil {
|
||||||
|
result = ¤t.Result{}
|
||||||
|
}
|
||||||
|
return types.PrintResult(result, conf.CNIVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
conf, err := parseConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := getBackend(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tolerate errors if the container namespace has been torn down already
|
||||||
|
containerNS, err := ns.GetNS(args.Netns)
|
||||||
|
if err == nil {
|
||||||
|
defer containerNS.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime errors are ignored
|
||||||
|
if err := backend.Del(conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
368
plugins/meta/firewall/firewall_iptables_test.go
Normal file
368
plugins/meta/firewall/firewall_iptables_test.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
// Copyright 2017 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
"github.com/containernetworking/plugins/pkg/ns"
|
||||||
|
"github.com/containernetworking/plugins/pkg/testutils"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findChains(chains []string) (bool, bool) {
|
||||||
|
var foundAdmin, foundPriv bool
|
||||||
|
for _, ch := range chains {
|
||||||
|
if ch == "CNI-ADMIN" {
|
||||||
|
foundAdmin = true
|
||||||
|
} else if ch == "CNI-FORWARD" {
|
||||||
|
foundPriv = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundAdmin, foundPriv
|
||||||
|
}
|
||||||
|
|
||||||
|
func findForwardJumpRules(rules []string) (bool, bool) {
|
||||||
|
var foundAdmin, foundPriv bool
|
||||||
|
for _, rule := range rules {
|
||||||
|
if strings.Contains(rule, "-j CNI-ADMIN") {
|
||||||
|
foundAdmin = true
|
||||||
|
} else if strings.Contains(rule, "-j CNI-FORWARD") {
|
||||||
|
foundPriv = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundAdmin, foundPriv
|
||||||
|
}
|
||||||
|
|
||||||
|
func findForwardAllowRules(rules []string, ip string) (bool, bool) {
|
||||||
|
var foundOne, foundTwo bool
|
||||||
|
for _, rule := range rules {
|
||||||
|
if !strings.HasSuffix(rule, "-j ACCEPT") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(rule, fmt.Sprintf(" -s %s ", ip)) {
|
||||||
|
foundOne = true
|
||||||
|
} else if strings.Contains(rule, fmt.Sprintf(" -d %s ", ip)) && strings.Contains(rule, "RELATED,ESTABLISHED") {
|
||||||
|
foundTwo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundOne, foundTwo
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrevResult(bytes []byte) *current.Result {
|
||||||
|
type TmpConf struct {
|
||||||
|
types.NetConf
|
||||||
|
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||||
|
PrevResult *current.Result `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &TmpConf{}
|
||||||
|
err := json.Unmarshal(bytes, conf)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
if conf.RawPrevResult == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBytes, err := json.Marshal(conf.RawPrevResult)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
res, err := version.NewResult(conf.CNIVersion, resultBytes)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
prevResult, err := current.NewResultFromResult(res)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return prevResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFullRuleset(bytes []byte) {
|
||||||
|
prevResult := getPrevResult(bytes)
|
||||||
|
|
||||||
|
for _, ip := range prevResult.IPs {
|
||||||
|
ipt, err := iptables.NewWithProtocol(protoForIP(ip.Address))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Ensure chains
|
||||||
|
chains, err := ipt.ListChains("filter")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
foundAdmin, foundPriv := findChains(chains)
|
||||||
|
Expect(foundAdmin).To(Equal(true))
|
||||||
|
Expect(foundPriv).To(Equal(true))
|
||||||
|
|
||||||
|
// Look for the FORWARD chain jump rules to our custom chains
|
||||||
|
rules, err := ipt.List("filter", "FORWARD")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(rules)).Should(BeNumerically(">", 1))
|
||||||
|
_, foundPriv = findForwardJumpRules(rules)
|
||||||
|
Expect(foundPriv).To(Equal(true))
|
||||||
|
|
||||||
|
// Look for the allow rules in our custom FORWARD chain
|
||||||
|
rules, err = ipt.List("filter", "CNI-FORWARD")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(rules)).Should(BeNumerically(">", 1))
|
||||||
|
foundAdmin, _ = findForwardJumpRules(rules)
|
||||||
|
Expect(foundAdmin).To(Equal(true))
|
||||||
|
|
||||||
|
// Look for the IP allow rules
|
||||||
|
foundOne, foundTwo := findForwardAllowRules(rules, ipString(ip.Address))
|
||||||
|
Expect(foundOne).To(Equal(true))
|
||||||
|
Expect(foundTwo).To(Equal(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCleanedUp(bytes []byte) {
|
||||||
|
prevResult := getPrevResult(bytes)
|
||||||
|
|
||||||
|
for _, ip := range prevResult.IPs {
|
||||||
|
ipt, err := iptables.NewWithProtocol(protoForIP(ip.Address))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Our private and admin chains don't get cleaned up
|
||||||
|
chains, err := ipt.ListChains("filter")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
foundAdmin, foundPriv := findChains(chains)
|
||||||
|
Expect(foundAdmin).To(Equal(true))
|
||||||
|
Expect(foundPriv).To(Equal(true))
|
||||||
|
|
||||||
|
// Look for the FORWARD chain jump rules to our custom chains
|
||||||
|
rules, err := ipt.List("filter", "FORWARD")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, foundPriv = findForwardJumpRules(rules)
|
||||||
|
Expect(foundPriv).To(Equal(true))
|
||||||
|
|
||||||
|
// Look for the allow rules in our custom FORWARD chain
|
||||||
|
rules, err = ipt.List("filter", "CNI-FORWARD")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
foundAdmin, _ = findForwardJumpRules(rules)
|
||||||
|
Expect(foundAdmin).To(Equal(true))
|
||||||
|
|
||||||
|
// Expect no IP address rules for this IP
|
||||||
|
foundOne, foundTwo := findForwardAllowRules(rules, ipString(ip.Address))
|
||||||
|
Expect(foundOne).To(Equal(false))
|
||||||
|
Expect(foundTwo).To(Equal(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("firewall plugin iptables backend", func() {
|
||||||
|
var originalNS, targetNS ns.NetNS
|
||||||
|
const IFNAME string = "dummy0"
|
||||||
|
|
||||||
|
fullConf := []byte(`{
|
||||||
|
"name": "test",
|
||||||
|
"type": "firewall",
|
||||||
|
"backend": "iptables",
|
||||||
|
"ifName": "dummy0",
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"prevResult": {
|
||||||
|
"interfaces": [
|
||||||
|
{"name": "dummy0"}
|
||||||
|
],
|
||||||
|
"ips": [
|
||||||
|
{
|
||||||
|
"version": "4",
|
||||||
|
"address": "10.0.0.2/24",
|
||||||
|
"interface": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"address": "2001:db8:1:2::1/64",
|
||||||
|
"interface": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Create a new NetNS so we don't modify the host
|
||||||
|
var err error
|
||||||
|
originalNS, err = testutils.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err = netlink.LinkAdd(&netlink.Dummy{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: IFNAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, err = netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
targetNS, err = testutils.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
Expect(targetNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("passes prevResult through unchanged", func() {
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNS.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: fullConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
r, _, err := testutils.CmdAddWithResult(targetNS.Path(), IFNAME, fullConf, func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
result, err := current.GetResult(r)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(len(result.Interfaces)).To(Equal(1))
|
||||||
|
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||||
|
Expect(len(result.IPs)).To(Equal(2))
|
||||||
|
Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24"))
|
||||||
|
Expect(result.IPs[1].Address.String()).To(Equal("2001:db8:1:2::1/64"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("installs the right iptables rules on the host", func() {
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNS.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: fullConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, _, err := testutils.CmdAddWithResult(targetNS.Path(), IFNAME, fullConf, func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
validateFullRuleset(fullConf)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("correctly handles a custom IptablesAdminChainName", func() {
|
||||||
|
conf := []byte(`{
|
||||||
|
"name": "test",
|
||||||
|
"type": "firewall",
|
||||||
|
"backend": "iptables",
|
||||||
|
"ifName": "dummy0",
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"iptablesAdminChainName": "CNI-foobar",
|
||||||
|
"prevResult": {
|
||||||
|
"interfaces": [
|
||||||
|
{"name": "dummy0"}
|
||||||
|
],
|
||||||
|
"ips": [
|
||||||
|
{
|
||||||
|
"version": "4",
|
||||||
|
"address": "10.0.0.2/24",
|
||||||
|
"interface": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"address": "2001:db8:1:2::1/64",
|
||||||
|
"interface": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNS.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: conf,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, _, err := testutils.CmdAddWithResult(targetNS.Path(), IFNAME, conf, func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var ipt *iptables.IPTables
|
||||||
|
for _, proto := range []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6} {
|
||||||
|
ipt, err = iptables.NewWithProtocol(proto)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Ensure custom admin chain name
|
||||||
|
chains, err := ipt.ListChains("filter")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
var foundAdmin bool
|
||||||
|
for _, ch := range chains {
|
||||||
|
if ch == "CNI-foobar" {
|
||||||
|
foundAdmin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(foundAdmin).To(Equal(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("cleans up on delete", func() {
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNS.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: fullConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, _, err := testutils.CmdAddWithResult(targetNS.Path(), IFNAME, fullConf, func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
validateFullRuleset(fullConf)
|
||||||
|
|
||||||
|
err = testutils.CmdDelWithResult(targetNS.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
validateCleanedUp(fullConf)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
27
plugins/meta/firewall/firewall_suite_test.go
Normal file
27
plugins/meta/firewall/firewall_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright 2017 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFirewall(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "firewall Suite")
|
||||||
|
}
|
213
plugins/meta/firewall/iptables.go
Normal file
213
plugins/meta/firewall/iptables.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// Copyright 2016 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 "meta-plugin". It reads in its own netconf, it does not create
|
||||||
|
// any network interface but just changes the network sysctl.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPrivChainRules(ip string) [][]string {
|
||||||
|
var rules [][]string
|
||||||
|
rules = append(rules, []string{"-d", ip, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
|
||||||
|
rules = append(rules, []string{"-s", ip, "-j", "ACCEPT"})
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureChain(ipt *iptables.IPTables, table, chain string) error {
|
||||||
|
chains, err := ipt.ListChains(table)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list iptables chains: %v", err)
|
||||||
|
}
|
||||||
|
for _, ch := range chains {
|
||||||
|
if ch == chain {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipt.NewChain(table, chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFilterRule(privChainName string) []string {
|
||||||
|
return []string{"-m", "comment", "--comment", "CNI firewall plugin rules", "-j", privChainName}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAdminRule(adminChainName string) []string {
|
||||||
|
return []string{"-m", "comment", "--comment", "CNI firewall plugin admin overrides", "-j", adminChainName}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupRules(ipt *iptables.IPTables, privChainName string, rules [][]string) {
|
||||||
|
for _, rule := range rules {
|
||||||
|
ipt.Delete("filter", privChainName, rule...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureFirstChainRule(ipt *iptables.IPTables, chain string, rule []string) error {
|
||||||
|
exists, err := ipt.Exists("filter", chain, rule...)
|
||||||
|
if !exists && err == nil {
|
||||||
|
err = ipt.Insert("filter", chain, 1, rule...)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *iptablesBackend) setupChains(ipt *iptables.IPTables) error {
|
||||||
|
privRule := generateFilterRule(ib.privChainName)
|
||||||
|
adminRule := generateFilterRule(ib.adminChainName)
|
||||||
|
|
||||||
|
// Ensure our private chains exist
|
||||||
|
if err := ensureChain(ipt, "filter", ib.privChainName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureChain(ipt, "filter", ib.adminChainName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure our filter rule exists in the forward chain
|
||||||
|
if err := ensureFirstChainRule(ipt, "FORWARD", privRule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure our admin override chain rule exists in our private chain
|
||||||
|
if err := ensureFirstChainRule(ipt, ib.privChainName, adminRule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func protoForIP(ip net.IPNet) iptables.Protocol {
|
||||||
|
if ip.IP.To4() != nil {
|
||||||
|
return iptables.ProtocolIPv4
|
||||||
|
}
|
||||||
|
return iptables.ProtocolIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *iptablesBackend) addRules(conf *FirewallNetConf, ipt *iptables.IPTables, proto iptables.Protocol) error {
|
||||||
|
rules := make([][]string, 0)
|
||||||
|
for _, ip := range conf.PrevResult.IPs {
|
||||||
|
if protoForIP(ip.Address) == proto {
|
||||||
|
rules = append(rules, getPrivChainRules(ipString(ip.Address))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) > 0 {
|
||||||
|
if err := ib.setupChains(ipt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on any errors
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
cleanupRules(ipt, ib.privChainName, rules)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
err = ipt.AppendUnique("filter", ib.privChainName, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *iptablesBackend) delRules(conf *FirewallNetConf, ipt *iptables.IPTables, proto iptables.Protocol) error {
|
||||||
|
rules := make([][]string, 0)
|
||||||
|
for _, ip := range conf.PrevResult.IPs {
|
||||||
|
if protoForIP(ip.Address) == proto {
|
||||||
|
rules = append(rules, getPrivChainRules(ipString(ip.Address))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) > 0 {
|
||||||
|
cleanupRules(ipt, ib.privChainName, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProtos(conf *FirewallNetConf) []iptables.Protocol {
|
||||||
|
protos := []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6}
|
||||||
|
if conf.PrevResult != nil {
|
||||||
|
// If PrevResult is given, scan all IP addresses to figure out
|
||||||
|
// which IP versions to use
|
||||||
|
protos = []iptables.Protocol{}
|
||||||
|
for _, addr := range conf.PrevResult.IPs {
|
||||||
|
if addr.Address.IP.To4() != nil {
|
||||||
|
protos = append(protos, iptables.ProtocolIPv4)
|
||||||
|
} else {
|
||||||
|
protos = append(protos, iptables.ProtocolIPv6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return protos
|
||||||
|
}
|
||||||
|
|
||||||
|
type iptablesBackend struct {
|
||||||
|
protos map[iptables.Protocol]*iptables.IPTables
|
||||||
|
privChainName string
|
||||||
|
adminChainName string
|
||||||
|
ifName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// iptablesBackend implements the FirewallBackend interface
|
||||||
|
var _ FirewallBackend = &iptablesBackend{}
|
||||||
|
|
||||||
|
func newIptablesBackend(conf *FirewallNetConf) (FirewallBackend, error) {
|
||||||
|
adminChainName := conf.IptablesAdminChainName
|
||||||
|
if adminChainName == "" {
|
||||||
|
adminChainName = "CNI-ADMIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := &iptablesBackend{
|
||||||
|
privChainName: "CNI-FORWARD",
|
||||||
|
adminChainName: adminChainName,
|
||||||
|
protos: make(map[iptables.Protocol]*iptables.IPTables),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proto := range []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6} {
|
||||||
|
ipt, err := iptables.NewWithProtocol(proto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not initialize iptables protocol %v: %v", proto, err)
|
||||||
|
}
|
||||||
|
backend.protos[proto] = ipt
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *iptablesBackend) Add(conf *FirewallNetConf) error {
|
||||||
|
for proto, ipt := range ib.protos {
|
||||||
|
if err := ib.addRules(conf, ipt, proto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *iptablesBackend) Del(conf *FirewallNetConf) error {
|
||||||
|
for proto, ipt := range ib.protos {
|
||||||
|
ib.delRules(conf, ipt, proto)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user