tests/vendor-differential.rs pins the vendored extraction's RFC edge behavior as explicit bytes, per vendor.md §Maintenance Posture (the differential-testing contract; ADR-011): - negotiation: empty/zero method lists -> 0xff, server-order method preference, 0x04 greeting refusal before the methods byte, EOF mid-handshake typed error, password sub-protocol EmptyUsername/ EmptyPassword + the 0x01/0xff-then-error reject path - command reads: unknown command -> 0x07 reply, RSV != 0 tolerance (upstream reads it unchecked), ATYP 0x02/0x05 -> 0x08, the 255-byte domain wire maximum, truncated v4/domain address errors - reply forms (new_reply regression guard): 10/22-byte shapes, constant VER/RSV across codes, zero-addr error replies, caller sock_addr verbatim on success - client session: per-code ReplyError mapping (incl. the 0x06 double map), domain request encoding, UDP-ASSOCIATE zero-target fallback form, skip_auth emits no greeting - UDP codec: GarbageInReserved, FRAG passthrough (the parser returns it; refusing is the caller's job), v6 round-trip, payload split - transfer/wait_on_tcp: pump both directions to completion, EOF on clean close, UnexpectedUdpControlGarbage carries the byte Every non-obvious expectation names the upstream behavior it pins (file + line in /workspace/fast-socks5, v1.0.0); the reference is consulted, never linked — no build-time dependency. Duplex pairs only (no TcpListener/UdpSocket/tokio::net in test code). Verification: cargo test (37 unit + 32 differential), cargo test --all-features (identical), cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo check --target wasm32-unknown-unknown — all clean. Task: tasks/implementation/vendor-differential-tests.md (status -> completed).
159 lines
8.1 KiB
Markdown
159 lines
8.1 KiB
Markdown
---
|
|
id: implementation/vendor-differential-tests
|
|
name: Vendor — the differential RFC edge-case test suite
|
|
status: completed
|
|
depends_on: [implementation/vendor-server-typestate, implementation/vendor-client-session]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
Build the crate's RFC edge-case test suite for the extracted vendor —
|
|
the differential-testing posture vendor.md pins: "the crate's test
|
|
suite covers the edge cases the extracted code must keep passing; when
|
|
in doubt, diff behavior against the reference checkout." The per-task
|
|
unit tests (wire vocabulary, typestate, client session) cover the
|
|
happy paths; this task covers the RFC edge cases and the
|
|
extraction-fidelity checks, using `/workspace/fast-socks5` as the
|
|
behavioral reference (read-only — tests never depend on it at build
|
|
time; the reference is consulted while writing the tests, and the
|
|
assertions pin the expected bytes).
|
|
|
|
Test infrastructure: tokio duplex streams (`tokio::io::duplex`) —
|
|
no sockets, no `local` feature, runs in the default wasm-clean crate's
|
|
test suite (tests are native-only via dev-deps; the crate itself stays
|
|
wasm-clean). A first-party test harness drives the vendored typestate
|
|
server against the vendored client session over duplex pairs.
|
|
|
|
**Edge-case coverage** (each pinned with explicit expected bytes where
|
|
the wire form is the point):
|
|
|
|
1. **Negotiation edge cases**: method-list parsing (empty method list,
|
|
zero methods offered → 0xff), method-order preference (server picks
|
|
its first match, not the client's first offer), version mismatch
|
|
(0x04 greeting → refusal), truncated greeting (EOF mid-handshake →
|
|
typed error, no hang), password-auth sub-protocol (empty username →
|
|
`EmptyUsername`, empty password → `EmptyPassword`, reject path →
|
|
0x01/0xff reply then error).
|
|
2. **Command-read edge cases**: unknown command byte (0x04+ →
|
|
`UnknownCommand` → 0x07 reply), reserved-byte tolerance (RSV ≠ 0 —
|
|
upstream reads it without checking; pin the upstream behavior),
|
|
address-type edge cases (ATYP 0x02/0x05+ → `IncorrectAddressType`
|
|
→ 0x08 reply), domain-length boundary (255-byte domain OK,
|
|
256+ rejected at encode time by `to_be_bytes`), truncated address
|
|
fields (EOF mid-address → typed error).
|
|
3. **Reply-form edge cases** (the `new_reply` carried change's
|
|
regression guard): v4 reply = 10 bytes, v6 reply = 22 bytes,
|
|
VER=0x05/RSV=0x00 in every reply, error replies carry
|
|
`0.0.0.0:0`-equivalent (ATYP=1, zero addr+port), success replies
|
|
carry the caller's `sock_addr` verbatim.
|
|
4. **Client-session edge cases**: reply#1 with a non-zero reply code →
|
|
the mapped `ReplyError` (the `from_u8` table), domain-target
|
|
request encoding (ATYP=3, len-prefixed), the UDP-ASSOCIATE
|
|
zero-target fallback (upstream `request_header`'s
|
|
`target_addr: None` branch — pin the 10-byte zero form),
|
|
`skip_auth` behavior (no method negotiation bytes on the wire).
|
|
5. **UDP codec edge cases**: `parse_udp_request` with garbage RSV →
|
|
`GarbageInReserved`, FRAG ≠ 0 passthrough (the parser returns frag;
|
|
the *caller* refuses — pin the parser's contract),
|
|
`new_udp_header`/`parse_udp_request` round-trip for v4/v6/domain.
|
|
6. **`transfer`/`wait_on_tcp` semantics**: `transfer` pumps both
|
|
directions to completion over duplexes (including half-close
|
|
propagation); `wait_on_tcp` returns `EOF` on clean close,
|
|
`UnexpectedUdpControlGarbage` on a control byte.
|
|
|
|
**Differential method**: for each behavior above, consult the
|
|
reference checkout's behavior (its own tests + code reading) and pin
|
|
the *same* expectation in-tree, noting the upstream source line in the
|
|
test's doc comment where the expectation is non-obvious. Where the
|
|
extraction deliberately diverges (the pruned error variants, the
|
|
published `new_reply`), the test pins the *in-tree* contract and the
|
|
divergence is already recorded in vendor.md — no new divergence may be
|
|
introduced by this task.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] The six coverage groups above each have at least one test;
|
|
all pass in `cargo test`
|
|
- [x] Tests use only duplex/in-memory streams — no `TcpListener`/
|
|
`UdpSocket`/`tokio::net` in test code (the suite runs in the
|
|
default-feature build)
|
|
- [x] Every non-obvious expectation carries a doc-comment note naming
|
|
the upstream behavior it pins (file + line in
|
|
`/workspace/fast-socks5`)
|
|
- [x] `cargo test` (default features) green;
|
|
`cargo test --all-features` green (no feature-gated vendor code
|
|
exists, so both must pass identically)
|
|
- [x] `cargo check --target wasm32-unknown-unknown` still clean
|
|
(tests are dev-only; the crate surface unchanged)
|
|
- [x] `cargo clippy --all-targets -- -D warnings` and
|
|
`cargo fmt --check` clean
|
|
|
|
## References
|
|
|
|
- docs/architecture/vendor.md §Maintenance Posture (the
|
|
differential-testing contract)
|
|
- docs/architecture/decisions/011-inline-vendor.md (differential
|
|
testing guards the RFC edge cases)
|
|
- docs/architecture/decisions/002-protocol-base.md (faithful
|
|
reply-code mapping)
|
|
- Reference checkout: `/workspace/fast-socks5` (v1.0.0 — behavior
|
|
source for the pinned expectations; its `src/lib.rs` test module and
|
|
`src/server.rs` test module are the upstream coverage to mirror
|
|
where extractable)
|
|
- RFC 1928 (the wire form's authority)
|
|
|
|
## Notes
|
|
|
|
- **Suite location**: `tests/vendor-differential.rs` (an integration
|
|
test — dev-deps only, no crate-surface change). 32 tests over the
|
|
six coverage groups; tokio duplex pairs throughout, no sockets.
|
|
- **Differential method followed**: each non-obvious expectation's
|
|
doc comment names the upstream file + line it pins (e.g.
|
|
`src/server.rs:903-914` for method-order preference,
|
|
`src/client.rs:277-285` for the UDP-ASSOCIATE zero fallback,
|
|
`src/server.rs:1236-1242` for the parser/caller FRAG split). The
|
|
reference checkout was read, not linked — assertions pin bytes.
|
|
- **Client-session private-API edge cases**: two wire behaviors are
|
|
only reachable through private methods (`request_header`'s
|
|
`target_addr: None` zero fallback; the `target_addr` field reset
|
|
the in-module tests use). The integration suite pins the wire form
|
|
byte-for-byte (the 10-byte zero request, verified decodable to
|
|
`0.0.0.0:0` via the vendored `read_address`); the code-path forms
|
|
of those tests already live in `src/vendor/client.rs`'s in-module
|
|
tests (privacy-scoped), written by the client-session task. No new
|
|
divergence introduced — the pin notes cross-reference both.
|
|
- **Duplex half-close shape**: `DuplexStream`'s shutdown closes the
|
|
whole pipe's write side, so the `transfer` half-close-propagation
|
|
group is driven with drops (full EOF) rather than
|
|
`shutdown()`-per-direction; the test's doc comment records why.
|
|
- **First-party-only divergence check**: the only in-tree contracts
|
|
the tests pin beyond upstream behavior are the two recorded
|
|
carried changes/adjustments (`new_reply` published — group 3; the
|
|
typed `MissingTargetAddr` replacing upstream's `anyhow::Error` —
|
|
group 4 note). No new divergence introduced.
|
|
|
|
## Summary
|
|
|
|
Built the vendor's differential RFC edge-case suite per vendor.md
|
|
§Maintenance Posture: `tests/vendor-differential.rs`, 32 tests
|
|
covering the six groups — negotiation edge cases (empty/zero method
|
|
lists → 0xff, server-order preference, 0x04 greeting refusal, EOF
|
|
mid-handshake typed error, password sub-protocol empty
|
|
username/password + the 0x01/0xff-then-error reject path); command
|
|
reads (unknown command → 0x07, RSV≠0 tolerance, ATYP 0x02/0x05 →
|
|
0x08, the 255-byte domain boundary, truncated v4/domain address
|
|
errors); reply forms (10/22-byte shapes, constant VER/RSV, zero-addr
|
|
error replies, caller-`sock_addr`-verbatim success replies); client
|
|
sessions (per-code `ReplyError` mapping incl. the 0x06 double map,
|
|
domain request encoding, UDP-ASSOCIATE zero fallback,
|
|
`skip_auth`'s no-greeting wire shape); UDP codec (garbage RSV, FRAG
|
|
passthrough contract, v6 round-trip, payload split); and
|
|
`transfer`/`wait_on_tcp` (pump-to-completion, EOF on clean close,
|
|
`UnexpectedUdpControlGarbage` carrying the byte). All expectations
|
|
byte-pinned with upstream file+line references; verification green
|
|
(`cargo test` 37+32, `--all-features` identical, clippy/fmt/wasm
|
|
clean). |