feat(netguard,wireguard): design-13 G1+G2 netguard foundation & compiler, W1 topology, W2 apply safety - #6
Merged
Merged
Conversation
…nly legacy views
- rbac + plugin capability registration: netguard:read (read),
netguard:admin (host), mirroring the iter-020 netpolicy pattern.
- store: SecurityGroups/GuardZones/GuardBindings collections with
nil-guarded state upgrade and ErrGuardVersionConflict optimistic
concurrency on group/binding upserts.
- new internal/netguard package: PortRanges compression and
LegacyBaseline (NFTInputs -> node-private sg-legacy-<node> group +
observe-only binding + resolved builtin zones); semantics preserved,
wireguard stays a rule remote, never a trusted zone.
- read-only GET /api/netguard/{groups,zones,nodes} behind netguard:read
with per-node allowlist filtering; stored records supersede legacy
views; conversion persists nothing.
- SDK pin bumped to v0.2.17-0.20260709050800-d0f6124704ec (netguard
model types).
Zero apply-path changes. Verified: go build, gofmt, go vet, and
go test -race across netguard/store/rbac/plugin/server (server suite
355s green) with GOWORK=off against the pinned SDK.
Claude-Session: https://claude.ai/code/session_01D6PbasV2UT8nytJXGpn47Q
…ogy + W2 apply safety
G2 (iter-069) — netguard compiler LOWERS zones/groups/bindings into the
existing network.NFTPlan, so GenerateNFTPlan stays the single renderer of
lattice_guard and byte-parity with the legacy baseline is structural.
The parity gate is mutation-checked: disabling the fast path turns it red.
- network.NFTInputRule gains Interface -> renders iifname, which is what
makes a trusted overlay zone (tailscale0) expressible; it renders before
the broad allows, so a node's overlay path survives a policy-drop guard.
- lockout_risk_ssh is a BLOCKING lint: a default-drop plan with no path to
tcp/22 is refused before it reaches a node. The dmit-eb-wee failure class
moves from post-apply watchdog rollback to pre-plan refusal. Overriding it
is explicit and audited.
- netguard:admin write path: rules compile at write time (no unrenderable
rule reaches the store), referential integrity on delete, reserved legacy
id space, 409 on stale versions.
- Plans ride Approval{Plugin:nft} - same ruleset, same rollback-protected
apply script - so G2 is end-to-end usable with zero apply-path change.
W1 (iter-070) — wireguard.BuildTopology generalizes BuildMesh to named
networks with mesh/hub-and-spoke and a fail-closed custom mode. Mesh renders
identically to BuildMesh (interface, peers, and rendered config) for every
node of the fixture fleet. Host-route pinning survives: a spoke's
self-declared 0.0.0.0/0 is ignored; only a hub's reviewed ExtraAllowedIPs
widen AllowedIPs. Interface gains MTU/DNS; AllowedIPs accepts multi-value.
W2 (iter-070) — wireguard was the ONLY host-mutating apply path with no
dead-man protection. It now validates (wg-quick strip), snapshots, arms a
detached watchdog, commits, runs the control-plane selfcheck, and refuses to
report success if the watchdog fired. wg syncconf fast path avoids flapping
established tunnels when only peers changed, gated on an unchanged
[Interface] block. applyWatchdogWindowSec is now shared by nft and wireguard.
The key-bearing stripped config is removed on the failure path too.
The nft watchdog itself is untouched.
Verified: go build, gofmt, go vet clean; go test -race ./... exit 0 across
25 packages. Every generated apply script passes sh -n, which catches quoting
errors in the watchdog's nested sh -c bodies that string assertions cannot.
SDK pin -> v0.2.17-0.20260709055807-30d4d08e6fa8.
Claude-Session: https://claude.ai/code/session_01D6PbasV2UT8nytJXGpn47Q
lr00rl
marked this pull request as ready for review
July 9, 2026 06:26
The server now treats NetGuard groups and zones as fleet-global catalog objects: node-allowlisted PATs can still read/adopt/plan their allowed nodes, but cannot list or mutate global groups and zones. Node remotes are also normalized to host routes before nft lowering, so a node-reported wide WireGuard prefix cannot widen source matches. The server pin moves to the SDK commit that keeps zone_id and the guard models coherent across protobuf and Go/JSON contracts. Constraint: Firewall authoring is a host-risk path; node allowlists must remain hard boundaries Rejected: Filter group catalog partially for restricted PATs | shared groups/zones are fleet-global and need an explicit ownership model before scoped editing Rejected: Trust node-reported CIDR prefixes | a compromised node could expand source matches beyond its identity Confidence: high Scope-risk: moderate Directive: Do not expose global NetGuard catalog routes to server-allowlisted tokens without a node-owned resource model and regression tests Tested: go test ./... Not-tested: Live nft/wireguard rollback E2E on a scratch VM
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements design-13 (LatticeNet/lattice#1) slices G1, G2, W1, W2 — iter-068/069/070.
G1 — foundation (read-only, zero apply-path change)
netguard:read/netguard:adminin the RBAC catalog + plugin capability risk tableSecurityGroups/GuardZones/GuardBindingswith nil-guarded upgrade andErrGuardVersionConflictoptimistic concurrency (closes the silent-clobber gap legacyNFTInputsupserts have)internal/netguard.LegacyBaselineconverts every existing baseline into the design-13 shape; conversion happens on read and persists nothingG2 — compiler, byte-parity gate, lint, write path
The central call: lower, don't rewrite. The compiler lowers zones/groups/bindings into the existing
network.NFTPlan;GenerateNFTPlanstays the single renderer oftable inet lattice_guard. Byte-parity with the legacy path is therefore structural, and the gate is mutation-checked — disabling the fast path turnsTestLegacyBaselineRendersByteIdenticallyred (8 fixtures incl. the real dmit-eb-wee baseline).NFTInputRule.Interface→ rendersiifname, making a trusted overlay zone expressible. It renders before the broad allows, so a node'stailscale0path survives a policy-drop guard — the exact lockout class found on dmit-eb-wee.lockout_risk_sshis a blocking lint: a default-drop plan with no path to tcp/22 is refused before it reaches a node. Cleared by a management-port allow, an any-protocol accept, or a trusted zone. Overriding is explicit and audited.unverified_applywarns (loudly) whenpublic_urlis unset.netguard:adminwrite path: rules compile at write time; referential integrity on delete; reservedsg-legacy-*id space;409on stale versions.Approval.Plugin="nft"— same ruleset, same rollback-protected apply script — so G2 is end-to-end usable with zero apply-path change.W1 — WireGuard topology
BuildTopologygeneralizesBuildMeshto named networks: mesh, hub-and-spoke, and a fail-closedcustom(never silently degrades to mesh). Security invariants preserved verbatim: host-route pinning survives a member reporting10.66.0.5/16; a spoke's self-declared0.0.0.0/0is ignored; only a hub's reviewedExtraAllowedIPswiden a peer.Interfacegains MTU/DNS (rendered only when set, validated);AllowedIPsaccepts multi-value.Migration gate: the existing implicit mesh renders identically through
BuildTopology(mesh)— same interface, same peers, same rendered config, for every node.W2 — the headline safety gap, closed
wireguardwas the only host-mutating apply path with no dead-man protection (server.go:4810-4821). A badwg0.confstranded a node. It now does what nft always did:wg-quick stripvalidate → snapshot → arm detached watchdog → commit →--selfcheck-controlplane→assert_watchdog_clean→ disarm.Plus a
wg syncconffast path (no tunnel flap on peer-only changes, gated on an unchanged[Interface]block), a sharedapplyWatchdogWindowSecacross nft and wireguard, and removal of the key-bearing stripped config on the failure path. The nft watchdog itself is untouched — a tested crown-jewel path got a structurally identical sibling, not a risky refactor.Test plan
go build,gofmt -l,go vetclean (GOWORK=off, pinned SDK)go test -race -count=1 ./...→ exit 0, 25 packagessh -n— catches quoting errors in the watchdog's nestedsh -cbodies that string assertions cannotSDK pin →
v0.2.17-0.20260709055807-30d4d08e6fa8(pairs with LatticeNet/lattice-sdk#5). No stable tags minted; the alpha image train stays atalpha-0.2.1a24until this merges.Next: G3 reality/drift, G4 dashboard, W1b store/API/discovery.
https://claude.ai/code/session_01D6PbasV2UT8nytJXGpn47Q