mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-21 01:36:46 +00:00
261 lines
7.3 KiB
Go
261 lines
7.3 KiB
Go
//go:build linux
|
|
|
|
package firewalld
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
dbusDest = "org.fedoraproject.FirewallD1"
|
|
dbusPath = "/org/fedoraproject/FirewallD1"
|
|
dbusRootIface = "org.fedoraproject.FirewallD1"
|
|
dbusZoneIface = "org.fedoraproject.FirewallD1.zone"
|
|
|
|
errZoneAlreadySet = "ZONE_ALREADY_SET"
|
|
errAlreadyEnabled = "ALREADY_ENABLED"
|
|
errUnknownIface = "UNKNOWN_INTERFACE"
|
|
errNotEnabled = "NOT_ENABLED"
|
|
|
|
// callTimeout bounds each individual DBus or firewall-cmd invocation.
|
|
// A fresh context is created for each call so a slow DBus probe can't
|
|
// exhaust the deadline before the firewall-cmd fallback gets to run.
|
|
callTimeout = 3 * time.Second
|
|
)
|
|
|
|
var (
|
|
errDBusUnavailable = errors.New("firewalld dbus unavailable")
|
|
|
|
// trustLogOnce ensures the "added to trusted zone" message is logged at
|
|
// Info level only for the first successful add per process; repeat adds
|
|
// from other init paths are quieter.
|
|
trustLogOnce sync.Once
|
|
|
|
parentCtxMu sync.RWMutex
|
|
parentCtx context.Context = context.Background()
|
|
)
|
|
|
|
// SetParentContext installs a parent context whose cancellation aborts any
|
|
// in-flight TrustInterface call. It does not affect UntrustInterface, which
|
|
// always uses a fresh Background-rooted timeout so cleanup can still run
|
|
// during engine shutdown when the engine context is already cancelled.
|
|
func SetParentContext(ctx context.Context) {
|
|
parentCtxMu.Lock()
|
|
parentCtx = ctx
|
|
parentCtxMu.Unlock()
|
|
}
|
|
|
|
func getParentContext() context.Context {
|
|
parentCtxMu.RLock()
|
|
defer parentCtxMu.RUnlock()
|
|
return parentCtx
|
|
}
|
|
|
|
// TrustInterface places iface into firewalld's trusted zone if firewalld is
|
|
// running. It is idempotent and best-effort: errors are returned so callers
|
|
// can log, but a non-running firewalld is not an error. Only the first
|
|
// successful call per process logs at Info. Respects the parent context set
|
|
// via SetParentContext so startup-time cancellation unblocks it.
|
|
func TrustInterface(iface string) error {
|
|
parent := getParentContext()
|
|
if !isRunning(parent) {
|
|
return nil
|
|
}
|
|
if err := addTrusted(parent, iface); err != nil {
|
|
return fmt.Errorf("add %s to firewalld trusted zone: %w", iface, err)
|
|
}
|
|
trustLogOnce.Do(func() {
|
|
log.Infof("added %s to firewalld trusted zone", iface)
|
|
})
|
|
log.Debugf("firewalld: ensured %s is in trusted zone", iface)
|
|
return nil
|
|
}
|
|
|
|
// UntrustInterface removes iface from firewalld's trusted zone if firewalld
|
|
// is running. Idempotent. Uses a Background-rooted timeout so it still runs
|
|
// during shutdown after the engine context has been cancelled.
|
|
func UntrustInterface(iface string) error {
|
|
if !isRunning(context.Background()) {
|
|
return nil
|
|
}
|
|
if err := removeTrusted(context.Background(), iface); err != nil {
|
|
return fmt.Errorf("remove %s from firewalld trusted zone: %w", iface, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newCallContext(parent context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(parent, callTimeout)
|
|
}
|
|
|
|
func isRunning(parent context.Context) bool {
|
|
ctx, cancel := newCallContext(parent)
|
|
ok, err := isRunningDBus(ctx)
|
|
cancel()
|
|
if err == nil {
|
|
return ok
|
|
}
|
|
if errors.Is(err, errDBusUnavailable) || errors.Is(err, context.DeadlineExceeded) {
|
|
ctx, cancel = newCallContext(parent)
|
|
defer cancel()
|
|
return isRunningCLI(ctx)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func addTrusted(parent context.Context, iface string) error {
|
|
ctx, cancel := newCallContext(parent)
|
|
err := addDBus(ctx, iface)
|
|
cancel()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, errDBusUnavailable) {
|
|
log.Debugf("firewalld: dbus add failed, falling back to firewall-cmd: %v", err)
|
|
}
|
|
ctx, cancel = newCallContext(parent)
|
|
defer cancel()
|
|
return addCLI(ctx, iface)
|
|
}
|
|
|
|
func removeTrusted(parent context.Context, iface string) error {
|
|
ctx, cancel := newCallContext(parent)
|
|
err := removeDBus(ctx, iface)
|
|
cancel()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, errDBusUnavailable) {
|
|
log.Debugf("firewalld: dbus remove failed, falling back to firewall-cmd: %v", err)
|
|
}
|
|
ctx, cancel = newCallContext(parent)
|
|
defer cancel()
|
|
return removeCLI(ctx, iface)
|
|
}
|
|
|
|
func isRunningDBus(ctx context.Context) (bool, error) {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return false, fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
|
}
|
|
obj := conn.Object(dbusDest, dbusPath)
|
|
|
|
var zone string
|
|
if err := obj.CallWithContext(ctx, dbusRootIface+".getDefaultZone", 0).Store(&zone); err != nil {
|
|
return false, fmt.Errorf("firewalld getDefaultZone: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func isRunningCLI(ctx context.Context) bool {
|
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
|
return false
|
|
}
|
|
return exec.CommandContext(ctx, "firewall-cmd", "--state").Run() == nil
|
|
}
|
|
|
|
func addDBus(ctx context.Context, iface string) error {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
|
}
|
|
obj := conn.Object(dbusDest, dbusPath)
|
|
|
|
call := obj.CallWithContext(ctx, dbusZoneIface+".addInterface", 0, TrustedZone, iface)
|
|
if call.Err == nil {
|
|
return nil
|
|
}
|
|
|
|
if dbusErrContains(call.Err, errAlreadyEnabled) {
|
|
return nil
|
|
}
|
|
|
|
if dbusErrContains(call.Err, errZoneAlreadySet) {
|
|
move := obj.CallWithContext(ctx, dbusZoneIface+".changeZoneOfInterface", 0, TrustedZone, iface)
|
|
if move.Err != nil {
|
|
return fmt.Errorf("firewalld changeZoneOfInterface: %w", move.Err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("firewalld addInterface: %w", call.Err)
|
|
}
|
|
|
|
func removeDBus(ctx context.Context, iface string) error {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
|
}
|
|
obj := conn.Object(dbusDest, dbusPath)
|
|
|
|
call := obj.CallWithContext(ctx, dbusZoneIface+".removeInterface", 0, TrustedZone, iface)
|
|
if call.Err == nil {
|
|
return nil
|
|
}
|
|
|
|
if dbusErrContains(call.Err, errUnknownIface) || dbusErrContains(call.Err, errNotEnabled) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("firewalld removeInterface: %w", call.Err)
|
|
}
|
|
|
|
func addCLI(ctx context.Context, iface string) error {
|
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
|
return fmt.Errorf("firewall-cmd not available: %w", err)
|
|
}
|
|
|
|
// --change-interface (no --permanent) binds the interface for the
|
|
// current runtime only; we do not want membership to persist across
|
|
// reboots because netbird re-asserts it on every startup.
|
|
out, err := exec.CommandContext(ctx,
|
|
"firewall-cmd", "--zone="+TrustedZone, "--change-interface="+iface,
|
|
).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("firewall-cmd change-interface: %w: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeCLI(ctx context.Context, iface string) error {
|
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
|
return fmt.Errorf("firewall-cmd not available: %w", err)
|
|
}
|
|
|
|
out, err := exec.CommandContext(ctx,
|
|
"firewall-cmd", "--zone="+TrustedZone, "--remove-interface="+iface,
|
|
).CombinedOutput()
|
|
if err != nil {
|
|
msg := strings.TrimSpace(string(out))
|
|
if strings.Contains(msg, errUnknownIface) || strings.Contains(msg, errNotEnabled) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("firewall-cmd remove-interface: %w: %s", err, msg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dbusErrContains(err error, code string) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
var de dbus.Error
|
|
if errors.As(err, &de) {
|
|
for _, b := range de.Body {
|
|
if s, ok := b.(string); ok && strings.Contains(s, code) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return strings.Contains(err.Error(), code)
|
|
}
|