Skip to content

Add ts dev proxy design spec and implementation plan#798

Draft
aram356 wants to merge 37 commits into
mainfrom
worktree-review-ts-dev-proxy-spec
Draft

Add ts dev proxy design spec and implementation plan#798
aram356 wants to merge 37 commits into
mainfrom
worktree-review-ts-dev-proxy-spec

Conversation

@aram356

@aram356 aram356 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the design spec and implementation plan for ts dev proxy — a local TLS-terminating (MITM) developer proxy that serves a production publisher hostname from a dev/staging upstream by swapping the TLS SNI, so a real browser shows the production domain while a Compute/staging service answers.

This PR is docs-only (no code yet): a reviewed spec plus a task-by-task TDD plan ready for implementation.

  • docs/superpowers/specs/2026-06-22-ts-dev-proxy-design.md — technical spec
  • docs/superpowers/plans/2026-06-22-ts-dev-proxy.md — 8-phase implementation plan

Spec highlights

  • CONNECT MITM with per-machine local CA (trust once); unmatched hosts are blind-tunneled, matched hosts get a minted leaf with SNI→TO.
  • Default Host = FROM (preserve the production host) because Trusted Server core anchors URL rewriting to the inbound Host; --rewrite-host opts into Host = TO.
  • Loopback-only bind by default; --allow-non-loopback disables blind tunnel/forward so it can't become an open proxy.
  • HTTPS-only browser proxying (Chrome/Firefox/Safari), PAC served as a first-class local route, Safari active-service detection.

Plan shape

Crate skeleton (excluded from the wasm workspace, native target) → rewrite core → local CA → CONNECT/MITM server → header/auth polish → browser/PAC orchestration → config inference → docs. Each task ends in an independently testable deliverable; pure-logic tasks carry full TDD code.

Notes

  • The spec and plan went through several review rounds; resolved items are recorded in-document (spec design-decisions table; plan self-review "review round" notes).
  • Implementation has not started — this is the planning artifact for that work.

aram356 added 30 commits June 22, 2026 13:21
- Compare r.from case-insensitively in RuleTable::first_match to enforce the
  lowercase invariant regardless of how Rule.from was built
- Reject trailing-colon inputs (empty port string) as RuleError::Port in
  Authority::parse; add rejects_empty_or_missing_port test
- Assert scheme_is_tls in rewrite_default_preserves_from_host_and_sets_sni_to_to
  and rewrite_host_uses_to_authority_with_port
Add ConfigError::BasicAuthFile variant with a path-carrying display message
so file-not-found and permission errors are no longer reported as the
misleading "--basic-auth must be USER:PASS" format error. The read failure
now maps to BasicAuthFile; only parse failures map to BasicAuth. Includes
a unit test that asserts the correct variant is returned for a missing file.
Replace the write-then-chmod pattern in CertAuthority::persist() with a
single OpenOptions::create_new(true).mode(0o600) open, eliminating the
window where the private key was briefly world/group-readable on disk.
…NNECT

- Thread the raw buffered bytes through `RequestHead` so `blind_forward_http`
  can write the complete request head to the upstream before piping the rest
  of the socket bidirectionally (spec §8.4). Previously the head was discarded,
  sending a truncated/empty request.

- Update `blind_forward_http` doc comment to reflect that it now replays the
  original head rather than falsely claiming it always did.

- Add `unmatched_connect_off_loopback_is_refused_with_403` integration test.
  The proxy listener is bound on `127.0.0.1:0` (real socket) but
  `cfg.listen` is patched to `0.0.0.0:<port>` before being handed to
  `serve_on`, so `is_loopback` is computed as false while the test can
  still connect via loopback. Asserts that an unmatched `CONNECT` receives
  `403` and no tunnel is established.
Replace the dead `std::thread::park()` restore thread in `launch_safari`
with a file-based persist-and-recover scheme.  Before applying the PAC
URL, write `<ca_dir>/safari-proxy-restore` capturing the network service
name and the prior auto-proxy URL (or an empty second line when
auto-proxy was off).  Add `restore_system_proxy_if_pending(ca_dir)`,
which reads and deletes that file then runs the appropriate `networksetup`
command to put things back.

Wire it into `run()` in two places: at startup (crash recovery from a
previous hard-killed run) and in a `tokio::select!` on `ctrl_c()` (clean
exit).  Also move the function-local `use std::time::{SystemTime,
UNIX_EPOCH}` out of `make_temp_dir` to the top-level imports per project
convention.
…-spec' into worktree-review-ts-dev-proxy-spec
@aram356 aram356 self-assigned this Jun 23, 2026
aram356 added 6 commits June 23, 2026 10:48
…FROM validation

- Abort `ca regenerate` when the old CA's keychain revocation can't be
  confirmed, so on-disk key material never outlives its OS trust.
- Declare the CLI macOS-only via `compile_error!` on non-macOS targets
  (keychain, Safari, and networksetup are all macOS-specific), and gate the
  macOS-only helpers (`manual_restore_command`, `restore_auto_proxy`) while
  ungating `shell_quote` so the shared launch path compiles cleanly.
- Shell-quote the keychain, cert, and profile paths in the `ca install` and
  Firefox `certutil` fallback instructions.
- Validate rule FROM as a bare hostname before embedding it in the generated
  PAC, browser URL, and upstream Host header.
- Ignore the workspace-excluded crate's `target/` directory.

Spec, plan, and guide updated to match.
The macOS-only `compile_error!` lived inside the proxy module while all native
deps stayed unconditional, so a build for the repo-default wasm32-wasip1 target
failed first in tokio/ring/aws-lc-sys instead of with the intended "macOS only"
error — an easy developer footgun (a plain `cargo check` in the crate inherits
the wasm default from .cargo/config.toml).

- Move every dependency (and dev-dependency) under
  `[target.'cfg(target_os = "macos")'.dependencies]` so unsupported targets
  build none of the native TLS/networking stack.
- Lift the platform gate to the crate root: `compile_error!` in lib.rs and
  `#[cfg(target_os = "macos")]` on the command modules, the CLI types, and the
  binary entry point. The proxy module's own `compile_error!` is removed.
- Gate the e2e test crate to macOS (its deps are now macOS-scoped).

Now `cargo check --target wasm32-wasip1` emits exactly one error — the macOS-only
message — with no dependency build attempts. Native build/test unchanged
(31 unit + 6 e2e pass). Spec and plan updated to match.
Let `--to` target a bare IP and `--rewrite-host` carry the hostname that
endpoint expects. The flag now takes an optional value:

- omitted        -> Host = FROM (default; unchanged)
- --rewrite-host -> Host = TO host (the prior bare-flag behavior)
- --rewrite-host <HOST> -> Host = <HOST> and TLS SNI = <HOST>

Connection still dials `--to`, so pointing at an IP works: the proxy presents
the explicit hostname for both SNI and Host while the socket goes to the IP.

Replaces `Rule.preserve_host: bool` with a `HostMode { PreserveFrom, UseTo,
Explicit }` enum threaded through `rewrite_for`; the explicit host is validated
as a hostname (new `ConfigError::InvalidRewriteHost`). Spec, plan, and guide
updated; adds tests for all three forms plus invalid-value rejection.
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