From e8b0fa950bd564639af37f8869f926136abea87a Mon Sep 17 00:00:00 2001
From: Arko Dasgupta <arko.dasgupta@docker.com>
Date: Mon, 4 May 2020 13:51:42 -0700
Subject: [PATCH] Add docker interfaces to firewalld docker zone

If firewalld is running, create a new docker zone and
add the docker interfaces to the docker zone to allow
container networking for distros with firewalld enabled

Fixes: https://github.com/moby/libnetwork/issues/2496

Signed-off-by: Arko Dasgupta <arko.dasgupta@docker.com>
(cherry picked from commit 7a7209221542dc99b316748c97608dfc276c40f6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
---
 iptables/firewalld.go | 136 ++++++++++++++++++++++++++++++++++++++----
 iptables/iptables.go  |  13 ++++
 2 files changed, 139 insertions(+), 10 deletions(-)

diff --git a/iptables/firewalld.go b/iptables/firewalld.go
index 8f13c864..33eb749a 100644
--- a/iptables/firewalld.go
+++ b/iptables/firewalld.go
@@ -19,20 +19,46 @@ const (
 	// Ebtables point to bridge table
 	Ebtables IPV = "eb"
 )
+
 const (
-	dbusInterface = "org.fedoraproject.FirewallD1"
-	dbusPath      = "/org/fedoraproject/FirewallD1"
+	dbusInterface  = "org.fedoraproject.FirewallD1"
+	dbusPath       = "/org/fedoraproject/FirewallD1"
+	dbusConfigPath = "/org/fedoraproject/FirewallD1/config"
+	dockerZone     = "docker"
 )
 
 // Conn is a connection to firewalld dbus endpoint.
 type Conn struct {
-	sysconn *dbus.Conn
-	sysobj  dbus.BusObject
-	signal  chan *dbus.Signal
+	sysconn    *dbus.Conn
+	sysObj     dbus.BusObject
+	sysConfObj dbus.BusObject
+	signal     chan *dbus.Signal
+}
+
+// ZoneSettings holds the firewalld zone settings, documented in
+// https://firewalld.org/documentation/man-pages/firewalld.dbus.html
+type ZoneSettings struct {
+	version            string
+	name               string
+	description        string
+	unused             bool
+	target             string
+	services           []string
+	ports              [][]interface{}
+	icmpBlocks         []string
+	masquerade         bool
+	forwardPorts       [][]interface{}
+	interfaces         []string
+	sourceAddresses    []string
+	richRules          []string
+	protocols          []string
+	sourcePorts        [][]interface{}
+	icmpBlockInversion bool
 }
 
 var (
-	connection       *Conn
+	connection *Conn
+
 	firewalldRunning bool      // is Firewalld service running
 	onReloaded       []*func() // callbacks when Firewalld has been reloaded
 )
@@ -51,6 +77,9 @@ func FirewalldInit() error {
 	}
 	if connection != nil {
 		go signalHandler()
+		if err := setupDockerZone(); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -76,8 +105,8 @@ 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))
-
+	c.sysObj = c.sysconn.Object(dbusInterface, dbus.ObjectPath(dbusPath))
+	c.sysConfObj = c.sysconn.Object(dbusInterface, dbus.ObjectPath(dbusConfigPath))
 	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)
@@ -150,7 +179,7 @@ func checkRunning() bool {
 	var err error
 
 	if connection != nil {
-		err = connection.sysobj.Call(dbusInterface+".getDefaultZone", 0).Store(&zone)
+		err = connection.sysObj.Call(dbusInterface+".getDefaultZone", 0).Store(&zone)
 		return err == nil
 	}
 	return false
@@ -160,8 +189,95 @@ func checkRunning() bool {
 func Passthrough(ipv IPV, args ...string) ([]byte, error) {
 	var output string
 	logrus.Debugf("Firewalld passthrough: %s, %s", ipv, args)
-	if err := connection.sysobj.Call(dbusInterface+".direct.passthrough", 0, ipv, args).Store(&output); err != nil {
+	if err := connection.sysObj.Call(dbusInterface+".direct.passthrough", 0, ipv, args).Store(&output); err != nil {
 		return nil, err
 	}
 	return []byte(output), nil
 }
+
+// getDockerZoneSettings converts the ZoneSettings struct into a interface slice
+func getDockerZoneSettings() map[string]string {
+	return map[string]string{
+		"version":     "1.0",
+		"name":        dockerZone,
+		"description": "zone for docker bridge network interfaces",
+		"target":      "ACCEPT",
+	}
+}
+
+// setupDockerZone creates a zone called docker in firewalld which includes docker interfaces to allow
+// container networking
+func setupDockerZone() error {
+	var zones []string
+	// Check if zone exists
+	if err := connection.sysObj.Call(dbusInterface+".zone.getZones", 0).Store(&zones); err != nil {
+		return err
+	}
+	if contains(zones, dockerZone) {
+		logrus.Infof("Firewalld: %s zone already exists, returning", dockerZone)
+		return nil
+	}
+	logrus.Debugf("Firewalld: creating %s zone", dockerZone)
+
+	settings := getDockerZoneSettings()
+	// Permanent
+	if err := connection.sysConfObj.Call(dbusInterface+".config.addZone", 0, dockerZone, settings).Err; err != nil {
+		return err
+	}
+	// Reload for change to take effect
+	if err := connection.sysObj.Call(dbusInterface+".reload", 0).Err; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// AddInterfaceFirewalld adds the interface to the trusted zone
+func AddInterfaceFirewalld(intf string) error {
+	var intfs []string
+	// Check if interface is already added to the zone
+	if err := connection.sysObj.Call(dbusInterface+".zone.getInterfaces", 0, dockerZone).Store(&intfs); err != nil {
+		return err
+	}
+	// Return if interface is already part of the zone
+	if contains(intfs, intf) {
+		logrus.Infof("Firewalld: interface %s already part of %s zone, returning", intf, dockerZone)
+		return nil
+	}
+
+	logrus.Debugf("Firewalld: adding %s interface to %s zone", intf, dockerZone)
+	// Runtime
+	if err := connection.sysObj.Call(dbusInterface+".zone.addInterface", 0, dockerZone, intf).Err; err != nil {
+		return err
+	}
+	return nil
+}
+
+// DelInterfaceFirewalld removes the interface from the trusted zone
+func DelInterfaceFirewalld(intf string) error {
+	var intfs []string
+	// Check if interface is part of the zone
+	if err := connection.sysObj.Call(dbusInterface+".zone.getInterfaces", 0, dockerZone).Store(&intfs); err != nil {
+		return err
+	}
+	// Remove interface if it exists
+	if !contains(intfs, intf) {
+		return fmt.Errorf("Firewalld: unable to find interface %s in %s zone", intf, dockerZone)
+	}
+
+	logrus.Debugf("Firewalld: removing %s interface from %s zone", intf, dockerZone)
+	// Runtime
+	if err := connection.sysObj.Call(dbusInterface+".zone.removeInterface", 0, dockerZone, intf).Err; err != nil {
+		return err
+	}
+	return nil
+}
+
+func contains(list []string, val string) bool {
+	for _, v := range list {
+		if v == val {
+			return true
+		}
+	}
+	return false
+}
diff --git a/iptables/iptables.go b/iptables/iptables.go
index 5523c485..bd262eb8 100644
--- a/iptables/iptables.go
+++ b/iptables/iptables.go
@@ -146,6 +146,19 @@ func ProgramChain(c *ChainInfo, bridgeName string, hairpinMode, enable bool) err
 		return errors.New("Could not program chain, missing chain name")
 	}
 
+	// Either add or remove the interface from the firewalld zone
+	if firewalldRunning {
+		if enable {
+			if err := AddInterfaceFirewalld(bridgeName); err != nil {
+				return err
+			}
+		} else {
+			if err := DelInterfaceFirewalld(bridgeName); err != nil {
+				return err
+			}
+		}
+	}
+
 	switch c.Table {
 	case Nat:
 		preroute := []string{
-- 
2.29.2

