Skip to content

feat(firewall): use direct nftables when supported - #3157

Open
qdm12 wants to merge 21 commits into
masterfrom
nftables
Open

feat(firewall): use direct nftables when supported#3157
qdm12 wants to merge 21 commits into
masterfrom
nftables

Conversation

@qdm12

@qdm12 qdm12 commented Feb 25, 2026

Copy link
Copy Markdown
Member

Description

Try it out using image tag :pr-3157 and FIREWALL_IMPLEMENTATION=nftables

  • New option FIREWALL_IMPLEMENTATION which can be iptables (default) or nftables
  • When using nftables, it uses kernel calls directly and does not rely on iptables at all.
  • Add nft cli (100KB extra) to support custom post rules like it is currently with iptables.
  • Tested out, test there is no leak
  • Github wiki updated

Later, after v3.42.0 release, change it such that:

  • If the nftables backend is supported, default to using the nftables implementation. Otherwise, fallback on using iptables.

Issue

customRulesPath = "/iptables/post-rules.txt"
case "nftables":
impl = nftables.New(logger)
customRulesPath = "/gluetun/firewall/nftables/post-rules.txt"

@qdm12 qdm12 Aug 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: this needs documentation in the github-wiki after this PR gets merged.

Comment thread Dockerfile
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
apk add --no-cache --update openvpn ca-certificates nftables iptables iptables-legacy tzdata && \

@qdm12 qdm12 Aug 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nit: nftables adds 100KB to the image, to be able to run nft commands, worth it imo

@qdm12
qdm12 marked this pull request as ready for review August 19, 2026 18:20
@qdm12
qdm12 deployed to secrets August 19, 2026 21:54 — with GitHub Actions Active
@qdm12
qdm12 deployed to secrets August 19, 2026 21:56 — with GitHub Actions Active
@qdm12 qdm12 added Status: 🟡 Nearly resolved This might be resolved or is about to be resolved and removed Status: 🔒 After next release Will be done after the next release labels Aug 22, 2026
@qdm12
qdm12 requested a lite review from Copilot August 22, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a selectable direct nftables firewall backend while retaining iptables support.

Changes:

  • Adds firewall implementation configuration and wiring.
  • Implements nftables filtering, NAT, conntrack, custom rules, and rollback.
  • Updates dependencies, container tooling, callers, and tests.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 6 comments.

Show a summary per file
File Review summary
internal/pmtud/tcp/tcp_integration_test.go Updates firewall construction in integration tests.
internal/pmtud/tcp/helpers_test.go Updates firewall construction helpers.
internal/pmtud/pmtud_integration_test.go Updates PMTUD firewall setup.
internal/firewall/nftables/version_test.go Tests nftables version handling.
internal/firewall/nftables/unsupported.go Provides unsupported-platform behavior.
internal/firewall/nftables/tcp.go Implements nftables TCP rules.
internal/firewall/nftables/tcp_test.go Tests TCP rule generation.
internal/firewall/nftables/support.go Detects support and parses user rules.
internal/firewall/nftables/support_test.go Critical (1 vote): mock expectation does not match the variadic warning arguments.
internal/firewall/nftables/redirect.go Implements redirect rules.
internal/firewall/nftables/redirect_test.go Tests redirect handling.
internal/firewall/nftables/output.go Critical (2 votes): mixed address families are not rejected.
internal/firewall/nftables/output_test.go Tests output rules.
internal/firewall/nftables/mocks_test.go Defines test mocks.
internal/firewall/nftables/mocks_local_test.go Provides local test mocks.
internal/firewall/nftables/mocks_generate_test.go Supports mock generation.
internal/firewall/nftables/interfaces.go Defines nftables interfaces.
internal/firewall/nftables/interfaces_local.go Defines platform-local interfaces.
internal/firewall/nftables/input.go Implements input rules.
internal/firewall/nftables/input_test.go Tests input rule generation.
internal/firewall/nftables/firewall.go Implements nftables firewall lifecycle.
internal/firewall/nftables/filter.go Critical (1 vote): existing user-owned chains may be modified and flushed.
internal/firewall/nftables/filter_test.go Tests filtering behavior.
internal/firewall/nftables/exprs.go Builds nftables expressions.
internal/firewall/nftables/exprs_test.go Tests expression generation.
internal/firewall/nftables/delete.go Implements rule deletion.
internal/firewall/nftables/delete_test.go Tests deletion behavior.
internal/firewall/nftables/conntrack.go Implements conntrack handling.
internal/firewall/nftables/conntrack_test.go Tests conntrack behavior.
internal/firewall/nftables/basechains.go Configures nftables base chains.
internal/firewall/nftables/basechains_test.go Tests base-chain configuration.
internal/firewall/nftables/atomic.go Critical (3 votes): rollback does not restore tracked rules, remove newly created tables, or losslessly preserve unsupported/stateful objects.
internal/firewall/nftables/atomic_test.go Tests atomic operations.
internal/firewall/iptables/iptables.go Refactors iptables policy handling.
internal/firewall/interfaces.go Extends the firewall abstraction.
internal/firewall/firewall.go Selects the configured backend.
internal/firewall/enable.go Configures firewall policy behavior.
internal/configuration/settings/settings_test.go Updates settings expectations.
internal/configuration/settings/firewall.go Defines firewall implementation configuration.
internal/configuration/settings/firewall_test.go Tests firewall setting validation.
go.sum Records dependency checksums.
go.mod Adds the nftables dependency.
Dockerfile Installs nftables tooling.
cmd/gluetun/main.go Passes backend selection to firewall construction.
.devcontainer/Dockerfile Adds nftables development tooling.
Suppressed comments (2)

internal/firewall/nftables/filter.go:39

  • The new backend is covered only by mocked netlink tests, so the tests do not verify that these expressions are accepted by the target kernel or that the resulting inet filter/NAT rules actually prevent leaks. Add a Linux integration test that exercises packet filtering and enable/restore behavior with the required capabilities before making this backend generally usable.
func setupFilterWithBaseChains(conn conn, policy *nftables.ChainPolicy) (table *nftables.Table,
	inputChain, forwardChain, outputChain *nftables.Chain,
	err error,
) {

internal/firewall/nftables/redirect.go:169

  • The iptables backend implements this API with REDIRECT, which rewrites the destination to a local address as well as changing the port. This rule uses port-only DNAT, so it leaves the packet addressed to the VPN IP; a service bound only to loopback or another local address will not receive the redirected traffic. Use the nftables redirect to :port expression (expr.Redir) to preserve the existing behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +46 to +48
return func(ctx context.Context) {
f.restoreTablesLocked(ctx, tables)
}, nil

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the restore function now snapshots the tracked rules at save time and resets f.rules to that snapshot, so disable/enable cycles can't double-track rules

Comment on lines +118 to +120
func restoreTables(conn conn, savedTables []savedTable) error {
for _, savedTable := range savedTables {
table := conn.AddTable(savedTable.table)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

restore now deletes every table that exists but wasn't in the snapshot, so the owned table (and its DROP policy) is removed after disable or a failed enable. Since the backend now owns a dedicated inet gluetun table, this deletes exactly what the backend created; user post rules are re-run at each enable so tables they create are harmlessly removed. Verified with a new kernel integration test (GLUETUN_NFTABLES_INTEGRATION=1)

Comment on lines +79 to +80
// saveTables saves the state of all the tables, their chains, and their rules.
func saveTables(conn conn) ([]savedTable, error) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed structurally: the backend no longer snapshots or re-adds user state — it owns a single dedicated table whose rollback is a plain DelTable (no rule parsing involved), so unknown expressions/sets can't cause a weakened re-add. The remaining snapshot is only used to roll back user post rules, which has the same inherent limitation in iptables mode (iptables-save/restore). A fully lossless CLI-based restore (nft -a list rulesetnft -f) was considered but conflicts with the direct-netlink design goal of this backend.

Comment on lines +64 to +69
ensureChain := func(name string, hooknum *nftables.ChainHook) *nftables.Chain {
if chain, ok := existingChains[name]; ok {
if policy != nil {
chain.Policy = policy
conn.AddChain(chain)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — the backend now uses a dedicated inet gluetun table with its own chains instead of reusing the conventional inet filter/inet nat tables, so user-owned policies/chains/rules are never modified (this also stops Gluetun's dnat rules from landing in Docker's nat table in iptables-nft containers).

Comment on lines +133 to +134
exprs := append(outputInterfaceExprs(intf), sourceIPExprs(source.Addr())...)
exprs = append(exprs, destinationIPExprs(destination.Addr())...)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — both AcceptOutputFromIPPortToIPPort and AcceptOutputFromIPToSubnet (same latent issue) now reject mixed families with the same error the iptables backend returns.


ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Warnf(gomock.Any(), gomock.Any()).Times(testCase.expectedWarnings)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The tests actually pass as-is: with go.uber.org/mock variadic matching, the last expectation matcher covers the first variadic argument, so Warnf(Any, Any) matches the 3-argument call (see Call.matches in gomock/call.go). That said, the expectation is now precise — it matches the format string, line number, and line text explicitly

… validation

- use a dedicated inet gluetun table for all backend chains, so
  user-defined tables, chains, and rules are never modified
- restore removes tables created after the save (no more DROP policy
  or rule leak after disable), and resets the tracked rules to the
  saved state (no more double tracking across enable/disable cycles)
- remove a stale gluetun table left by a previous crashed session
  before saving, so the restore does not resurrect it
- reject mixed source/destination address families in
  AcceptOutputFromIPPortToIPPort and AcceptOutputFromIPToSubnet,
  matching the iptables backend behavior
- match the Warnf mock expectation in Test_parseUserRules with the
  three actual arguments (format, line number, line text)
- add a kernel integration test verifying the no-leak restore,
  skipped unless GLUETUN_NFTABLES_INTEGRATION=1
- gofumpt unsupported.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: enabling firewall fails, Couldn't load match `conntrack' Feature request: use google/nftables to handle nftables firewall

2 participants