diff --git a/pkg/firewalld/firewalld.go b/pkg/firewalld/firewalld.go new file mode 100644 index 00000000..f69b7ef0 --- /dev/null +++ b/pkg/firewalld/firewalld.go @@ -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) +} diff --git a/plugins/meta/firewall/README.md b/plugins/meta/firewall/README.md index b9b83b63..a437c8d0 100644 --- a/plugins/meta/firewall/README.md +++ b/plugins/meta/firewall/README.md @@ -2,23 +2,41 @@ ## 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 is only useful when used in addition to other plugins. ## Operation The following network configuration file -``` + +```json { - "name": "mynet", - "type": "firewall", - "ifName": "cni0" + "cniVersion": "0.3.1", + "name": "bridge-firewalld", + "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 diff --git a/plugins/meta/firewall/firewall.go b/plugins/meta/firewall/firewall.go index a278d590..44099bc5 100644 --- a/plugins/meta/firewall/firewall.go +++ b/plugins/meta/firewall/firewall.go @@ -74,20 +74,22 @@ func parseConf(data []byte) (*FirewallNetConf, error) { } // 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) - } + if conf.RawPrevResult == nil { + return nil, fmt.Errorf("missing prevResult from earlier plugin") + } + + 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 diff --git a/plugins/meta/firewall/firewall_firewalld_test.go b/plugins/meta/firewall/firewall_firewalld_test.go new file mode 100644 index 00000000..1d242c39 --- /dev/null +++ b/plugins/meta/firewall/firewall_firewalld_test.go @@ -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")) + }) +}) diff --git a/plugins/meta/firewall/firewalld.go b/plugins/meta/firewall/firewalld.go new file mode 100644 index 00000000..101cece0 --- /dev/null +++ b/plugins/meta/firewall/firewalld.go @@ -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 +}