go-firewalld/firewalld.go
2026-07-02 14:08:37 -05:00

215 lines
7.9 KiB
Go

// Package firewalld is a comprehensive Go client for the firewalld D-Bus API.
//
// It exposes both runtime (transient) and permanent configuration operations,
// mirroring firewall-cmd's default and --permanent modes, and adapts to the
// connected server's version so a single binary serves both old (EL7 0.6.x) and
// new (EL9 1.x) firewalld. Compound values are encoded as concrete Go structs so
// godbus emits the D-Bus tuples firewalld requires (see CLAUDE.md).
package firewalld
import (
"context"
"github.com/godbus/dbus/v5"
)
// D-Bus names for the firewalld service and its well-known object paths.
const (
dest = "org.fedoraproject.FirewallD1"
basePath = dbus.ObjectPath("/org/fedoraproject/FirewallD1")
configPath = dbus.ObjectPath("/org/fedoraproject/FirewallD1/config")
// Interface names.
ifaceMain = "org.fedoraproject.FirewallD1"
ifaceZone = "org.fedoraproject.FirewallD1.zone"
ifaceIPSet = "org.fedoraproject.FirewallD1.ipset"
ifaceDirect = "org.fedoraproject.FirewallD1.direct"
ifacePolicy = "org.fedoraproject.FirewallD1.policy"
ifaceLockdown = "org.fedoraproject.FirewallD1.policies"
ifaceConfig = "org.fedoraproject.FirewallD1.config"
ifaceConfigZone = "org.fedoraproject.FirewallD1.config.zone"
ifaceConfigIPSet = "org.fedoraproject.FirewallD1.config.ipset"
ifaceConfigPolicy = "org.fedoraproject.FirewallD1.config.policy"
ifaceConfigService = "org.fedoraproject.FirewallD1.config.service"
ifaceConfigICMP = "org.fedoraproject.FirewallD1.config.icmptype"
ifaceConfigHelper = "org.fedoraproject.FirewallD1.config.helper"
ifaceConfigDirect = "org.fedoraproject.FirewallD1.config.direct"
ifaceProperties = "org.freedesktop.DBus.Properties"
)
// Conn is a connection to the firewalld service on the system bus. It caches the
// server version and derived capabilities so callers can branch on Supports and
// internal code can pick the tuple or dict transport. A Conn is safe for
// concurrent use to the extent godbus's connection is.
type Conn struct {
conn *dbus.Conn
version Version
caps map[Capability]bool
// ownConn records whether we dialed the bus (and must close it) or adopted a
// caller-provided connection (and must leave it open).
ownConn bool
}
// Connect dials the system bus, reads the firewalld version property, and derives
// the capability set. The context bounds the connection and initial version read.
func Connect(ctx context.Context) (*Conn, error) {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, err
}
c, err := newConn(ctx, conn, true)
if err != nil {
conn.Close()
return nil, err
}
return c, nil
}
// Open builds a Conn on a caller-supplied bus connection. The caller retains
// ownership: Close will not tear the bus down. Useful for sharing a bus or for
// tests against a private connection.
func Open(ctx context.Context, bus *dbus.Conn) (*Conn, error) {
return newConn(ctx, bus, false)
}
// newConn wires up the object handles and probes the version/caps.
func newConn(ctx context.Context, bus *dbus.Conn, own bool) (*Conn, error) {
c := &Conn{
conn: bus,
ownConn: own,
}
v, err := c.readVersion(ctx)
if err != nil {
return nil, err
}
c.version = v
c.caps = capsFor(v)
return c, nil
}
// readVersion reads the "version" property on the main interface via a
// context-bounded Properties.Get call.
func (c *Conn) readVersion(ctx context.Context) (Version, error) {
var variant dbus.Variant
err := c.call(ctx, basePath, ifaceProperties, "Get", []any{&variant}, ifaceMain, "version")
if err != nil {
return Version{}, err
}
s, _ := variant.Value().(string)
return parseVersion(s), nil
}
// Version returns the firewalld version reported by the server.
func (c *Conn) Version() Version { return c.version }
// Supports reports whether the connected server provides a capability.
func (c *Conn) Supports(cap Capability) bool { return c.caps[cap] }
// Close releases the connection. If the bus was dialed by Connect it is closed;
// a bus adopted via Open is left open for its owner.
func (c *Conn) Close() error {
if c.ownConn && c.conn != nil {
return c.conn.Close()
}
return nil
}
// call invokes a method on an arbitrary object path/interface and decodes the
// return values into rets. It is the single choke point through which every typed
// method flows, so error mapping happens in exactly one place.
func (c *Conn) call(ctx context.Context, path dbus.ObjectPath, iface, method string, rets []any, args ...any) error {
obj := c.conn.Object(dest, path)
call := obj.CallWithContext(ctx, iface+"."+method, 0, args...)
if call.Err != nil {
return mapError(call.Err)
}
if len(rets) == 0 {
return nil
}
return mapError(call.Store(rets...))
}
// Call is the low-level escape hatch for any method not yet wrapped by a typed
// helper. It targets the given object path and interface, passes args verbatim,
// and stores results into rets (pointers). Errors are mapped to sentinels.
func (c *Conn) Call(ctx context.Context, path dbus.ObjectPath, iface, method string, rets []any, args ...any) error {
return c.call(ctx, path, iface, method, rets, args...)
}
// Reload reloads firewalld's permanent configuration into the runtime, keeping
// active bindings where possible.
func (c *Conn) Reload(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "reload", nil)
}
// CompleteReload reloads and also re-reads interface-to-zone bindings, dropping
// active runtime state. Heavier than Reload; use when the permanent set changed
// structurally.
func (c *Conn) CompleteReload(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "completeReload", nil)
}
// RuntimeToPermanent persists the current runtime configuration as permanent.
func (c *Conn) RuntimeToPermanent(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "runtimeToPermanent", nil)
}
// ResetToDefaults resets the permanent configuration to firewalld's defaults.
// Requires firewalld >= 1.0; returns ErrUnsupported on older servers.
func (c *Conn) ResetToDefaults(ctx context.Context) error {
if !c.caps[ResetToDefaults] {
return ErrUnsupported
}
return c.call(ctx, basePath, ifaceMain, "resetToDefaults", nil)
}
// CheckPermanentConfig validates the on-disk permanent configuration, returning a
// firewalld error if it is inconsistent.
func (c *Conn) CheckPermanentConfig(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "checkPermanentConfig", nil)
}
// Panic-mode controls. Panic mode drops all traffic; callers on a remote bus must
// take care not to lock themselves out.
// EnablePanicMode drops all inbound and outbound packets.
func (c *Conn) EnablePanicMode(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "enablePanicMode", nil)
}
// DisablePanicMode restores normal packet processing.
func (c *Conn) DisablePanicMode(ctx context.Context) error {
return c.call(ctx, basePath, ifaceMain, "disablePanicMode", nil)
}
// QueryPanicMode reports whether panic mode is active.
func (c *Conn) QueryPanicMode(ctx context.Context) (bool, error) {
var on bool
err := c.call(ctx, basePath, ifaceMain, "queryPanicMode", []any{&on})
return on, err
}
// LogDenied returns the current LogDenied setting ("off", "all", "unicast", ...).
func (c *Conn) LogDenied(ctx context.Context) (string, error) {
var s string
err := c.call(ctx, basePath, ifaceMain, "getLogDenied", []any{&s})
return s, err
}
// SetLogDenied sets the LogDenied value.
func (c *Conn) SetLogDenied(ctx context.Context, value string) error {
return c.call(ctx, basePath, ifaceMain, "setLogDenied", nil, value)
}
// DefaultZone returns the name of the default zone.
func (c *Conn) DefaultZone(ctx context.Context) (string, error) {
var s string
err := c.call(ctx, basePath, ifaceMain, "getDefaultZone", []any{&s})
return s, err
}
// SetDefaultZone sets the default zone by name.
func (c *Conn) SetDefaultZone(ctx context.Context, zone string) error {
return c.call(ctx, basePath, ifaceMain, "setDefaultZone", nil, zone)
}