Skip to content

feat(pairing): dial loopback relays by handle (E4) + close MCP poisoned-card gap#358

Merged
laulpogan merged 2 commits into
mainfrom
e4-clean
Jun 30, 2026
Merged

feat(pairing): dial loopback relays by handle (E4) + close MCP poisoned-card gap#358
laulpogan merged 2 commits into
mainfrom
e4-clean

Conversation

@laulpogan

Copy link
Copy Markdown
Collaborator

What

wire dial nick@127.0.0.1:8771 now reaches a local-dev / sandbox relay. The federation-handle domain validator rejected any :port, so a custom-port loopback relay couldn't be named in a handle at all — the E4 item in docs/V0_13_2_PLATFORM_HARDENING.md. (Builds on #357; the invite flow remains the path for non-loopback custom-port relays.)

Change

  • One shared endpoints::is_loopback_host predicate (exact 127.0.0.1/localhost) now keys every E4 gate — the validator, the URL scheme builder, infer_scope_from_url, and the phishing-warning suppression — so scheme and scope can't drift.
  • is_valid_domain accepts a host:port authority only for a loopback host (port 1..=65535). Non-loopback host+port stays rejected.
  • New relay_url_for_domainhttp:// for loopback, https:// otherwise, replacing all 4 format!("https://{domain}") fallback sites (incl. wire up --relay 127.0.0.1:PORT).
  • is_known_relay_domain treats loopback as known — no spurious cross-relay phishing warning for the operator's own machine.

Security (from the adversarial design review — 2 reviewers, gate-1 + gate-2)

  • MCP tool_add now runs the CLI's poisoned-card DID-key fingerprint hard-refuse — a rogue relay serving a card whose key ≠ its claimed DID is rejected on both the CLI and MCP paths (was CLI-only; E4's loopback path made the MCP gap newly reachable).
  • IPv6 ::1 handles intentionally rejected (would need [::1]:port bracketing the handle path doesn't carry) rather than half-accepted into a malformed URL.
  • THREAT_MODEL T14 documents the loopback-SSRF trade-off; bilateral wire_accept + the fingerprint refuse remain the gates.

Tests

Loopback parse / non-loopback reject / port-range / empty-host / IPv6-reject unit tests + a relay_url_for_domain public-path regression guard. In-situ: wire dial nick@127.0.0.1:PORT → bilateral pair → message round-trip delivered + verified over http://127.0.0.1:PORT (the exact path that errored "domain invalid" before). Full lib suite green (615); pairing e2e (handle_pair + bilateral) green.

🤖 Generated with Claude Code

laulpogan and others added 2 commits June 29, 2026 23:16
…ed-card gap

`wire dial nick@127.0.0.1:8771` now reaches a local-dev / sandbox relay. The
federation-handle domain validator rejected any `:port`, so a custom-port
loopback relay couldn't be named in a handle at all (the V0_13_2 E4 item).

- `endpoints::is_loopback_host` — one shared predicate (exact `127.0.0.1` /
  `localhost` / `::1`) now used by `infer_scope_from_url`, the validator, the
  URL builder, and the phishing-warning suppression, so scheme and scope
  classification can't drift.
- `is_valid_domain` accepts a `host:port` authority ONLY for a loopback host
  (port 1..=65535). Non-loopback host+port stays rejected — public handles are
  port-less; a public relay on a custom port belongs behind a 443 TLS edge.
- new `relay_url_for_domain` picks `http://` for a loopback authority, `https://`
  otherwise, replacing the 4 unconditional `format!("https://{domain}")` fallback
  sites (pair_profile, cli/pairing, cli/setup `wire up --relay`, mcp tool_add).
- `is_known_relay_domain` treats loopback as known — no spurious cross-relay
  phishing warning for the operator's own machine.

Security (from the adversarial design review):
- MCP `tool_add` now runs the same poisoned-card DID-key fingerprint hard-refuse
  the CLI `cmd_add` had — a rogue relay serving a card whose key != its claimed
  DID is rejected on BOTH paths (was CLI-only; E4's loopback path made the MCP
  gap newly reachable).
- THREAT_MODEL T14 documents the loopback-SSRF trade-off (prompt-injected dial to
  a loopback port); bilateral accept + the fingerprint refuse remain the gates.

Tests: loopback parse / non-loopback reject / port-range unit tests +
relay_url_for_domain scheme regression guard; in-situ `wire dial nick@127.0.0.1:PORT`
pair + message round-trip over http loopback. Full lib suite green (615).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…oc, +tests

Built-thing review of the E4 change (3 MINOR, no blockers):
- Exclude IPv6 `::1` from `is_loopback_host`: it was accepted by the validator
  but `relay_url_for_domain` produced a bracket-less, malformed `http://::1:port`.
  IPv6 loopback handles aren't a real use case and need `[::1]:port` bracketing
  the handle path doesn't carry — reject cleanly instead. (`::1` was vestigial in
  `infer_scope_from_url` anyway: its host-extraction never yields a bare `::1`.)
- Correct the `is_loopback_host` doc: it's the single predicate for the E4
  TRUST-PATH gates, not literally every loopback check — `session.rs::url_is_loopback`
  is a separate, deliberately-broader /8 predicate for same-box session discovery.
- Tests: reject `bob@:8771` (empty host) and `bob@::1:8771` (IPv6 exclusion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying wireup-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: 13888e5
Status: ✅  Deploy successful!
Preview URL: https://a62e7705.wireup-landing.pages.dev
Branch Preview URL: https://e4-clean.wireup-landing.pages.dev

View logs

@laulpogan laulpogan merged commit e5bfb6c into main Jun 30, 2026
13 checks passed
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