Skip to content

feat(network): add network policy create (--stamp/--deny/--reply-stamp)#803

Open
brunodam wants to merge 1 commit into
mainfrom
00758-network-policy-create
Open

feat(network): add network policy create (--stamp/--deny/--reply-stamp)#803
brunodam wants to merge 1 commit into
mainfrom
00758-network-policy-create

Conversation

@brunodam

@brunodam brunodam commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

Implements solo-provisioner network policy create — Story 1.2 of epic TS_1 (#738), the second scope in the network command family after network firewall (#757). This is the command-driven authoring surface for the inet weaver classification/ACL plane (design §7.2.4, §8.4.2, §8.4.6).

create renders one named category's rule(s) into the inet weaver forward chain, ensures its nft set @<name> exists, writes a per-policy registry JSON under /etc/solo-provisioner/policies/, applies the full chain to the live kernel with nft -f, and atomically rewrites /etc/solo-provisioner/network-weaver.nft. The new internal/network/policy package mirrors the merged internal/network/firewall package (Manager, atomic temp+rename write, shared apply flock, shared boot oneshot) but targets the inet weaver table, which has the opposite lifecycle — its set membership churns continuously as the daemon reconciles it from statusz.

Key design decisions (so reviewers don't reverse-engineer them):

  • --stamp priority lookup is in-code. The 6 stable QoS classes (publisherreserve-egress) map to fixed skb->priority / ct-mark values from design §5, baked into class.go. Referencing an unknown class is a create-time error. This means Story 1.2 has no runtime dependency on Story 1.4 (network shape) — the name→priority encoding is static; shape only sets each class's bandwidth.
  • Full chain rendered in tier order (deny → reply-restore → specific stamp → fallthrough stamp → est/rel accept → drop), never creation order. A mis-ordered deny would let already-open connections survive a quarantine, so ordering is a correctness invariant, pinned by a golden file. This is the boundary the epic owner and I agreed on for 1.2; see "Scope boundary" below for what's deferred to Story 1.5 — Policy registry and tier-order chain re-render #772.
  • Set membership is never persisted to network-weaver.nft (§8.3.1) — statusz is the source of truth and the daemon reconciles it. --cidrs seeds the live set only, via a separate nft add element.
  • Kernel apply is direct nft -f, not a service restart (the shared boot oneshot does not load network-weaver.nft until Story 0.3 — inet host reboot persistence (network-host.nft + oneshot) #780 extends it). create still enables the shared unit for boot readiness.

Files changed

File Change
internal/network/policy/paths.go Package doc + inet weaver path/service/lock constants
internal/network/policy/class.go Static §5 class→mark/priority map + lookupClass
internal/network/policy/policy.go Policy registry model + Validate (all flag-combination rules)
internal/network/policy/render.go Tier-order full-chain renderer + atomicWriteFile
internal/network/policy/registry.go Per-policy registry JSON read/write/load-all
internal/network/policy/manager.go Manager.Create (flock, idempotent, apply+persist)
internal/network/policy/nft.go Exec nft Runner (Apply/AddElements/List/Delete/Exists)
internal/network/policy/service_{linux,other}.go Shared oneshot enable (Linux) / no-op (other)
internal/network/policy/*_test.go + testdata/network-weaver.golden.nft Render golden, tier-order, validation, idempotency, registry tests
cmd/cli/commands/network/policy/{policy,create}.go network policy create verb + flags
cmd/cli/commands/network/policy/policy_test.go Command structure, flag, and execution tests
cmd/cli/commands/network/network.go Wire policy.GetCmd() into the network group
docs/quickstart.md Create a Traffic Policy section (examples + flags table)

Review guide

Code review checklist

  • class.go — the 6 class priorities/marks match design §5 exactly (publisher 0x10/0x10010 … reserve-egress 0x60/0x10060); reply ct-mark = the reply class's mark.
  • policy.go:Validate--stamp--deny; --reply-stamp requires --stamp+--direction egress; --from-entity world--cidrs and stamp-only; --reply-stamp CIDRs are ip:port; IPv6 rejected (ipv4_addr sets).
  • render.go — tier order (deny → reply-restore → specific → fallthrough → est/rel → drop); membership set schemas carry no elements, only _ports sets do; compound ipv4_addr . inet_service set for reply-stamp; ip daddr . compound match on the egress forward rule.
  • manager.go:Create — render happens before any disk write (a failed apply leaves the registry untouched); idempotent no-op on identical config; created_at preserved across a config-change re-render; whole op under the shared flock.
  • Port-set naming (@<policy-name>_ports) — the design examples are internally inconsistent (@bn_ports_publisher by class vs @bn_ports_subscriber shared across policies). I chose a mechanical per-policy set. Flag if you want shared class-grouped port sets instead.
  • network-weaver.golden.nft — read it top-to-bottom against §7.2.4; it is the behavioral contract.

Scope boundary (epic #738) — this PR is create only. Deferred:

Test commands

# Unit tests (macOS OK — internal/network/policy has no Linux-only deps)
go test -race -cover -tags='!integration' ./internal/network/policy/...

# CLI command package (Linux only — pulls in internal/mount; run in the VM)
task vm:test:unit

# Linux cross-compile of the touched packages
GOOS=linux GOARCH=amd64 go build ./internal/network/policy/... ./cmd/cli/commands/network/...

# Lint (errorx/forbidigo) + license headers
task lint:check
task license:check

Manual UAT (UTM VM — Debian/Ubuntu with nftables + systemd)

  1. Build and copy the binary to the VM:
    task build:cli GOOS=linux GOARCH=amd64
    scp bin/solo-provisioner-linux-amd64 <vm>:/usr/local/bin/solo-provisioner
  2. Create a stamp policy (pod CIDR passed explicitly so no cluster is needed):
    sudo solo-provisioner network policy create --name bn-publisher \
      --direction ingress --ports 40840 --stamp publisher \
      --cidrs 10.1.0.1/32 --pod-cidr 10.4.0.0/24
    Verify the live chain:
    sudo nft list table inet weaver
    Expect a chain forward with policy drop, the set bn-publisher (type ipv4_addr; flags interval), the port set bn-publisher_ports with elements = { 40840 }, and the rule:
    ip daddr 10.4.0.0/24 ip saddr @bn-publisher tcp dport @bn-publisher_ports meta priority set 0x10010 accept
    
    The live @bn-publisher set should contain 10.1.0.1/32, but the on-disk file must not:
    sudo nft list set inet weaver bn-publisher        # shows 10.1.0.1/32 (live)
    grep 10.1.0.1 /etc/solo-provisioner/network-weaver.nft   # no match (membership not persisted)
    cat /etc/solo-provisioner/policies/bn-publisher.json     # registry entry present
  3. Add a quarantine (deny) policy and confirm the drops render above ct state established,related accept:
    sudo solo-provisioner network policy create --name bn-restricted \
      --deny --cidrs 10.99.0.0/16 --pod-cidr 10.4.0.0/24
    sudo nft -a list table inet weaver | grep -nE 'drop|established'
    Expect ip saddr @bn-restricted drop / ip daddr @bn-restricted drop to appear before the ct state established,related accept line.
  4. Add a backfill reply-stamp policy and confirm the compound set + auto-rendered restore rule:
    sudo solo-provisioner network policy create --name bn-backfill \
      --direction egress --stamp reserve-egress --reply-stamp backfill-response \
      --cidrs 10.30.5.7:43473 --pod-cidr 10.4.0.0/24
    sudo nft list table inet weaver | grep -E 'ct direction reply|ct mark set 0x20|bn-backfill'
    Expect the set bn-backfill { type ipv4_addr . inet_service; }, the egress forward rule with ct mark set 0x20 meta priority set 0x10060, and the ingress restore ct direction reply ct mark 0x20 meta priority set 0x10020 accept.
  5. Idempotency: re-run step 2 verbatim → command reports "already exists with identical configuration"; re-run with a changed --ports → re-renders. nft list table inet weaver reflects the change with no duplicate rules.
  6. Error path: --stamp bogus exits non-zero with unknown class "bogus": must be one of … and leaves the table unchanged.
  7. Boot readiness: systemctl is-enabled solo-provisioner-network-nft.serviceenabled.

Risks / rollback

Related Issues

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 2, 2026 22:00
@brunodam brunodam requested a review from a team as a code owner July 2, 2026 22:00
@brunodam brunodam requested a review from tomzhenghedera July 2, 2026 22:00
@swirlds-automation

swirlds-automation commented Jul 2, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds the solo-provisioner network policy create command and a new internal/network/policy implementation for authoring and applying tier-ordered inet weaver nftables policies (stamp/deny/reply-stamp), including on-disk persistence (network-weaver.nft) and a per-policy registry.

Changes:

  • Introduce internal/network/policy with policy model + validation, deterministic full-chain renderer (golden-file pinned), registry IO, and a Manager.Create implementation that applies via nft -f and seeds live set membership.
  • Wire a new network policy create Cobra verb (flags, pod-CIDR auto-detect, tests) and register it under the network command group.
  • Document the new command in docs/quickstart.md with examples and a flags table.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/network/policy/paths.go Defines inet weaver constants (paths/service/lock) and package-level scope docs
internal/network/policy/class.go Static QoS class → mark/priority lookup used by --stamp/--reply-stamp
internal/network/policy/policy.go Policy registry model + flag-combination and input validation
internal/network/policy/render.go Tier-ordered full-chain renderer + atomic file writer
internal/network/policy/registry.go Read/write/load-all for per-policy registry JSON files
internal/network/policy/manager.go Manager.Create orchestration (lock, merge, apply, persist, enable service)
internal/network/policy/nft.go nft runner implementation (Apply, AddElements, List, Delete, Exists)
internal/network/policy/service_linux.go Linux systemd unit install/enable logic for boot readiness
internal/network/policy/service_other.go Non-Linux no-op service enabler for cross-platform unit tests
internal/network/policy/validate_test.go Unit tests for Policy.Validate and lookupClass
internal/network/policy/policy_test.go Golden render + tier invariants + create/registry behavior tests
internal/network/policy/testdata/network-weaver.golden.nft Golden expected nft document contract for render output
cmd/cli/commands/network/policy/policy.go Adds network policy command group and wires create
cmd/cli/commands/network/policy/create.go Implements network policy create flag parsing, CIDR resolution, podCIDR detection
cmd/cli/commands/network/policy/policy_test.go CLI structure/flags tests + end-to-end command execution tests (stubbed manager/detect)
cmd/cli/commands/network/network.go Registers policy under the network command group
docs/quickstart.md Adds quickstart docs for creating traffic policies and flag semantics

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/network/policy/manager.go
Comment thread internal/network/policy/manager.go
Comment thread cmd/cli/commands/network/policy/create.go Outdated
@brunodam brunodam force-pushed the 00758-network-policy-create branch 4 times, most recently from b300d6c to 1b0b97b Compare July 3, 2026 01:08
…amp)

Implement the `solo-provisioner network policy create` verb (epic TS_1
#738, Story 1.2 #758): the command-driven authoring surface for the
`inet weaver` classification/ACL plane (design §8.4.2, §8.4.6).

`create` renders one named category's rule(s) into the inet weaver
forward chain, ensures its nft set `@<name>` exists, writes a per-policy
registry JSON under /etc/solo-provisioner/policies/, applies the full
chain to the live kernel with `nft -f`, and atomically rewrites
/etc/solo-provisioner/network-weaver.nft. It mirrors the merged
internal/network/firewall package (Manager, atomic write, shared flock,
shared boot oneshot) but targets the inet weaver table.

- --stamp resolves the priority from an in-code map of the 6 stable §5
  classes; referencing an unknown class is an error (no dependency on
  Story 1.4's shape state).
- --deny / --reply-stamp / --from-entity world / --ports / --cidrs(-file)
  per the flag-validity rules in §8.4.2/§8.4.6.
- The full chain is rendered in tier order (deny -> reply-restore ->
  specific stamp -> fallthrough -> est/rel -> drop), never creation order.
  Set membership is never persisted (§8.3.1); --cidrs seeds the live set
  only via nft add element.

Scope boundary (epic #738): create only. Element verbs are #759; delete,
overlap-rejection, the created_at tiebreaker and last-policy teardown are
#772. Port sets are named `@<policy-name>_ports` (a mechanical choice
where the design examples are inconsistent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Bruno Marques <bruno.marques@swirldslabs.com>
@brunodam brunodam force-pushed the 00758-network-policy-create branch from 1b0b97b to a22616d Compare July 4, 2026 20:41
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.

Story 1.2 — Implement network policy create (--stamp/--deny/--reply-stamp)

3 participants