Files
moby/libnetwork/iptables/firewalld.go
Matthieu MOREL bc9ec5fc02 fix emptyStringTest from go-critic
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2025-06-07 09:57:59 +02:00

376 lines
11 KiB
Go

//go:build linux
package iptables
import (
"context"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/containerd/log"
"github.com/docker/docker/pkg/rootless"
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
firewalldInitCalled bool
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. The error return is non-nil if the status cannot be
// determined because the initialisation function has not been called.
func UsingFirewalld() (bool, error) {
// If called before startup has completed, the firewall backend is unknown.
// But, if running rootless, the init function is not called because
// firewalld will be running in the host's netns, not in rootlesskit's.
if !firewalldInitCalled && !rootless.RunningWithRootlessKit() {
return false, fmt.Errorf("iptables.firewalld is not initialised")
}
return firewalldRunning, nil
}
// 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
firewalldInitCalled = true
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 []interface{}) {
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 [][]interface{}
icmpBlocks []string
masquerade bool
forwardPorts [][]interface{}
interfaces []string
sourceAddresses []string
richRules []string
protocols []string
sourcePorts [][]interface{}
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() []interface{} {
return []interface{}{
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]interface{}{
"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
}