From 5e167a6493e6764eeaec9b031a6ada54939c1bd0 Mon Sep 17 00:00:00 2001 From: Jiri Popelka Date: Wed, 26 Nov 2014 12:14:50 +0100 Subject: [PATCH 1/3] Support for Firewalld Firewalld [1] is a firewall managing daemon with D-Bus interface. What sort of problem are we trying to solve with this ? Firewalld internally also executes iptables/ip6tables to change firewall settings. It might happen on systems where both docker and firewalld are running concurrently, that both of them try to call iptables at the same time. The result is that the second one fails because the first one is holding a xtables lock. One workaround is to use --wait/-w option in both docker & firewalld when calling iptables. It's already been done in both upstreams: https://github.com/docker/docker/commit/b315c380f4acd65cc0428009702f99a266f96c59 https://github.com/t-woerner/firewalld/commit/b3b451d6f8946986b8f50c8bcddeef50ed7a5f8f But it'd still be better if docker used firewalld when it's running. Other problem the firewalld support would solve is that iptables/firewalld service's restart flushes all firewall rules previously added by docker. See next patch for possible solution. This patch utilizes firewalld's D-Bus interface. If firewalld is running, we call direct.passthrough() [2] method instead of executing iptables directly. direct.passthrough() takes the same arguments as iptables tool itself and passes them through to iptables tool. It might be better to use other methods, like direct.addChain and direct.addRule [3] so it'd be more intergrated with firewalld, but that'd make the patch much bigger. If firewalld is not running, everything works as before. [1] http://www.firewalld.org/ [2] https://jpopelka.fedorapeople.org/firewalld/doc/firewalld.dbus.html#FirewallD1.direct.Methods.passthrough [3] https://jpopelka.fedorapeople.org/firewalld/doc/firewalld.dbus.html#FirewallD1.direct.Methods.addChain https://jpopelka.fedorapeople.org/firewalld/doc/firewalld.dbus.html#FirewallD1.direct.Methods.addRule Signed-off-by: Jiri Popelka --- iptables/firewalld.go | 94 +++++++++++++++++++++++++++++++++++++++++++ iptables/iptables.go | 7 ++++ 2 files changed, 101 insertions(+) create mode 100644 iptables/firewalld.go diff --git a/iptables/firewalld.go b/iptables/firewalld.go new file mode 100644 index 0000000..cb7e0b4 --- /dev/null +++ b/iptables/firewalld.go @@ -0,0 +1,94 @@ +package iptables + +import ( + "github.com/Sirupsen/logrus" + "github.com/godbus/dbus" +) + +type IPV string + +const ( + Iptables IPV = "ipv4" + Ip6tables IPV = "ipv6" + Ebtables IPV = "eb" +) +const ( + dbusInterface = "org.fedoraproject.FirewallD1" + dbusPath = "/org/fedoraproject/FirewallD1" +) + +// Conn is a connection to firewalld dbus endpoint. +type Conn struct { + sysconn *dbus.Conn + sysobj *dbus.Object + signal chan *dbus.Signal +} + +var ( + connection *Conn + firewalldRunning bool // is Firewalld service running +) + +func FirewalldInit() { + var err error + + connection, err = newConnection() + + if err != nil { + logrus.Errorf("Failed to connect to D-Bus system bus: %s", err) + } + + firewalldRunning = checkRunning() +} + +// New() establishes a connection to the system bus. +func newConnection() (*Conn, error) { + c := new(Conn) + if err := c.initConnection(); err != nil { + return nil, err + } + + return c, nil +} + +// Innitialize D-Bus connection. +func (c *Conn) initConnection() error { + var err error + + c.sysconn, err = dbus.SystemBus() + if err != nil { + return err + } + + // This never fails, even if the service is not running atm. + c.sysobj = c.sysconn.Object(dbusInterface, dbus.ObjectPath(dbusPath)) + + return nil +} + +// Call some remote method to see whether the service is actually running. +func checkRunning() bool { + var zone string + var err error + + if connection != nil { + err = connection.sysobj.Call(dbusInterface+".getDefaultZone", 0).Store(&zone) + logrus.Infof("Firewalld running: %t", err == nil) + return err == nil + } + logrus.Info("Firewalld not running") + return false +} + +// Firewalld's passthrough method simply passes args through to iptables/ip6tables +func Passthrough(ipv IPV, args ...string) ([]byte, error) { + var output string + + logrus.Debugf("Firewalld passthrough: %s, %s", ipv, args) + err := connection.sysobj.Call(dbusInterface+".direct.passthrough", 0, ipv, args).Store(&output) + if output != "" { + logrus.Debugf("passthrough output: %s", output) + } + + return []byte(output), err +} diff --git a/iptables/iptables.go b/iptables/iptables.go index f8b3aa7..9019f34 100644 --- a/iptables/iptables.go +++ b/iptables/iptables.go @@ -275,6 +275,13 @@ func Exists(table Table, chain string, rule ...string) bool { // Call 'iptables' system command, passing supplied arguments func Raw(args ...string) ([]byte, error) { + if firewalldRunning { + output, err := Passthrough(Iptables, args...) + if err == nil || !strings.Contains(err.Error(), "was not provided by any .service files") { + return output, err + } + + } if err := initCheck(); err != nil { return nil, err From bc71145164603247bfba4cda938aceaf1250b455 Mon Sep 17 00:00:00 2001 From: Jiri Popelka Date: Wed, 26 Nov 2014 19:10:35 +0100 Subject: [PATCH 2/3] React to firewalld's reload/restart When firewalld (or iptables service) restarts/reloads, all previously added docker firewall rules are flushed. With firewalld we can react to its Reloaded() [1] D-Bus signal and recreate the firewall rules. Also when firewalld gets restarted (stopped & started) we can catch the NameOwnerChanged signal [2]. To specify which signals we want to react to we use AddMatch [3]. Libvirt has been doing this for quite a long time now. Docker changes firewall rules on basically 3 places. 1) daemon/networkdriver/portmapper/mapper.go - port mappings Portmapper fortunatelly keeps list of mapped ports, so we can easily recreate firewall rules on firewalld restart/reload New ReMapAll() function does that 2) daemon/networkdriver/bridge/driver.go When setting a bridge, basic firewall rules are created. This is done at once during start, it's parametrized and nowhere tracked so how can one know what and how to set it again when there's been firewalld restart/reload ? The only solution that came to my mind is using of closures [4], i.e. I keep list of references to closures (anonymous functions together with a referencing environment) and when there's firewalld restart/reload I re-call them in the same order. 3) links/links.go - linking containers Link is added in Enable() and removed in Disable(). In Enable() we add a callback function, which creates the link, that's OK so far. It'd be ideal if we could remove the same function from the list in Disable(). Unfortunatelly that's not possible AFAICT, because we don't know the reference to that function at that moment, so we can only add a reference to function, which removes the link. That means that after creating and removing a link there are 2 functions in the list, one adding and one removing the link and after firewalld restart/reload both are called. It works, but it's far from ideal. [1] https://jpopelka.fedorapeople.org/firewalld/doc/firewalld.dbus.html#FirewallD1.Signals.Reloaded [2] http://dbus.freedesktop.org/doc/dbus-specification.html#bus-messages-name-owner-changed [3] http://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules [4] https://en.wikipedia.org/wiki/Closure_%28computer_programming%29 Signed-off-by: Jiri Popelka --- iptables/firewalld.go | 71 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/iptables/firewalld.go b/iptables/firewalld.go index cb7e0b4..3087794 100644 --- a/iptables/firewalld.go +++ b/iptables/firewalld.go @@ -1,8 +1,10 @@ package iptables import ( + "fmt" "github.com/Sirupsen/logrus" "github.com/godbus/dbus" + "strings" ) type IPV string @@ -26,7 +28,8 @@ type Conn struct { var ( connection *Conn - firewalldRunning bool // is Firewalld service running + firewalldRunning bool // is Firewalld service running + onReloaded []*func() // callbacks when Firewalld has been reloaded ) func FirewalldInit() { @@ -63,9 +66,75 @@ func (c *Conn) initConnection() error { // This never fails, even if the service is not running atm. c.sysobj = c.sysconn.Object(dbusInterface, dbus.ObjectPath(dbusPath)) + rule := fmt.Sprintf("type='signal',path='%s',interface='%s',sender='%s',member='Reloaded'", + dbusPath, dbusInterface, dbusInterface) + c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule) + + rule = fmt.Sprintf("type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus',sender='org.freedesktop.DBus',arg0='%s'", + dbusInterface) + c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule) + + c.signal = make(chan *dbus.Signal, 10) + c.sysconn.Signal(c.signal) + go signalHandler() + return nil } +func signalHandler() { + if connection != nil { + for signal := range connection.signal { + if strings.Contains(signal.Name, "NameOwnerChanged") { + firewalldRunning = checkRunning() + dbusConnectionChanged(signal.Body) + } else if strings.Contains(signal.Name, "Reloaded") { + reloaded() + } + } + } +} + +func dbusConnectionChanged(args []interface{}) { + name := args[0].(string) + old_owner := args[1].(string) + new_owner := args[2].(string) + + if name != dbusInterface { + return + } + + if len(new_owner) > 0 { + connectionEstablished() + } else if len(old_owner) > 0 { + connectionLost() + } +} + +func connectionEstablished() { + reloaded() +} + +func connectionLost() { + // Doesn't do anything for now. Libvirt also doesn't react to this. +} + +// call all callbacks +func reloaded() { + for _, pf := range onReloaded { + (*pf)() + } +} + +// add callback +func OnReloaded(callback func()) { + for _, pf := range onReloaded { + if pf == &callback { + return + } + } + onReloaded = append(onReloaded, &callback) +} + // Call some remote method to see whether the service is actually running. func checkRunning() bool { var zone string From 4bc66e193f6ca3a2d5790e8583fc5229a8a25206 Mon Sep 17 00:00:00 2001 From: Jiri Popelka Date: Thu, 15 Jan 2015 18:23:20 +0100 Subject: [PATCH 3/3] Firewalld tests Signed-off-by: Jiri Popelka --- iptables/firewalld_test.go | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 iptables/firewalld_test.go diff --git a/iptables/firewalld_test.go b/iptables/firewalld_test.go new file mode 100644 index 0000000..3896007 --- /dev/null +++ b/iptables/firewalld_test.go @@ -0,0 +1,78 @@ +package iptables + +import ( + "net" + "strconv" + "testing" +) + +func TestFirewalldInit(t *testing.T) { + FirewalldInit() +} + +func TestReloaded(t *testing.T) { + var err error + var fwdChain *Chain + + fwdChain, err = NewChain("FWD", "lo", Filter) + if err != nil { + t.Fatal(err) + } + defer fwdChain.Remove() + + // copy-pasted from iptables_test:TestLink + ip1 := net.ParseIP("192.168.1.1") + ip2 := net.ParseIP("192.168.1.2") + port := 1234 + proto := "tcp" + + err = fwdChain.Link(Append, ip1, ip2, port, proto) + if err != nil { + t.Fatal(err) + } else { + // to be re-called again later + OnReloaded(func() { fwdChain.Link(Append, ip1, ip2, port, proto) }) + } + + rule1 := []string{ + "-i", fwdChain.Bridge, + "-o", fwdChain.Bridge, + "-p", proto, + "-s", ip1.String(), + "-d", ip2.String(), + "--dport", strconv.Itoa(port), + "-j", "ACCEPT"} + + if !Exists(fwdChain.Table, fwdChain.Name, rule1...) { + t.Fatalf("rule1 does not exist") + } + + // flush all rules + fwdChain.Remove() + + reloaded() + + // make sure the rules have been recreated + if !Exists(fwdChain.Table, fwdChain.Name, rule1...) { + t.Fatalf("rule1 hasn't been recreated") + } +} + +func TestPassthrough(t *testing.T) { + rule1 := []string{ + "-i", "lo", + "-p", "udp", + "--dport", "123", + "-j", "ACCEPT"} + + if firewalldRunning { + _, err := Passthrough(Iptables, append([]string{"-A"}, rule1...)...) + if err != nil { + t.Fatal(err) + } + if !Exists(Filter, "INPUT", rule1...) { + t.Fatalf("rule1 does not exist") + } + } + +}