Files
moby/daemon/libnetwork/iptables/firewalld.go
Paweł Gronowski 62ed24a87c modernize: Use slices.Contains
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-12-15 18:56:34 +01:00

370 lines
11 KiB
Go

//go:build linux
package iptables
import (
"context"
"fmt"
"os"
"slices"
"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()) {
if slices.Contains(onReloaded, &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 {
return slices.Contains(list, val)
}