- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
4.1 KiB
ADR-012: Target Policy and the Virtual-Subnet Translation Layer
Status
Accepted (2026-09-20, Phase 1; the dialer-refuses shape is OQ-SK-05's evidence-backed resolution (see ADR-006), and the 2026-09-14 virtual-subnet proposal is resolved as assembly-layer policy)
Context
OQ-SK-05's residual Phase 1 questions: (a) does the base crate ship a per-target allowlist hook, or is target policy entirely the dialer's concern; (b) domain-form targets (RFC 1928 ATYP 0x03): resolve producer-side, refuse, or pass through unresolved; (c) the 2026-09-14 virtual-subnet proposal (identity-scoped subnets so proxied resources become IP-addressable ("BND.ADDR ↔ channel id" translation, a hub exposing proxied resources per identity under ACL).
The proposal's key property: the mechanism needs no wire change;
DST.ADDR and BND.ADDR are arbitrary RFC fields, so virtual
addresses ride ordinary CONNECT/BIND requests; the translation layer
is the dial callback (dial(target) matches a virtual subnet
against a per-identity resource table).
Decision
- No allowlist hook in v1 (ADR-006's resolution, restated as the target-policy half): the dial callback is the policy point; per-target refusal is an RFC reply code. The declarative-allowlist variant remains an additive future extension if a deployment needs declarative policy; the evidence-backed default is dialer-refuses.
- Domain-form targets: pass through unresolved to the dialer.
Resolution is the egress's/dialer's concern (POC #2 resolved the
local-egress half: "the egress owns resolution" (the ssh
-Dsemantic). The protocol layer never resolves: a wasm producer may have no resolver at all, and the composed variant's targets ride the alktunnels direct op (producer-side resolution there, per alktunnels ADR-007's own posture). fast-socks5'sdns_resolveconfig (and itsresolve_dnsstep) is not part of the extracted vendor region at all; the crate's producer never resolves (ADR-006/ADR-012), so the extraction drops both. - The virtual-subnet translation layer is assembly-layer policy,
not a crate feature. The crate ships no translation-table
helper in v1: the dial callback is the seam, and a deployment
wanting identity-scoped virtual subnets implements the
subnet→resource table there (per-identity scoping = resources +
ACL + target policy; hub exposure = the terminate-and-re-produce
story; no new wire vocabulary). The full-tunnel composition (the
host routes the subnet into the proxy via tun2proxy) is where a
virtual subnet becomes routable from vanilla apps:
connect(10.x.y.z:p)is an ordinary CONNECT the dialer translates. tun2proxy's virtual-network/virtual-DNS support is the prior art to check if and when a deployment builds this; the crate's obligation is only that the seams support it, which ADR-004's shape already does.
Consequences
- The security boundary stays exactly two mechanisms: the open
gate (
socks5:open/socks5:bind) and the dial/accept callbacks. No third policy layer. - The virtual-subnet endgame is unblocked without crate work: the translation layer rides the dial callback; if a deployment materializes with concrete needs, a translation-table helper can be added additively (assembly-layer code, possibly later graduated into the crate behind a feature); the seams don't change.
- DNS posture is uniform: producer-side resolution everywhere (local egress resolves; composed egress hands the name to the direct op which resolves at the tunnel producer).
References
- OQ-SK-05 (the full question set + the proposal's record), OQ-SK2-07 (the deferred helper question this ADR's resolution feeds), OQ-SK-01 (the dial callback), OQ-SK-09 (the multiplexing rejection; the virtual-subnet idea that survives is recorded in the tracker)
- POC #2 (
poc-udp-associate-findings.md: the egress-owns-DNS resolution), ADR-004 (the seams), ADR-006 (the ACL posture), ADR-008 (the provider defaults) - alktunnels phase-0-findings OQ-TN-08 (the target-policy
dissolution precedent); tun2proxy (
/workspace/tun2proxy, the virtual-network prior art + the endgame composition partner)