Skip to content

[client] Trust wg interface in firewalld to bypass owner-flagged chains#5928

Merged
lixmal merged 1 commit intomainfrom
client/firewalld-trusted-zone
Apr 21, 2026
Merged

[client] Trust wg interface in firewalld to bypass owner-flagged chains#5928
lixmal merged 1 commit intomainfrom
client/firewalld-trusted-zone

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Apr 20, 2026

Describe your changes

When firewalld is running on recent versions it creates its nftables chains with the NFT_CHAIN_OWNER flag, which makes any attempt by another process to insert rules into them return EPERM. NetBird currently tries to insert accept rules into firewalld-owned filter_FORWARD/filter_INPUT chains and logs a failure. This change delegates acceptance to firewalld itself by adding the wg interface to firewalld's trusted zone, matching what Tailscale does.

  • Add a small client/firewall/firewalld package that adds/removes an interface from the trusted zone via DBus, with a firewall-cmd fallback
  • Call it from the nftables router init/Reset, the iptables manager Init/Close, and both managers' AllowNetbird so userspace-firewall mode is covered too
  • Skip chains in any table named firewalld from the "add accept rules to external chains" walk so we stop hitting EPERM
  • All calls are bounded by a 3s context and errors are logged as warnings: firewalld integration is best-effort and never blocks firewall setup
  • Cleanup of the trusted-zone membership runs on normal shutdown and via the existing ShutdownState crash-recovery path

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Internal compatibility shim, no user-facing configuration or behavior change beyond the interface being placed in firewalld's trusted zone while NetBird is running.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Linux: firewalld integration trusts the WireGuard interface during startup/allow flows and untrusts on shutdown/reset; failures are logged as warnings and do not block operation.
    • Non-Linux platforms use safe no-op fallbacks.
  • Tests

    • Added unit tests validating firewalld-related error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a new firewalld package (DBus + firewall-cmd fallback) and integrates it across iptables, nftables, uspfilter, and router code to add/remove the WireGuard interface from firewalld's "trusted" zone and to exclude firewalld-owned nftables tables from external-chain logic.

Changes

Cohort / File(s) Summary
Firewalld package
client/firewall/firewalld/firewalld.go, client/firewall/firewalld/firewalld_linux.go, client/firewall/firewalld/firewalld_other.go
New package: exported TrustedZone constant; Linux implementation: TrustInterface/UntrustInterface using DBus (3s timeout) with firewall-cmd fallback and idempotent/error-mapping behavior; non-Linux stubs return nil.
Firewalld tests
client/firewall/firewalld/firewalld_linux_test.go
Adds TestDBusErrContains to validate DBus error-body substring detection and fallback matching.
Iptables manager
client/firewall/iptables/manager_linux.go
Calls firewalld.TrustInterface in Init/AllowNetbird (warn on failure) and UntrustInterface in Close (append errors to multierror).
Nftables manager
client/firewall/nftables/manager_linux.go
Calls firewalld.TrustInterface after flushing allow rules in AllowNetbird; failures logged but not returned.
Nftables router
client/firewall/nftables/router_linux.go
Calls firewalld.TrustInterface in container creation and UntrustInterface in Reset (errors aggregated); adds firewalld table exclusion so firewalld-owned tables are not treated as external chains.
Uspfilter & iface changes
client/firewall/uspfilter/allow_netbird.go, client/firewall/uspfilter/common/iface.go, client/firewall/uspfilter/filter_test.go
Adds IFaceMapper.Name() string and mock Name(); uspfilter AllowNetbird/Close call firewalld.TrustInterface/UntrustInterface and log warnings on failure.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Mgr as Firewall Manager\n(iptables / nftables / uspfilter)
    participant FWD as firewalld Package
    participant DBus as DBus\n(org.fedoraproject.FirewallD1)
    participant CMD as firewall-cmd\n(CLI)

    App->>Mgr: Init() / AllowNetbird()
    Mgr->>FWD: TrustInterface(iface)
    FWD->>FWD: create 3s timeout context
    FWD->>DBus: probe getDefaultZone()
    alt DBus available
        DBus-->>FWD: zone info
        FWD->>DBus: addInterface(iface)
        alt Success or idempotent error
            DBus-->>FWD: OK / handled
        else Assigned to other zone
            FWD->>DBus: changeZoneOfInterface(iface, trusted)
            DBus-->>FWD: OK
        end
    else DBus unavailable
        FWD->>CMD: firewall-cmd --zone=trusted --change-interface=<iface>
        CMD-->>FWD: stdout/exit
        FWD->>FWD: parse output for known non-errors
    end
    FWD-->>Mgr: success or warning (logged)

    App->>Mgr: Close() / Reset()
    Mgr->>FWD: UntrustInterface(iface)
    alt DBus available
        FWD->>DBus: removeInterface(iface)
        DBus-->>FWD: OK or idempotent
    else DBus unavailable
        FWD->>CMD: firewall-cmd --zone=trusted --remove-interface=<iface>
        CMD-->>FWD: stdout/exit
    end
    FWD-->>Mgr: success or aggregated error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • mlsmaycon

Poem

🐰 I hopped to mark the iface "trusted" with delight,
DBus asked first, then CLI saved the night,
I skip the chains that firewalld owns,
Tunnels snug in trusted zones,
A little rabbit cheers the netbird's flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: trusting the WireGuard interface in firewalld to bypass owner-flagged chains.
Description check ✅ Passed The PR description is comprehensive and addresses all critical template sections: detailed explanation of changes, bug fix classification, and documentation justification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client/firewalld-trusted-zone

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
client/firewall/iptables/manager_linux.go (1)

183-212: Close appending untrust error to merr intentionally blocks DeleteState.

This is a subtle but correct behavior: if firewalld untrust fails, the ShutdownState is preserved so the crash-recovery path can retry cleanup on next startup. Worth a short code comment here since the pattern differs from Init/AllowNetbird which only warn.

💡 Optional clarifying comment
 	if err := m.untrustInFirewalld(); err != nil {
+		// Append to merr so ShutdownState is preserved for the next startup's
+		// crash-recovery path to retry firewalld cleanup.
 		merr = multierror.Append(merr, err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/iptables/manager_linux.go` around lines 183 - 212, The Close
method intentionally appends the error from m.untrustInFirewalld() to merr so
that DeleteState on stateManager is skipped when untrust fails (preserving
ShutdownState for crash-recovery); add a short clarifying comment in
Manager.Close near the m.untrustInFirewalld() append (and/or above the "attempt
to delete state only if all other operations succeeded" block) referencing
m.untrustInFirewalld, the merr variable, and stateManager.DeleteState to explain
this deliberate difference from Init/AllowNetbird which only warn.
client/firewall/firewalld/firewalld_linux.go (1)

30-67: Consider consolidating the per-caller boilerplate into the package.

Every caller (iptables.Manager, nftables.Manager, nftables.router) now wraps IsRunning + Add/RemoveTrustedInterface with the same 3s-timeout context and identical logging pattern — three copies of trustInFirewalld and two of untrustInFirewalld. Exposing higher-level helpers from this package would remove the duplication and keep the firewalldCallTimeout/IsRunning policy in one place.

♻️ Suggested API
// in client/firewall/firewalld/firewalld.go (or a new helpers file)
const CallTimeout = 3 * time.Second

// Trust adds iface to the trusted zone if firewalld is running. No-op otherwise.
// Uses an internal bounded context so callers don't have to manage timeouts.
func Trust(iface string) error {
    ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
    defer cancel()
    if !IsRunning(ctx) {
        return nil
    }
    if err := AddTrustedInterface(ctx, iface); err != nil {
        return fmt.Errorf("add %s to firewalld trusted zone: %w", iface, err)
    }
    log.Infof("added %s to firewalld trusted zone", iface)
    return nil
}

// Untrust is the inverse of Trust.
func Untrust(iface string) error { /* symmetric */ }

Callers collapse to a single line, e.g.:

-if err := m.trustInFirewalld(); err != nil {
+if err := firewalld.Trust(m.wgIface.Name()); err != nil {
 	log.Warnf("failed to trust interface in firewalld: %v", err)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/firewalld/firewalld_linux.go` around lines 30 - 67, The
package has duplicated caller-side boilerplate that creates a 3s timeout
context, checks IsRunning, calls AddTrustedInterface/RemoveTrustedInterface and
logs; centralize this by adding package-level helpers (e.g., a CallTimeout
constant and two functions Trust(iface string) error and Untrust(iface string)
error) that create an internal context.WithTimeout(context.Background(),
CallTimeout), call IsRunning(ctx) and then
AddTrustedInterface/RemoveTrustedInterface, wrap and return errors with context,
and emit the shared log messages; update callers to invoke
firewalld.Trust/Untrust instead of repeating the timeout+IsRunning+call pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/firewall/firewalld/firewalld_linux.go`:
- Around line 30-67: The package has duplicated caller-side boilerplate that
creates a 3s timeout context, checks IsRunning, calls
AddTrustedInterface/RemoveTrustedInterface and logs; centralize this by adding
package-level helpers (e.g., a CallTimeout constant and two functions
Trust(iface string) error and Untrust(iface string) error) that create an
internal context.WithTimeout(context.Background(), CallTimeout), call
IsRunning(ctx) and then AddTrustedInterface/RemoveTrustedInterface, wrap and
return errors with context, and emit the shared log messages; update callers to
invoke firewalld.Trust/Untrust instead of repeating the timeout+IsRunning+call
pattern.

In `@client/firewall/iptables/manager_linux.go`:
- Around line 183-212: The Close method intentionally appends the error from
m.untrustInFirewalld() to merr so that DeleteState on stateManager is skipped
when untrust fails (preserving ShutdownState for crash-recovery); add a short
clarifying comment in Manager.Close near the m.untrustInFirewalld() append
(and/or above the "attempt to delete state only if all other operations
succeeded" block) referencing m.untrustInFirewalld, the merr variable, and
stateManager.DeleteState to explain this deliberate difference from
Init/AllowNetbird which only warn.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89fd9754-f845-4869-981b-4ecdfc9b6d64

📥 Commits

Reviewing files that changed from the base of the PR and between 3098f48 and c6dfd33.

📒 Files selected for processing (6)
  • client/firewall/firewalld/firewalld.go
  • client/firewall/firewalld/firewalld_linux.go
  • client/firewall/firewalld/firewalld_other.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
client/firewall/firewalld/firewalld_linux.go (2)

95-121: Minor: scope ret inside the ZONE_ALREADY_SET branch.

ret is only consumed by move.Store(&ret) in the migration path; declaring it at the top of addDBus is slightly misleading (implies addInterface result is being captured too). Declaring it next to move.Store makes the intent clearer and keeps the success/early-return paths cleaner. Purely cosmetic.

♻️ Suggested change
-	var ret string
 	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 {
+			var ret string
 			return move.Store(&ret)
 		}
 		return fmt.Errorf("firewalld changeZoneOfInterface: %w", move.Err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/firewalld/firewalld_linux.go` around lines 95 - 121, The
local variable ret is declared at the top of addDBus but only used when handling
the dbusZoneIface+".changeZoneOfInterface" branch; move the declaration of ret
into that branch immediately before calling move.Store(&ret) to limit its scope
and clarify intent (update references in the
dbusZoneIface+".changeZoneOfInterface" handling where move and move.Store are
used within addDBus).

142-154: Consider tolerating ALREADY_ENABLED output in addCLI for parity with removeCLI.

firewall-cmd --change-interface generally moves/binds the interface and returns 0 even when it's already bound to the target zone, so this is usually fine in practice. However, if some firewalld versions/states emit a non-zero exit with an ALREADY_ENABLED-style message, the DBus path tolerates it while the CLI fallback surfaces an error. For symmetry with removeCLI (which already filters UNKNOWN_INTERFACE/NOT_ENABLED), consider the same non-error mapping here.

♻️ Optional
 	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)))
+		msg := strings.TrimSpace(string(out))
+		if strings.Contains(msg, errAlreadyEnabled) {
+			return nil
+		}
+		return fmt.Errorf("firewall-cmd change-interface: %w: %s", err, msg)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/firewalld/firewalld_linux.go` around lines 142 - 154, The
addCLI function currently treats any non-zero exit from firewall-cmd as an
error; update addCLI to tolerate ALREADY_ENABLED output like removeCLI does by
checking the CombinedOutput string for "ALREADY_ENABLED" (or equivalent
casing/whitespace) after exec.CommandContext("firewall-cmd",
"--zone="+TrustedZone, "--change-interface="+iface). If the output contains
ALREADY_ENABLED, return nil instead of an error; otherwise keep the existing
fmt.Errorf behavior. Keep the existing exec.LookPath("firewall-cmd") check and
mirror the same string-filtering approach used in removeCLI for symmetry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/firewall/firewalld/firewalld_linux.go`:
- Around line 95-121: The local variable ret is declared at the top of addDBus
but only used when handling the dbusZoneIface+".changeZoneOfInterface" branch;
move the declaration of ret into that branch immediately before calling
move.Store(&ret) to limit its scope and clarify intent (update references in the
dbusZoneIface+".changeZoneOfInterface" handling where move and move.Store are
used within addDBus).
- Around line 142-154: The addCLI function currently treats any non-zero exit
from firewall-cmd as an error; update addCLI to tolerate ALREADY_ENABLED output
like removeCLI does by checking the CombinedOutput string for "ALREADY_ENABLED"
(or equivalent casing/whitespace) after exec.CommandContext("firewall-cmd",
"--zone="+TrustedZone, "--change-interface="+iface). If the output contains
ALREADY_ENABLED, return nil instead of an error; otherwise keep the existing
fmt.Errorf behavior. Keep the existing exec.LookPath("firewall-cmd") check and
mirror the same string-filtering approach used in removeCLI for symmetry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae21212d-eaac-41b7-aa4b-e04fa394cea8

📥 Commits

Reviewing files that changed from the base of the PR and between c6dfd33 and e95d703.

📒 Files selected for processing (1)
  • client/firewall/firewalld/firewalld_linux.go

@lixmal lixmal force-pushed the client/firewalld-trusted-zone branch from e95d703 to 45d682f Compare April 20, 2026 10:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/firewall/firewalld/firewalld_linux.go`:
- Around line 47-55: The timeout context currently created once in
TrustInterface (ctx from context.WithTimeout using callTimeout) is reused for
isRunning and subsequent DBus/CLI calls, which can leave addCLI/removeCLI
running with an expired context; change the code so that isRunning gets its own
short timeout and each DBus or CLI attempt (addTrusted, addCLI, removeCLI, and
their counterparts in the other affected blocks) creates its own context via
context.WithTimeout(callTimeout) and defers cancel immediately before the call
so every probe/mutation gets a fresh deadline; update TrustInterface and the
similar blocks (functions/methods addTrusted, addCLI, removeCLI and the code
paths referenced at lines 66-74 and 79-109) to follow this pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f741b3c6-725d-4d58-86f9-73753b12a803

📥 Commits

Reviewing files that changed from the base of the PR and between e95d703 and 45d682f.

📒 Files selected for processing (6)
  • client/firewall/firewalld/firewalld_linux.go
  • client/firewall/firewalld/firewalld_linux_test.go
  • client/firewall/firewalld/firewalld_other.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • client/firewall/firewalld/firewalld_other.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/nftables/manager_linux.go

Comment thread client/firewall/firewalld/firewalld_linux.go
@lixmal lixmal force-pushed the client/firewalld-trusted-zone branch 2 times, most recently from 28c1739 to ddc0ada Compare April 20, 2026 10:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/firewall/iptables/manager_linux.go`:
- Around line 81-87: The code trusts the WG interface via
firewalld.TrustInterface(m.wgIface.Name()) before performing later fatal init
steps (e.g., m.aclMgr.init(stateManager)), so if a subsequent init fails the
interface remains trusted; update Init() to either move the TrustInterface call
until after all fatal initializations succeed or add a rollback on error: if
m.aclMgr.init(...) (or any subsequent fatal step) returns an error, call the
complementary firewalld untrust/untust method (e.g., firewalld.UntrustInterface
or similar) with m.wgIface.Name() before returning, and log any untrust
failures; ensure the rollback is applied for every early return path so the
interface is not left trusted on failed startup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a9bf26b-fe88-4f0f-b7b4-d1eb89cdcb70

📥 Commits

Reviewing files that changed from the base of the PR and between 45d682f and 28c1739.

📒 Files selected for processing (9)
  • client/firewall/firewalld/firewalld_linux.go
  • client/firewall/firewalld/firewalld_linux_test.go
  • client/firewall/firewalld/firewalld_other.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/uspfilter/allow_netbird.go
  • client/firewall/uspfilter/common/iface.go
  • client/firewall/uspfilter/filter_test.go
✅ Files skipped from review due to trivial changes (1)
  • client/firewall/firewalld/firewalld_other.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/firewalld/firewalld_linux_test.go
  • client/firewall/firewalld/firewalld_linux.go

Comment thread client/firewall/iptables/manager_linux.go Outdated
@lixmal lixmal force-pushed the client/firewalld-trusted-zone branch 4 times, most recently from 7cb5aaa to 28fde7c Compare April 20, 2026 11:03
@lixmal lixmal force-pushed the client/firewalld-trusted-zone branch from 28fde7c to 9327582 Compare April 20, 2026 11:04
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
client/firewall/firewalld/firewalld_linux.go (2)

35-61: Process-global parent context may surprise concurrent callers.

parentCtx is process-global, so the last caller to SetParentContext wins for every subsequent TrustInterface. That is fine in the current single-engine client, but if this package is ever used from tests running engines in parallel, or a future second engine instance, cancellation scope could leak across instances. Also, trustLogOnce is package-scoped, so an engine restart within the same process will silence the "added to trusted zone" Info log on the second run.

Consider scoping state via a small struct (e.g. type Client struct { parent context.Context; logOnce sync.Once }) and returning a handle from a constructor, keeping the current free functions as thin wrappers over a package-default instance for compatibility. Not a blocker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/firewalld/firewalld_linux.go` around lines 35 - 61, The code
currently uses process-global state (parentCtx, parentCtxMu, trustLogOnce) which
can leak cancellation/logging across concurrent engine instances; refactor by
introducing a Client struct (e.g. type Client struct { parent context.Context;
parentMu sync.RWMutex; logOnce sync.Once }) with a constructor NewClient(ctx
context.Context) and make SetParentContext / getParentContext / trustLogOnce
usages methods on *Client (SetParentContext -> (*Client).SetParentContext,
getParentContext -> (*Client).getParentContext) while keeping package-level free
functions as thin wrappers that forward to a package-default Client for backward
compatibility; update TrustInterface/UntrustInterface and any references to
trustLogOnce to use the instance's logOnce so state is scoped per Client
instance.

145-157: isRunning silently returns false for non-sentinel DBus errors; consider a debug log.

Only errDBusUnavailable (from dbus.SystemBus() failures at Line 148) and context.DeadlineExceeded trigger the firewall-cmd fallback. Any other DBus error from getDefaultZone — e.g. org.freedesktop.DBus.Error.ServiceUnknown when firewalld isn't registered, AccessDenied from polkit, or wrapped peer errors that don't expose the deadline — causes isRunning to return false with no diagnostic. That's the correct outcome for "firewalld not running", but can make field debugging confusing (trust/untrust appear to silently no-op even though DBus was reachable).

Consider a single log.Debugf at Line 112 before the return false so operators have a breadcrumb when something is odd with the firewalld DBus service.

🪵 Suggested debug log
 	if errors.Is(err, errDBusUnavailable) || errors.Is(err, context.DeadlineExceeded) {
 		ctx, cancel = newCallContext(parent)
 		defer cancel()
 		return isRunningCLI(ctx)
 	}
+	log.Debugf("firewalld: dbus probe returned non-fallback error, treating as not running: %v", err)
 	return false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/firewalld/firewalld_linux.go` around lines 145 - 157, The
isRunningDBus function currently swallows non-sentinel DBus errors when
obj.CallWithContext(ctx, dbusRootIface+".getDefaultZone", 0).Store(&zone) fails;
add a debug log there to record the error before returning false so operators
can see DBus-level failures. Specifically, in isRunningDBus, just before the
existing "return false, fmt.Errorf(...)" for the getDefaultZone call, call the
package logger's Debugf (or log.Debugf) to include the error value and a short
context message mentioning getDefaultZone and dbusDest/dbusPath; keep the
existing error return behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/firewall/firewalld/firewalld_linux.go`:
- Around line 35-61: The code currently uses process-global state (parentCtx,
parentCtxMu, trustLogOnce) which can leak cancellation/logging across concurrent
engine instances; refactor by introducing a Client struct (e.g. type Client
struct { parent context.Context; parentMu sync.RWMutex; logOnce sync.Once })
with a constructor NewClient(ctx context.Context) and make SetParentContext /
getParentContext / trustLogOnce usages methods on *Client (SetParentContext ->
(*Client).SetParentContext, getParentContext -> (*Client).getParentContext)
while keeping package-level free functions as thin wrappers that forward to a
package-default Client for backward compatibility; update
TrustInterface/UntrustInterface and any references to trustLogOnce to use the
instance's logOnce so state is scoped per Client instance.
- Around line 145-157: The isRunningDBus function currently swallows
non-sentinel DBus errors when obj.CallWithContext(ctx,
dbusRootIface+".getDefaultZone", 0).Store(&zone) fails; add a debug log there to
record the error before returning false so operators can see DBus-level
failures. Specifically, in isRunningDBus, just before the existing "return
false, fmt.Errorf(...)" for the getDefaultZone call, call the package logger's
Debugf (or log.Debugf) to include the error value and a short context message
mentioning getDefaultZone and dbusDest/dbusPath; keep the existing error return
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb06b6c7-e46a-4ec3-9c30-ad701993130f

📥 Commits

Reviewing files that changed from the base of the PR and between 28c1739 and 9327582.

📒 Files selected for processing (11)
  • client/firewall/firewalld/firewalld.go
  • client/firewall/firewalld/firewalld_linux.go
  • client/firewall/firewalld/firewalld_linux_test.go
  • client/firewall/firewalld/firewalld_other.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/uspfilter/allow_netbird.go
  • client/firewall/uspfilter/common/iface.go
  • client/firewall/uspfilter/filter_test.go
  • client/internal/engine.go
✅ Files skipped from review due to trivial changes (2)
  • client/firewall/firewalld/firewalld.go
  • client/internal/engine.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • client/firewall/uspfilter/common/iface.go
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/firewalld/firewalld_linux_test.go
  • client/firewall/uspfilter/allow_netbird.go
  • client/firewall/uspfilter/filter_test.go

@lixmal lixmal merged commit 064ec1c into main Apr 21, 2026
42 checks passed
@lixmal lixmal deleted the client/firewalld-trusted-zone branch April 21, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants