Skip to content

Control the host Apollo's USB forwarding from the dashboard - #135

Merged
vibesoftwarecoder merged 1 commit into
masterfrom
feat/host-usb-control
Oct 28, 2026
Merged

vibesoftwarecoder merged 1 commit into
masterfrom
feat/host-usb-control

Conversation

@vibesoftwarecoder

@vibesoftwarecoder vibesoftwarecoder commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Seventh step of USB forwarding, on the MultiSeat side. The host card gets a USB forwarding panel that controls the console Apollo's USB tunnel through the control pipe ApolloVibe added in the earlier steps. Nothing changes for anyone who does not tick "USB Forwarding Control Pipe" in Apollo's own Network tab.

What the panel does

  • Shows whether the tunnel is on, what is forwarded now, and who is paired.
  • Turns the tunnel on or off, blocks a client's USB forwarding, and allows it again.
  • MultiSeat never edits that Apollo's config. A switch made here lasts until that Apollo restarts, and the panel says so.

Who may use it
The API has no key on a host that chose that, and every seat user can reach it on loopback. The pipe's access list cannot help, because the SYSTEM service is the legitimate client. So the whole /api/host/usb group, reading included (the status names paired clients and the devices they forward), is served only to the person at the console. A filter on the group covers every route now or later. A refusal is a fixed 403 console_session_required, and the request never reaches the pipe.

The check

  • The request's loopback source port names the owner process in the system TCP table, with the time the connection was created.
  • The service holds a handle to that process for the whole check, so its id cannot be given to another process meanwhile. A process created after the connection cannot be the one that made it, and a zero timestamp is refused.
  • The table is read again once the handle is held, and must name the same owner and the same console session.
  • The process must run in the console session. Its user must be the user logged on there, not SYSTEM or another service account, and not one of MultiSeat's seat accounts. A seat user who moves their own session to the console is still refused.
  • Every doubt refuses. Refusals are logged at most once a minute per outcome, because the dashboard polls.

What it does not stop: another program of the console user, which is the trust the console user already has over the host. A dashboard on another PC, or in a seat, cannot see or change USB forwarding, and its USB card says so.

The pipe client
Follows Apollo's pipe contract: identification-level open, owner SYSTEM or Administrators, server process is the one listening on Apollo's main port, bounded I/O, and a close with no reply is reported as busy. It sends nothing until those hold.

Docs
docs/security-posture.md (new section), README and CLAUDE.md.

Tests

  • 1927 passed, 0 failed, 18 skipped. Dashboard 82 of 82, and tsc is clean.
  • Each protection above was removed on purpose, one at a time, and the matching tests failed: the creation-time check, the seat-account refusal, the user comparison, the second table read, and the group gate.
  • A test reads the route table itself and asserts every route under /api/host/usb is refused to an outsider, so a route added later is covered.

Not tested yet

  • A real browser on the console against one in a seat. The real-rule tests use this process as the caller, not a seat session.
  • The tscon case (a seat user moving their session to the console) is covered by a unit test of the rule, not measured on a live seat.
  • Nothing is deployed. The first live use also needs the host owner's steps on the host: tick the control pipe in Apollo and restart it.

Refs the USB forwarding series (#133 and the ApolloVibe and MoonlightVibe steps before it).

🤖 Generated with Claude Code

The host card gets a USB forwarding panel: switch the host Apollo's tunnel
on or off, see what is forwarded, block a client or allow it again. The
service talks to Apollo's local control pipe and never edits Apollo's
config. A switch lasts until that Apollo restarts.

Because the API has no key on a host that chose that, and every seat user
can reach it on loopback, the whole /api/host/usb group, reading included
(the status names paired clients and their devices), is served only to the
person at the console. The check:
- finds the owner of the request's loopback connection in the system TCP
  table and holds a handle to that process for the whole check;
- refuses a process created after the connection (a reused process id),
  and a zero timestamp;
- reads the table twice and requires the same owner and console session;
- requires the console session and the user logged on there, and refuses
  service accounts and MultiSeat's seat accounts, so a seat user who moves
  their session to the console is still refused.
Every doubt refuses. Refusals are logged once a minute per outcome.

The pipe client follows Apollo's pipe contract: identification-level open,
owner SYSTEM or Administrators, server process = the listener of Apollo's
main port, bounded I/O.

Tests: 1927 passed, 0 failed, 18 skipped; dashboard 82/82. Each protection
was removed on purpose and the matching tests failed. Not yet tried against
a real browser on the console versus a seat, and not deployed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vibesoftwarecoder
vibesoftwarecoder merged commit cedf52f into master Oct 28, 2026
3 checks passed
@vibesoftwarecoder
vibesoftwarecoder deleted the feat/host-usb-control branch October 28, 2026 14:30
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