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

112 lines
3.1 KiB
Go

package firewalld
import (
"context"
"strings"
"github.com/godbus/dbus/v5"
)
// Signal is a firewalld D-Bus signal delivered to a watcher. Member is the short
// signal name (e.g. "Reloaded", "PortAdded"); Interface is the emitting interface;
// Body carries the signal's arguments in firewalld's declared order.
type Signal struct {
Member string
Interface string
Path dbus.ObjectPath
Body []any
}
// signalInterfaces are the firewalld interfaces whose signals a watcher subscribes
// to. Matching by interface keeps unrelated bus traffic out of the channel.
var signalInterfaces = []string{
ifaceMain, ifaceZone, ifaceIPSet, ifaceDirect, ifacePolicy, ifaceLockdown,
ifaceConfig, ifaceConfigZone, ifaceConfigIPSet, ifaceConfigPolicy,
ifaceConfigService, ifaceConfigICMP, ifaceConfigHelper, ifaceConfigDirect,
}
// WatchSignals subscribes to firewalld's signals and returns a channel of typed
// events. The subscription and channel are torn down when ctx is cancelled. The
// channel is buffered; a slow consumer that lets it fill will drop no earlier
// events but may block delivery of later ones until drained.
func (c *Conn) WatchSignals(ctx context.Context) (<-chan Signal, error) {
for _, iface := range signalInterfaces {
if err := c.conn.AddMatchSignal(dbus.WithMatchInterface(iface)); err != nil {
return nil, err
}
}
raw := make(chan *dbus.Signal, 64)
c.conn.Signal(raw)
out := make(chan Signal, 64)
go func() {
defer close(out)
defer c.removeSignalMatches(raw)
for {
select {
case <-ctx.Done():
return
case sig, ok := <-raw:
if !ok {
return
}
iface, member := splitSignalName(sig.Name)
// Ignore anything outside firewalld's namespace.
if !strings.HasPrefix(iface, ifaceMain) {
continue
}
ev := Signal{Member: member, Interface: iface, Path: sig.Path, Body: sig.Body}
select {
case out <- ev:
case <-ctx.Done():
return
}
}
}
}()
return out, nil
}
// WatchReloaded returns a channel that receives an empty struct each time
// firewalld emits its Reloaded signal, a convenience for callers that only need to
// re-read state after a reload.
func (c *Conn) WatchReloaded(ctx context.Context) (<-chan struct{}, error) {
signals, err := c.WatchSignals(ctx)
if err != nil {
return nil, err
}
out := make(chan struct{}, 4)
go func() {
defer close(out)
for sig := range signals {
if sig.Member == "Reloaded" {
select {
case out <- struct{}{}:
case <-ctx.Done():
return
}
}
}
}()
return out, nil
}
// removeSignalMatches unregisters the channel and drops the match rules added by
// WatchSignals. Best-effort: match removal errors are ignored during teardown.
func (c *Conn) removeSignalMatches(raw chan *dbus.Signal) {
c.conn.RemoveSignal(raw)
for _, iface := range signalInterfaces {
_ = c.conn.RemoveMatchSignal(dbus.WithMatchInterface(iface))
}
}
// splitSignalName splits a fully-qualified signal name "iface.Member" into its
// interface and member parts.
func splitSignalName(full string) (iface, member string) {
i := strings.LastIndex(full, ".")
if i < 0 {
return "", full
}
return full[:i], full[i+1:]
}