firewall: add firewalld functionality to firewall plugin

Example of usage, which uses flannel for allocating IP
addresses for containers and then registers them in `trusted`
zone in firewalld:

{
  "cniVersion": "0.3.1",
  "name": "flannel-firewalld",
  "plugins": [
    {
      "name": "cbr0",
      "type": "flannel",
      "delegate": {
        "isDefaultGateway": true
      }
    },
    {
      "type": "firewall",
      "backend": "firewalld",
      "zone": "trusted"
    }
  ]
}

Fixes #114

Signed-off-by: Alban Crequy <alban@kinvolk.io>
Signed-off-by: Michal Rostecki <mrostecki@suse.com>
This commit is contained in:
Michal Rostecki
2018-02-06 17:19:17 +01:00
committed by Michael Cambria
parent eb66fc201c
commit 9d6f1e9975
5 changed files with 386 additions and 21 deletions

View File

@ -0,0 +1,72 @@
// Copyright 2018 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 firewalld
import (
"log"
"net"
"strings"
"github.com/godbus/dbus"
)
const (
dbusName = "org.freedesktop.DBus"
dbusPath = "/org/freedesktop/DBus"
dbusGetNameOwnerMethod = ".GetNameOwner"
FirewalldName = "org.fedoraproject.FirewallD1"
FirewalldPath = "/org/fedoraproject/FirewallD1"
FirewalldZoneInterface = "org.fedoraproject.FirewallD1.zone"
FirewalldAddSourceMethod = ".addSource"
FirewalldRemoveSourceMethod = ".removeSource"
ErrZoneAlreadySet = "ZONE_ALREADY_SET"
)
// IsRunning checks whether firewalld is running.
func IsRunning(conn *dbus.Conn) bool {
dbusObj := conn.Object(dbusName, dbusPath)
var res string
if err := dbusObj.Call(dbusName+dbusGetNameOwnerMethod, 0, FirewalldName).Store(&res); err != nil {
return false
}
return true
}
// AddSourceToZone adds a firewalld rule which assigns the given source IP
// to the given zone.
func AddSourceToZone(conn *dbus.Conn, source net.IP, zone string) error {
firewalldObj := conn.Object(FirewalldName, FirewalldPath)
var res string
if err := firewalldObj.Call(FirewalldZoneInterface+FirewalldAddSourceMethod, 0, zone, source.String()).Store(&res); err != nil {
if strings.Contains(err.Error(), ErrZoneAlreadySet) {
log.Printf("ip %v already bound to %q zone, it can mean that this address was assigned before to the another container without cleanup\n", source, zone)
} else {
return err
}
}
return nil
}
// RemoveSourceFromZone removes firewalld rules which assigned the given source IP
// to the given zone.
func RemoveSourceFromZone(conn *dbus.Conn, source net.IP, zone string) error {
firewalldObj := conn.Object(FirewalldName, FirewalldPath)
var res string
return firewalldObj.Call(FirewalldZoneInterface+FirewalldRemoveSourceMethod, 0, zone, source.String()).Store(&res)
}

View File

@ -2,23 +2,41 @@
## Overview ## Overview
This plugin creates firewall rules to allow traffic to/from the host network interface given by "ifName". This plugin creates firewall rules to allow traffic to/from container IP address via the host network .
It does not create any network interfaces and therefore does not set up connectivity by itself. 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. It is only useful when used in addition to other plugins.
## Operation ## Operation
The following network configuration file The following network configuration file
```
```json
{ {
"name": "mynet", "cniVersion": "0.3.1",
"type": "firewall", "name": "bridge-firewalld",
"ifName": "cni0" "plugins": [
{
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.88.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
},
{
"type": "firewall",
}
]
} }
``` ```
will allow the given interface to send/receive traffic via the host. will allow any IP addresses configured by earlier plugins 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. 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 previous result.
## Backends ## Backends

View File

@ -74,20 +74,22 @@ func parseConf(data []byte) (*FirewallNetConf, error) {
} }
// Parse previous result. // Parse previous result.
if conf.RawPrevResult != nil { if conf.RawPrevResult == nil {
resultBytes, err := json.Marshal(conf.RawPrevResult) return nil, fmt.Errorf("missing prevResult from earlier plugin")
if err != nil { }
return nil, fmt.Errorf("could not serialize prevResult: %v", err)
} resultBytes, err := json.Marshal(conf.RawPrevResult)
res, err := version.NewResult(conf.CNIVersion, resultBytes) if err != nil {
if err != nil { return nil, fmt.Errorf("could not serialize prevResult: %v", err)
return nil, fmt.Errorf("could not parse prevResult: %v", err) }
} res, err := version.NewResult(conf.CNIVersion, resultBytes)
conf.RawPrevResult = nil if err != nil {
conf.PrevResult, err = current.NewResultFromResult(res) return nil, fmt.Errorf("could not parse prevResult: %v", err)
if err != nil { }
return nil, fmt.Errorf("could not convert result to current version: %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 return &conf, nil

View File

@ -0,0 +1,197 @@
// Copyright 2018 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 (
"bufio"
"fmt"
"os/exec"
"strings"
"sync"
"syscall"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/plugins/pkg/firewalld"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
"github.com/godbus/dbus"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const (
confTmpl = `{
"cniVersion": "0.3.1",
"name": "firewalld-test",
"type": "firewall",
"backend": "firewalld",
"zone": "trusted",
"prevResult": {
"cniVersion": "0.3.0",
"interfaces": [
{"name": "%s", "sandbox": "%s"}
],
"ips": [
{
"version": "4",
"address": "10.0.0.2/24",
"gateway": "10.0.0.1",
"interface": 0
}
],
"routes": []
}
}`
ifname = "eth0"
)
type fakeFirewalld struct {
zone string
source string
}
func (f *fakeFirewalld) clear() {
f.zone = ""
f.source = ""
}
func (f *fakeFirewalld) AddSource(zone, source string) (string, *dbus.Error) {
f.zone = zone
f.source = source
return "", nil
}
func (f *fakeFirewalld) RemoveSource(zone, source string) (string, *dbus.Error) {
f.zone = zone
f.source = source
return "", nil
}
func spawnSessionDbus(wg *sync.WaitGroup) (string, *exec.Cmd) {
// Start a private D-Bus session bus
path, err := invoke.FindInPath("dbus-daemon", []string{
"/bin", "/sbin", "/usr/bin", "/usr/sbin",
})
Expect(err).NotTo(HaveOccurred())
cmd := exec.Command(path, "--session", "--print-address", "--nofork", "--nopidfile")
stdout, err := cmd.StdoutPipe()
Expect(err).NotTo(HaveOccurred())
err = cmd.Start()
Expect(err).NotTo(HaveOccurred())
// Wait for dbus-daemon to print the bus address
bytes, err := bufio.NewReader(stdout).ReadString('\n')
Expect(err).NotTo(HaveOccurred())
busAddr := strings.TrimSpace(string(bytes))
Expect(strings.HasPrefix(busAddr, "unix:abstract")).To(BeTrue())
var startWg sync.WaitGroup
wg.Add(1)
startWg.Add(1)
go func() {
defer GinkgoRecover()
startWg.Done()
err = cmd.Wait()
Expect(err).NotTo(HaveOccurred())
wg.Done()
}()
startWg.Wait()
return busAddr, cmd
}
var _ = Describe("firewalld test", func() {
var (
targetNs ns.NetNS
cmd *exec.Cmd
conn *dbus.Conn
wg sync.WaitGroup
fwd *fakeFirewalld
busAddr string
)
BeforeEach(func() {
var err error
targetNs, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
// Start a private D-Bus session bus
busAddr, cmd = spawnSessionDbus(&wg)
conn, err = dbus.Dial(busAddr)
Expect(err).NotTo(HaveOccurred())
err = conn.Auth(nil)
Expect(err).NotTo(HaveOccurred())
err = conn.Hello()
Expect(err).NotTo(HaveOccurred())
// Start our fake firewalld
reply, err := conn.RequestName(firewalld.FirewalldName, dbus.NameFlagDoNotQueue)
Expect(err).NotTo(HaveOccurred())
Expect(reply).To(Equal(dbus.RequestNameReplyPrimaryOwner))
fwd = &fakeFirewalld{}
// Because firewalld D-Bus methods start with lower-case, and
// because in Go lower-case methods are private, we need to remap
// Go public methods to the D-Bus name
methods := map[string]string{
"AddSource": "addSource",
"RemoveSource": "removeSource",
}
conn.ExportWithMap(fwd, methods, firewalld.FirewalldPath, firewalld.FirewalldZoneInterface)
// Make sure the plugin uses our private session bus
testConn = conn
})
AfterEach(func() {
_, err := conn.ReleaseName(firewalld.FirewalldName)
Expect(err).NotTo(HaveOccurred())
err = cmd.Process.Signal(syscall.SIGTERM)
Expect(err).NotTo(HaveOccurred())
wg.Wait()
})
It("works with a 0.3.1 config", func() {
Expect(firewalld.IsRunning(conn)).To(BeTrue())
conf := fmt.Sprintf(confTmpl, ifname, targetNs.Path())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: targetNs.Path(),
IfName: ifname,
StdinData: []byte(conf),
}
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(fwd.zone).To(Equal("trusted"))
Expect(fwd.source).To(Equal("10.0.0.2/32"))
fwd.clear()
err = testutils.CmdDelWithResult(targetNs.Path(), ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(fwd.zone).To(Equal("trusted"))
Expect(fwd.source).To(Equal("10.0.0.2/32"))
})
})

View File

@ -0,0 +1,76 @@
// Copyright 2018 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 (
"fmt"
"github.com/containernetworking/plugins/pkg/firewalld"
"github.com/godbus/dbus"
)
// Only used for testcases to override the D-Bus connection
var testConn *dbus.Conn
type fwdBackend struct {
conn *dbus.Conn
}
// fwdBackend implements the FirewallBackend interface
var _ FirewallBackend = &fwdBackend{}
func getConn() (*dbus.Conn, error) {
if testConn != nil {
return testConn, nil
}
return dbus.SystemBus()
}
func isFirewalldRunning() bool {
conn, err := getConn()
if err != nil {
return false
}
return firewalld.IsRunning(conn)
}
func newFirewalldBackend(conf *FirewallNetConf) (FirewallBackend, error) {
conn, err := getConn()
if err != nil {
return nil, err
}
backend := &fwdBackend{
conn: conn,
}
return backend, nil
}
func (fb *fwdBackend) Add(conf *FirewallNetConf) error {
for _, ip := range conf.PrevResult.IPs {
if err := firewalld.AddSourceToZone(fb.conn, ip.Address.IP, conf.FirewalldZone); err != nil {
return fmt.Errorf("failed to add the address %v to %v zone: %v", ip.Address.IP, conf.FirewalldZone, err)
}
}
return nil
}
func (fb *fwdBackend) Del(conf *FirewallNetConf) error {
for _, ip := range conf.PrevResult.IPs {
firewalld.RemoveSourceFromZone(fb.conn, ip.Address.IP, conf.FirewalldZone)
}
return nil
}