Skip to content

fleet-proxy(broker): a present-but-invalid request-frame port silently reroutes to the default 4799 service — 70000, -1, 4801.5 and "4801" are all served by laptop-agent instead of refused #6516

Open

Description

@dezren39

Summary

The fleet session broker's request-frame parser silently reroutes a request to the default laptop-agent port (4799) whenever the frame's port field is present but not a valid u16 — out of range, negative, a float, or a string. A request addressed somewhere else (or nowhere valid) is delivered to a service it was never addressed to, with a 200, instead of being refused. A present-but-invalid port is not an absent port; only an absent one should take the default.

Evidence

pkgs/fleet/crates/fleet-proxy/src/serve.rs (at 540d3bea3f):

  • parse_request (:610; the port chain at :636-639): let port = frame.get("port").and_then(|v| v.as_u64()).and_then(|p| u16::try_from(p).ok()); — every failure mode of that chain (70000, -1, 4801.5, "4801") collapses to None, indistinguishable from the field being absent.
  • Broker::do_request (:507; the default at :511): let port = req.port.unwrap_or(fleet_config::PORT_LAPTOP_AGENT); — the None becomes port 4799, and the request (method, path, body, and the caller's token file) is forwarded there.
  • Contrast in the same function: method and path are strictly validated and rejected when malformed (fleet-proxy(broker): method/path CRLF injection — one frame smuggles a second request onto the pooled tunnel #5750), and port: 0 — which survives the chain as Some(0) — is dialled and fails. The invalid cases are not even consistent: 0 errors, 70000 silently reroutes.

The module's own contract says the lenient parsing "mirrors the Python's dict access" for defaults — the Python takes req.get("port", 4799) only when the key is absent; a present 70000 is used and the connect fails. The broker's hello also advertises a closed routing table (services_map, ports 4799/4800/4801 per fleet-config) with the proto rule that an unadvertised port is "not here" — a frame for port 70000 being served by 4799 breaks that contract from inside.

Verified (probe, laptop, real crates)

Probe ~/git/fleet/probe-broker-port (path-deps on the real fleet-proxy/fleet-config/fleet-proxy-proto crates at 540d3bea3f): stub HTTP services on 127.0.0.1:4799 and 127.0.0.1:4801 that answer with their own port; the real broker (serve::run, direct mode, temp socket/token) in front of them; legacy request frames over the broker socket:

  • port: 4801 → 200, served by 4801 (control)
  • port absent → 200, served by 4799 (control — the documented default)
  • port: 70000 → 200, served by 4799 (bug)
  • port: "4801" (string) → 200, served by 4799, not 4801 (bug)
  • port: -1 → 200, served by 4799 (bug)
  • port: 4801.5 → 200, served by 4799 (bug)
  • port: 0 → 502 broker: tunnel to 127.0.0.1:0: Connection refused (dial failure — the inconsistent contrast)

Why it matters

The broker is the fleet's local RPC router: a frame's port is its address. Silently delivering a request to the wrong service is worse than failing it — the wrong service acts on a request it was never sent (the laptop-agent daemon executes commands), the caller sees a plausible 200 from a service it did not address, and the misaddressing is invisible in logs. Client bugs that serialize a port as a string or compute an out-of-range port become silent misroutes instead of loud errors at the frame boundary, exactly where #5750 already established strict validation for method/path.

Acceptance criteria

  • parse_request distinguishes an absent port (→ default 4799) from a present-but-invalid one (non-integer, negative, float, string, or > 65535), and the broker answers the latter with the standard {status: 502, body: {error: "broker: …"}} frame naming the bad port value's shape — no connection is opened.
  • port: 0 gets the same frame-boundary refusal instead of a dial attempt (or is explicitly documented and tested as a dial error, consistently with the other invalid values).
  • Tests in fleet-proxy pin: valid port routes to that port, absent port routes to 4799, and each invalid shape (70000, -1, 4801.5, "4801", 0 per the chosen rule) is refused without touching any service.
  • Existing fleet-proxy tests keep passing (cargo test -p fleet-proxy on the laptop or CI).

Scope notes

Activity

  1. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    app/fleetpkgs/fleet multicall binary + platform crates
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  3. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, slug new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b run 2026-11-06 06:50 CST
    • Found via: filed this run
    • Task: Filed after verifying with a probe on the laptop (~/git/fleet/probe-broker-port, real fleet-proxy/fleet-config/fleet-proxy-proto crates @540d3bea3f): stub services on 127.0.0.1:4799 and 127.0.0.1:4801 behind the real broker — frames with port 70000, "4801", -1, and 4801.5 were all answered 200 by the 4799 service; controls (port 4801, port absent) routed correctly; port 0 502'd at dial. Triage: refuse present-but-invalid ports at the frame boundary in parse_request (serve.rs:636-639), keeping the absent-port default.
    • Expectation: effort s — done looks like: parse_request distinguishes absent from present-but-invalid port, invalid shapes get the standard 502 broker error frame with no connection opened, and fleet-proxy tests pin all shapes.
    • Links: issue body carries the full probe table and acceptance criteria; queue packet follows in this run.
  4. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  5. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  6. 24 remaining items

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 6, 2026
  8. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  10. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design-architect pass 62 — adopted as Phase 1 of design #6582

  11. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  12. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/fleetpkgs/fleet multicall binary + platform crateskind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions