Conversation
2c06921 to
9a5995f
Compare
40f126b to
44d5104
Compare
| customRulesPath = "/iptables/post-rules.txt" | ||
| case "nftables": | ||
| impl = nftables.New(logger) | ||
| customRulesPath = "/gluetun/firewall/nftables/post-rules.txt" |
There was a problem hiding this comment.
note: this needs documentation in the github-wiki after this PR gets merged.
| 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 && \ |
There was a problem hiding this comment.
nit: nftables adds 100KB to the image, to be able to run nft commands, worth it imo
There was a problem hiding this comment.
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 nftablesredirect to :portexpression (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.
| return func(ctx context.Context) { | ||
| f.restoreTablesLocked(ctx, tables) | ||
| }, nil |
There was a problem hiding this comment.
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
| func restoreTables(conn conn, savedTables []savedTable) error { | ||
| for _, savedTable := range savedTables { | ||
| table := conn.AddTable(savedTable.table) |
There was a problem hiding this comment.
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)
| // saveTables saves the state of all the tables, their chains, and their rules. | ||
| func saveTables(conn conn) ([]savedTable, error) { |
There was a problem hiding this comment.
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 ruleset → nft -f) was considered but conflicts with the direct-netlink design goal of this backend.
| ensureChain := func(name string, hooknum *nftables.ChainHook) *nftables.Chain { | ||
| if chain, ok := existingChains[name]; ok { | ||
| if policy != nil { | ||
| chain.Policy = policy | ||
| conn.AddChain(chain) | ||
| } |
There was a problem hiding this comment.
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).
| exprs := append(outputInterfaceExprs(intf), sourceIPExprs(source.Addr())...) | ||
| exprs = append(exprs, destinationIPExprs(destination.Addr())...) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
Description
Try it out using image tag
:pr-3157andFIREWALL_IMPLEMENTATION=nftablesFIREWALL_IMPLEMENTATIONwhich can beiptables(default) ornftablesnftables, it uses kernel calls directly and does not rely on iptables at all.nftcli (100KB extra) to support custom post rules like it is currently with iptables.Later, after v3.42.0 release, change it such that:
nftablesimplementation. Otherwise, fallback on using iptables.Issue
google/nftablesto handle nftables firewall #898