//go:build linux package iptables import ( "context" "fmt" "os" "strings" "sync" "sync/atomic" "time" "github.com/containerd/log" dbus "github.com/godbus/dbus/v5" "github.com/pkg/errors" ) const ( dbusInterface = "org.fedoraproject.FirewallD1" dbusPath = "/org/fedoraproject/FirewallD1" dbusConfigPath = "/org/fedoraproject/FirewallD1/config" dockerZone = "docker" dockerFwdPolicy = "docker-forwarding" ) // Conn is a connection to firewalld dbus endpoint. type Conn struct { sysconn *dbus.Conn sysObj dbus.BusObject sysConfObj dbus.BusObject signal chan *dbus.Signal } var ( connection *Conn firewalldRunning bool // is Firewalld service running // Time of the last firewalld reload. firewalldReloadedAt atomic.Value // Mutex to serialise firewalld reload callbacks. firewalldReloadMu sync.Mutex onReloaded []*func() // callbacks when Firewalld has been reloaded ) // UsingFirewalld returns true if iptables rules will be applied via firewalld's // passthrough interface. func UsingFirewalld() bool { _ = initCheck() return firewalldRunning } // FirewalldReloadedAt returns the time at which the daemon last completed a // firewalld reload, or a zero-valued time.Time if it has not been reloaded // since the daemon started. func FirewalldReloadedAt() time.Time { val := firewalldReloadedAt.Load() if val == nil { return time.Time{} } return val.(time.Time) } // firewalldInit initializes firewalld management code. func firewalldInit() error { var err error // DOCKER_TEST_NO_FIREWALLD is used by integration tests to disable firewalld integration to make sure that the // daemon started by the 'test-integration' script won't recreate iptables / nftables rules upon receiving the // firewalld reload signal, otherwise it'll race against the daemon-under-test started by networking integration // tests. This is an internal implementation detail and users shall never rely on this. if disable := os.Getenv("DOCKER_TEST_NO_FIREWALLD"); disable != "" { return nil } if connection, err = newConnection(); err != nil { return fmt.Errorf("Failed to connect to D-Bus system bus: %v", err) } firewalldRunning = checkRunning() if !firewalldRunning { connection.sysconn.Close() connection = nil } if connection != nil { go signalHandler() zoneAdded, err := setupDockerZone() if err != nil { return err } policyAdded, policyAddErr := setupDockerForwardingPolicy() if policyAddErr != nil { // Log the error, but still reload firewalld if necessary. log.G(context.TODO()).WithError(policyAddErr).Warnf("Firewalld: failed to add policy %s", dockerFwdPolicy) } if zoneAdded || policyAdded { // Reload for changes to take effect. if err := connection.sysObj.Call(dbusInterface+".reload", 0).Err; err != nil { return err } } } return nil } // newConnection establishes a connection to the system bus. func newConnection() (*Conn, error) { c := &Conn{} var err error c.sysconn, err = dbus.SystemBus() if err != nil { return nil, err } // This never fails, even if the service is not running atm. c.sysObj = c.sysconn.Object(dbusInterface, dbusPath) c.sysConfObj = c.sysconn.Object(dbusInterface, 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) 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) return c, nil } func signalHandler() { for signal := range connection.signal { switch { case strings.Contains(signal.Name, "NameOwnerChanged"): firewalldRunning = checkRunning() dbusConnectionChanged(signal.Body) case strings.Contains(signal.Name, "Reloaded"): reloaded() } } } func dbusConnectionChanged(args []any) { name := args[0].(string) oldOwner := args[1].(string) newOwner := args[2].(string) if name != dbusInterface { return } if newOwner != "" { connectionEstablished() } else if oldOwner != "" { connectionLost() } } func connectionEstablished() { reloaded() } func connectionLost() { // Doesn't do anything for now. Libvirt also doesn't react to this. } // call all callbacks func reloaded() { firewalldReloadMu.Lock() defer firewalldReloadMu.Unlock() for _, pf := range onReloaded { (*pf)() } firewalldReloadedAt.Store(time.Now()) } // OnReloaded 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 { if connection == nil { return false } var zone string err := connection.sysObj.Call(dbusInterface+".getDefaultZone", 0).Store(&zone) return err == nil } // passthrough method simply passes args through to iptables/ip6tables func passthrough(ipv IPVersion, args ...string) ([]byte, error) { var output string log.G(context.TODO()).Debugf("Firewalld passthrough: %s, %s", ipv, args) if err := connection.sysObj.Call(dbusInterface+".direct.passthrough", 0, ipv, args).Store(&output); err != nil { return nil, err } return []byte(output), nil } // firewalldZone holds the firewalld zone settings. // // Documented in https://firewalld.org/documentation/man-pages/firewalld.dbus.html#FirewallD1.zone type firewalldZone struct { version string name string description string unused bool target string services []string ports [][]any icmpBlocks []string masquerade bool forwardPorts [][]any interfaces []string sourceAddresses []string richRules []string protocols []string sourcePorts [][]any icmpBlockInversion bool } // settings returns the firewalldZone struct as an interface slice, which can be // passed to "org.fedoraproject.FirewallD1.config.addZone". Note that 'addZone', // which is deprecated, requires this whole struct. Its replacement, 'addZone2' // (introduced in firewalld 0.9.0) accepts a dictionary where only non-default // values need to be specified. func (z firewalldZone) settings() []any { return []any{ z.version, z.name, z.description, z.unused, z.target, z.services, z.ports, z.icmpBlocks, z.masquerade, z.forwardPorts, z.interfaces, z.sourceAddresses, z.richRules, z.protocols, z.sourcePorts, z.icmpBlockInversion, } } // setupDockerZone creates a zone called docker in firewalld which includes docker interfaces to allow // container networking. The bool return value is true if a firewalld reload is required. func setupDockerZone() (bool, error) { var zones []string // Check if zone exists if err := connection.sysObj.Call(dbusInterface+".zone.getZones", 0).Store(&zones); err != nil { return false, err } if contains(zones, dockerZone) { log.G(context.TODO()).Infof("Firewalld: %s zone already exists, returning", dockerZone) return false, nil } log.G(context.TODO()).Debugf("Firewalld: creating %s zone", dockerZone) // Permanent dz := firewalldZone{ version: "1.0", name: dockerZone, description: "zone for docker bridge network interfaces", target: "ACCEPT", } if err := connection.sysConfObj.Call(dbusInterface+".config.addZone", 0, dockerZone, dz.settings()).Err; err != nil { return false, err } return true, nil } // setupDockerForwardingPolicy creates a policy to allow forwarding to anywhere to the docker // zone (where packets will be dealt with by docker's usual/non-firewalld configuration). // The bool return value is true if a firewalld reload is required. func setupDockerForwardingPolicy() (bool, error) { // https://firewalld.org/documentation/man-pages/firewalld.dbus.html#FirewallD1.config policy := map[string]any{ "version": "1.0", "description": "allow forwarding to the docker zone", "ingress_zones": []string{"ANY"}, "egress_zones": []string{dockerZone}, "target": "ACCEPT", } if err := connection.sysConfObj.Call(dbusInterface+".config.addPolicy", 0, dockerFwdPolicy, policy).Err; err != nil { var derr dbus.Error if errors.As(err, &derr) { if derr.Name == dbusInterface+".Exception" && strings.HasPrefix(err.Error(), "NAME_CONFLICT") { log.G(context.TODO()).Debugf("Firewalld: %s policy already exists", dockerFwdPolicy) return false, nil } if derr.Name == dbus.ErrMsgUnknownMethod.Name { log.G(context.TODO()).Debugf("Firewalld: addPolicy %s: unknown method", dockerFwdPolicy) return false, nil } } return false, err } log.G(context.TODO()).Infof("Firewalld: created %s policy", dockerFwdPolicy) return true, nil } // AddInterfaceFirewalld adds the interface to the trusted zone. It is a // no-op if firewalld is not running. func AddInterfaceFirewalld(intf string) error { if !firewalldRunning { return nil } 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) { log.G(context.TODO()).Infof("Firewalld: interface %s already part of %s zone, returning", intf, dockerZone) return nil } log.G(context.TODO()).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 It is a // no-op if firewalld is not running. func DelInterfaceFirewalld(intf string) error { if !firewalldRunning { return nil } 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 &interfaceNotFound{fmt.Errorf("firewalld: interface %q not found in %s zone", intf, dockerZone)} } log.G(context.TODO()).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 } type interfaceNotFound struct{ error } func (interfaceNotFound) NotFound() {} func contains(list []string, val string) bool { for _, v := range list { if v == val { return true } } return false }