The batch-1 closing task: nine cross-cutting integration tests
(tests/core-integration.rs) pinning the whole protocol core end to
end over the channels harness, plus the architecture-sync docs pass.
Tests:
- One registration, all three commands: CONNECT + ASSOCIATE + BIND
interleaved live on the one channels/socks5/sub op; a teardown
ends only its own session (mux/demux isolation) — the POC #2/#5
regression guard, complete.
- Concurrency: nine concurrent mixed-command sessions (raw topology)
and six through the typed consumer sessions (consumer-side
topology) — the mux/demux pinned in both channel-id allocation
directions; fresh round-trips after the burst; no channel leaks.
- Teardown matrix (wire.md §Sentinels and lifecycle): CONNECT
half-close, ASSOCIATE EOF, BIND client-gone (a parked accept so
the quiet-wait exit is deterministic) — each followed by fresh
sessions of every command (no connection poisoning).
- Identity/scope matrix (ADR-006 end to end): open-only (CONNECT +
ASSOCIATE serve, BIND refused 0x02 at the command gate), bind-only
fail-closed at the open gate for all commands (the spec drift the
task's phrasing carried, reconciled — ADR-006's open gate requires
socks5:open unconditionally), both-scopes (everything, granted
explicitly); the CF-006 witness asserted per row.
- The readiness contract re-pinned under concurrent load: a
hot-Ok(None) egress across concurrent associations does not starve
the datagram stage's channel branch.
Docs sync: README Current State ("the protocol core is implemented"
+ the landed inventory; the decomposition residue gone); consumer.md
§Wiring reconciled to the landed session signatures; ADR-006
§Consequences states the grant asymmetry explicitly; lib.rs crate
docs describe the landed surface.
Verification: cargo test 302 green; --all-features green; clippy
--all-targets -D warnings clean; fmt --check clean; both wasm checks
clean; cargo doc --no-deps clean. Batch boundary holds: no
local/composed code, the default crate is protocol-only.
88 lines
4.4 KiB
Markdown
88 lines
4.4 KiB
Markdown
# ADR-006: Access-Control Posture — the Open Gate Plus the Command Gate
|
|
|
|
## Status
|
|
|
|
Accepted (2026-09-20, Phase 1; scope gates POC-validated in shape —
|
|
POC #1 (`socks5:open`), POC #5 (`socks5:bind` at command dispatch);
|
|
consequence asymmetry pinned by the integration suite, 2026-09-24)
|
|
|
|
## Context
|
|
|
|
SOCKS5 is arbitrary-egress by nature — the open gate plus the target
|
|
policy are the security boundary (AGENTS.md convention 12).
|
|
alktunnels resolved the sibling posture (its ADR-006): whatever ACL
|
|
governs the resource governs everything reachable through it, plus
|
|
whatever policy the SOCKS5 implementation itself applies downstream.
|
|
For SOCKS5, BIND adds an *inbound* capability — the accept-side twin
|
|
of arbitrary egress — so the open gate must distinguish it
|
|
(OQ-SK-08's security note): an identity with proxy egress must not
|
|
implicitly get ingress listeners. POC #5 validated exactly this
|
|
distinction at command dispatch from the per-call opener identity.
|
|
|
|
## Decision
|
|
|
|
- **The open gate is the boundary.** The open op's
|
|
`AccessControl.required_scopes` is `["socks5:open"]`
|
|
(`SOCKS5_OPEN_SCOPE`) — scope-gated via
|
|
`ChannelCore::register_openable` (the registry runs the ACL
|
|
before the open handler). Scope-string convention:
|
|
`<alpn-family>:<capability>` (`socks5:open`, `socks5:bind`) —
|
|
following alktty's `TTY_OPEN_SCOPE` / alktunnels'
|
|
`tunnel:open`/`tunnel:direct` shape. Stable once published.
|
|
- **The command gate: BIND requires `socks5:bind` at command
|
|
dispatch.** The open op carries the session-level gate; the
|
|
command-class distinction happens in-stream, at command read,
|
|
from the per-call opener identity (CF-005/CF-006): CONNECT and
|
|
UDP ASSOCIATE require only `socks5:open`; TCP BIND requires
|
|
`socks5:bind`. An identity without it gets the RFC-faithful
|
|
`ConnectionNotAllowed` (0x02) refusal while CONNECT still serves
|
|
the same identity (POC #5 test 4).
|
|
- **Target policy is the dialer's concern.** No target allowlists
|
|
in the base crate — the dial callback refuses (per-target refusal
|
|
is an RFC reply code; ADR-004's seams). The dialer-refuses shape
|
|
avoids a second policy layer (alktunnels OQ-TN-08's resolution
|
|
pattern, evidence-backed). Whatever ACL governs the socks5
|
|
resource governs everything reachable through it, plus whatever
|
|
the dial callback enforces.
|
|
- **Per-target ACL for the composed egress comes free**: each
|
|
destination rides its own alktunnels direct-op channel governed
|
|
by the `tunnel:direct` grant's ACL (POC #2 validated); the dial
|
|
callback governs per-destination checks.
|
|
- **Ownership**: optionally consult `OwnershipProvider` for
|
|
resource ownership (alknet ADR-050's seam) — a registration-time
|
|
choice, not a protocol concern.
|
|
- **Hub relaying**: the hub's terminate-and-re-produce applies per
|
|
hop (alkcall ADR-051's `ChannelRelay`/`HubLegTemplate`); the hub's
|
|
ACL governs the final dial — the hub is the network boundary. No
|
|
socks5-specific relay work.
|
|
|
|
## Consequences
|
|
|
|
- Two grants, clean separation: `socks5:open` (egress) and
|
|
`socks5:bind` (ingress listeners) — never implied by each other
|
|
(the alktunnels ADR-006 Amendment 1 posture: a capability on its
|
|
own scope). The implication asymmetry is pinned by the integration
|
|
suite (`tests/core-integration.rs`, the identity/scope matrix):
|
|
`socks5:open` alone serves CONNECT + ASSOCIATE and is refused BIND
|
|
(0x02) at the command gate; `socks5:bind` alone grants *nothing*
|
|
on the op — the open gate's `required_scopes = ["socks5:open"]`
|
|
fails closed for every command (the establisher never runs, no
|
|
phantom channel); both scopes, granted explicitly, serve
|
|
everything. A BIND-capable deployment grants both.
|
|
- BIND's security boundary is enforced even though BIND rides the
|
|
same op (ADR-001): the command gate is the mechanism, and it is
|
|
POC-validated (POC #5).
|
|
- The allowlist-hook variant (a declarative per-target policy at
|
|
registration) is *not* shipped in v1 — the dial callback is the
|
|
policy point; see OQ-SK-05's residue (OQ-SK2-07 in the tracker)
|
|
for the virtual-subnet translation-layer proposal, which is the
|
|
same dial-callback shape.
|
|
|
|
## References
|
|
|
|
- OQ-SK-05 (target policy), OQ-SK-08 (the BIND security note)
|
|
- POC #1 (the `socks5:open` gate; the identity seam), POC #5
|
|
(the `socks5:bind` command gate), POC #2 (composed per-target ACL)
|
|
- alktunnels ADR-006 + Amendment 1 (the sibling posture),
|
|
alknet ADR-024 (registry layering), alknet ADR-050 (ownership),
|
|
alkcall ADR-042 / ADR-051 (hub relay) |