Skip to content

review: batch-end round on the SOCKS5 opt-in, ports and lane-file state - #173

Merged
CMGS merged 6 commits into
mainfrom
review/post-lane
Sep 14, 2026
Merged

CMGS merged 6 commits into
mainfrom
review/post-lane

Conversation

@CMGS

@CMGS CMGS commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Batch-end round on the assumed-merged state of #169, #170 and #171, stacked on #171 (base feat/egress-lane-file); merge order #169 → #170 → #171 → this, retargeting each child to main before its base branch is deleted. Two commits: a fix with tests and the round itself. Codex reviewed the stack on the same thread as the three PRs and converged (see the end).

Scope

The hygiene ledger decided what was read: 18 Go files due for the style lens and 12 for the judge lens (exactly the files the three PRs touched), every silkd and boot/init source file for an ownership-weighted Rust pass on the owner's request, all 17 docs pages plus the os-image READMEs in full. Readers: 2 Go style (sonnet), 2 Go judge (opus), 1 Rust ownership (opus), 2 docs (sonnet), the four /simplify lenses (3 sonnet, altitude on opus), 2 adversarial correctness/contract (opus). Every finding was re-read at the cited line before it was applied or declined; the ledger closes with 0 files due on both lenses.

Fixed — fix: deny a tunnel whose authority names a port outside 1-65535 (with tests)

hostPort substituted the scheme default for a port it could not parse, and serveConnect dialed the raw authority, so the verdict and the dial could name different ports. Two readers proved it reachable: CONNECT example.com:99999 was judged and audited as 443 (a ports: [443] rule allowed it; the intercept path terminated TLS for it) and only the dial failed; a path-form CONNECT /x with Host: example.com:+22 was judged as 443, allowed, and dialed to port 22, which Go's dialer accepts — a port a rule forbids was reachable with an allow verdict, from the guest's own loopback relay. An unparsable or zero port is now a deny with an audit line carrying no port on both doors, serveConnect dials the parsed pair, the intercept handler builds its authority from the same pair, and serveSocks refuses port 0 the same way. Tests: TestUnparsablePortIsDeniedNotDefaulted (99999 on CONNECT and forward, the +8443 path-form CONNECT) and TestSocksPortZeroIsDeniedEvenByABareRule.

Applied — review: settle the door's owner, the lane wait and the ownership round

  • The SOCKS5 door is the pool policy's call. composite.ServesSocks() returns the pool's flag: the tenant's rules already gate every tunnel through Eval, so the tenant-side AND removed a capability without removing any reach and failed silently when a tenant policy omitted the flag (the docs example having to repeat the flag on the tenant was the tell). A tenant's flag counts only where the tenant policy is the whole policy. This reverses egress: make the SOCKS5 door opt-in per policy #169's composition; the altitude lens raised it and Codex accepted it.
  • Policy.ServesSocks() returns the flag; Validate already guarantees an opted-in policy has a rule that admits CONNECT, and no runtime path mutates policies.
  • The desktop unit's wait is bounded by the host's worst case, not a guess (Codex's MAJOR on egress: write the host's lane verdict into every NIC-bearing guest #171): 300 s, longer than the host's probe (90 s) plus write (120 s) timeouts, so a guest still waiting belongs to a boot the host already failed; the timeout branch exits non-zero so a mis-timed golden is visible; the NIC test matches silkd's device-backed probe; the verdict is read without tripping set -e; desktopsmoke asserts the unit is active and passes its probe URL as a positional argument.
  • Rust ownership: routes_directly() latches a relay verdict (the host never unlocks a lane, but a late lock can land after the first exec, so only the positive answer is cached); base_env returns a static slice instead of a Vec per spawn; the watcher moves its owned path into the frame; an in-function import moves to the file's group. The audit found no json! churn, no unwrap outside tests, every fan-out bounded; kept with triggers in perf: remaining performance ledger after the 2026-09 SOCKS5 batch #172: the eager 256-slot broadcast ring per exec, per-git-verb label strings.
  • hostOnly folded into hostPort; sockssmoke's IMAP-on-143 leg asserts curl's SOCKS5 refusal code (%{http_code} is 000 for any imap:// URL, so the old assertion could not fail); a cold-provision row for the unlocked egress lane.
  • Docs: deploy.md Upgrading gains the daemon/image lockstep for /etc/silkd-lane, the strict-config rollback (a config with socks5/ports does not load on an older sandboxd) and the opt-in behavior change; silkd.md says the 1080 dial is refused when the policy did not opt in; the desktop README names the installed apps and the unit's contract; egress.md/desktop.md follow the door decision; README lists the socks e2e driver.
  • Style: two restating comments deleted, logger locals bound once in reconcile and refillOne, exported consts first in lane.go, two t.Errorf with constant strings become t.Error. Comment lines in the round: +5 / −8.

Declined, with reasons

  • Repeated-port rejection in Validate (judge: no behavioral effect) — kept, egress: let a rule constrain destination ports #162's shape, config-time.
  • Named empty Lane, .nic sidecar rename (would force a golden rebuild), sync.OnceFunc restructuring, six-parameter relay, fmt.Errorf with a static string (house style for config errors), two-clause WHY comments that need both halves.
  • Fail-open on a lane write that fails right after a successful readiness probe on the direct lane — kept fail-closed: same path as the CA install; a guest that cannot take a file after answering is not one to hand out.
  • A boot-time channel for the verdict (would remove the wait entirely) needs a per-VM knob cocoon does not have; the reconcile late-write under a running desktop is documented as healed by a re-claim.
  • Old guest image on a new host and the reverse — documented in deploy.md; the marker shipped in no release.

LOC

Round (11e2fcb..edec7ec, Go + Rust, e2e drivers counted as prod): prod +93 −59 (net +34), test +119 −31 (net +88). The prod adds are the port fix's deny branches and the bare-host/IPv6 validation (+18), the SOCKS port-0 refusal (+1), silkd's relay latch (+9) and the two base_env statics (+4), desktopsmoke's unit check and positional probe (+3), sockssmoke's exit-code leg (+3); removals are hostOnly (−7), the comment cuts, the logger hoists and the ServesSocks conjunct. Whole batch against main (ca6d08f..edec7ec): prod +330 −157 (net +173), test +258 −54 (net +204) — the three features plus the round.

Gates

  • make go-lint 10 × 0 issues.; asl -forwarder=false clean on linux and darwin; go test -race on egress/pool/engine/config; go vet ./... on e2e.
  • silkd: cargo fmt --check, clippy --all-targets -D warnings, 111 tests on macOS and 111 in a rust:1.98.0 linux/arm64 container.
  • Hardware, cocoon-test1, this branch's sandboxd on a guest image carrying this branch's silkd (rt-lane5): scripts/socks-e2e.sh PASS (nine legs), scripts/egress-e2e.sh NET=none PASS and NET=egress BRIDGE=sbxbr0 PASS, scripts/sandboxd-e2e.sh BRIDGE=sbxbr0 PASS. Desktop lane on desktop-lane5 (desktop:24.04 + this silkd + this guest-proxy-setup): desktopsmoke -net egress PASS on a 2xlarge egress-lane pool with a policy — guest-proxy.service active, http_proxy in the session's environment, the allowed origin answering 200 through the relay (audited as GET port 80); desktopsmoke -net none PASS with the checkpoint/branch leg.
  • Four follow-up commits after Codex's rounds on this branch, each closing one of its MINORs on hostPort with a test: c710dde refuses a malformed authority such as host:443:8443 instead of defaulting it (and names the pool policy as the door's owner in three doc sentences), edec7ec unwraps a bracketed IPv6 literal without a port so the rules and the dial see the bare address, c42f1a8 checks bracket syntax before the bare-host default so [not-an-address] is a no-port deny under a wildcard rule too, 25299db defaults only a nonempty bare host with no colon or bracket so echo.internal] and an empty Host are denied as well.
  • Codex, same thread as the three PRs, five rounds on this branch: round 1 accepted the pool-owned door and the bounded wait and raised four MINORs, rounds 2–4 each one MINOR on hostPort's malformed-authority handling (all applied above), round 5 on 25299db: "No blockers remain for the review branch." Lane branch egress: write the host's lane verdict into every NIC-bearing guest #171: "No blockers remain for the lane branch."

@CMGS
CMGS force-pushed the feat/egress-lane-file branch from 11e2fcb to fe3416d Compare September 14, 2026 06:56
@CMGS
CMGS changed the base branch from feat/egress-lane-file to main September 14, 2026 06:57
hostPort substituted the default port for one it could not parse, and
serveConnect dialed the raw authority, so the verdict and the dial could
name different ports. CONNECT example.com:99999 was judged and audited
as port 443 (a rule listing 443 allowed it, the intercept path even
terminated TLS for it) and only the dial failed; a path-form CONNECT
with Host: example.com:+22 was judged as 443 and then dialed to port 22,
which Go's dialer accepts, so a port a rule forbids was reachable with
an allow verdict. An unparsable or zero port is now a deny with an audit
line carrying no port; serveConnect dials the parsed host and port, and
the intercept handler builds its authority from the same pair, so the
verdict, the dial and the inner requests agree on one target.
The SOCKS5 door is the pool policy's call: the tenant's rules already gate
every tunnel through Eval, so the tenant-side AND removed a capability
without removing any reach and failed silently when a tenant policy omitted
the flag. ServesSocks trusts Validate instead of re-checking the tunnel rule
it guarantees. A raw SOCKS5 request for port 0 is refused like the HTTP
doors refuse one. hostOnly folds into hostPort.

The desktop unit's wait outlives the host's probe and write timeouts
(90 s + 120 s) instead of 30 s, exits non-zero when no verdict arrives so a
mis-timed build is visible, tests device-backed NICs the way silkd does, and
reads the file without tripping set -e; desktopsmoke checks the unit is
active and passes its probe URL as a positional argument. sockssmoke's IMAP
leg asserts curl's SOCKS5 refusal code, since %{http_code} is 000 for any
imap:// URL.

silkd latches a relay verdict (the host never unlocks a lane, a late lock
can land after the first exec) so the per-exec read happens once on the
bridge lane, base_env returns a static slice instead of a Vec per spawn,
the watcher moves its owned path into the frame, and an import moves to the
file's use group. Deploy docs gain the daemon/image lockstep for the lane
file, the strict-config rollback and the opt-in behavior change; the desktop
README names the installed apps and the unit's contract; two restating
comments and the logger locals in reconcile and refillOne follow the
house style. Comment lines: +5 / -8.
… docs

hostPort treated every split error as a bare host, so a path-form CONNECT
with Host: example.com:443:8443 was judged as 443 and audited as allowed
before the dial failed; only a host with no colon, or a bracketed IPv6
literal, takes the default now. Three doc sentences still said "the
policy" opts into the SOCKS5 door where it is the pool policy, or the
tenant's only on a claim outside any configured pool.
…-bracketed

A path-form CONNECT whose Host was a bracketed IPv6 literal without a
port kept its brackets as the policy host, so the verdict was recorded
on "[addr]" and the dial went to "[[addr]]:443" and failed after an
allowed audit line; any bracketed text passed the same way. The literal
is now unwrapped and parsed with netip, a valid IPv6 address becomes the
bare host the rules and the dial both see, and anything else is refused.
A bracketed authority with no colon inside, such as "[not-an-address]",
reached the bare-host branch first and was allowed under a wildcard rule
before the dial failed. An authority that opens a bracket must now close
it around a valid IPv6 address, or it is refused; the test runs under a
wildcard policy and requires a no-port deny for the malformed forms.
A stray bracket such as "echo.internal]" or an empty Host still reached
the bare-host default and was allowed under a wildcard rule before the
dial failed. The default now applies only to a nonempty authority that
contains no colon and no bracket; the wildcard-policy test gains rows
for the stray bracket and the empty host, each a deny with no port.
@CMGS
CMGS merged commit 2da4865 into main Sep 14, 2026
2 checks passed
@CMGS
CMGS deleted the review/post-lane branch September 14, 2026 07:01
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.

1 participant