mirror of
https://github.com/games-on-whales/LXC2Docker.git
synced 2026-10-15 21:02:07 +00:00
[PR #100] [MERGED] fix(network): periodic reconcile of published-port DNAT (self-heal drift) #101
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
games-on-whales/LXC2Docker#101
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/games-on-whales/LXC2Docker/pull/100
Author: @JBailes
Created: 6/29/2026
Status: ✅ Merged
Merged: 6/29/2026
Merged by: @JBailes
Base:
main← Head:fix/dnat-periodic-reconcile📝 Commits (1)
15794a6fix(network): periodic reconcile of published-port DNAT (self-heal drift)📊 Changes
3 files changed (+343 additions, -0 deletions)
View changed files
📝
internal/lxc/manager.go(+147 -0)📝
internal/lxc/network.go(+142 -0)📝
internal/lxc/network_portforward_test.go(+54 -0)📄 Description
Problem
Published-port DNAT rules (
<proto> dport <hostPort> dnat to <containerIP>:<containerPort>in tableveth_nat, chainsprerouting+output) are keyed to a container's bridge IP, which is allocated dynamically and changes on recreation. Event-driven publishing (StartContainer) plus the startupreconcile()cover the normal paths, but any drift between events went unhealed until a daemon restart:nft flush/ table rebuild,This is the recurring host-port DNAT fragility seen in production (e.g. the
gateway -> host:8741forward that kept breaking whenever the dependent container was recreated, requiring a manualrefresh-containers).Fix
Add a declarative port-forward reconcile to the existing 30s network-reconciler tick (
StartNetworkReconciler), alongside the bridge/syncHostsre-apply it already does.CurrentPortForwardsreads both thepreroutingandoutputchains (AddPortForwardwrites both) and counts a forward "correct" only when it agrees across both chains with no duplicate rules. Any disagreement or duplicate is treated as drift and re-applied. It returns an error so a transient nft read failure skips the tick rather than churning the whole table; a genuinely missing table is treated as empty (heal).reconcilePortForwardsheals missing/stale/duplicated rules (mismatch-only — a correct, active rule is never delete+re-added, so live streams aren't dropped) and prunes orphan host ports that no running container claims, so a rule resurrected by a stop/reconcile race can't linger pointing at a freed (possibly reused) IP.startingwindow or a transientState()error can never delete a live rule. A host port claimed by two containers is logged and left alone rather than driven into evict/re-add churn.nftMuserializes the multi-step DNAT mutations (AddPortForward/RemovePortForwards/RemovePortForwardForHostPort) so the reconciler and the API lifecycle handlers can't interleave their phases.Testing
go build ./...,go vet ./...,go test ./...all green.TestParseDNATChain(dual-rule duplicate detection across the chain) andTestParseHostPortKey.starting/State()-error TOCTOU were each surfaced and addressed before this PR.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.