11================================================
2- USB Passthrough — Phase 2 Design (DRAFT)
2+ USB Passthrough — Phase 2 Design
33================================================
44
5- .. warning ::
6- **DRAFT — Linux-libusb path complete; cross-platform backends are
7- structural skeletons only. **
5+ .. note ::
6+ **All software-side work is complete; the eight open questions are
7+ resolved (see "Design decisions"). ** Two items remain that cannot
8+ be completed in code: **verification against real USB hardware ** and
9+ the **external human security sign-off (Phase 2e) **. The feature
10+ flag stays default-off until both are done.
811
9- **Shipped (rounds 27 / 34 / 37 / 39 / 40 / 41 / 42): **
12+ **Done and shipped (rounds 27 / 34 / 37 / 39 / 40 / 41 / 42 / 43 ): **
1013 Phase 1 (read-only enumeration), Phase 1.5 (hotplug events),
1114 Phase 2a (protocol + ABCs + ``LibusbBackend `` lifecycle +
1215 ``FakeUsbBackend `` for tests + feature flag, default off),
1316 Phase 2a.1 (full ``LibusbBackend `` transfers + CREDIT-based
1417 inbound flow control + audit hooks),
1518 **viewer-side ``UsbPassthroughClient`` ** (blocking
16- open / control_transfer / bulk_transfer / interrupt_transfer / close
17- with outbound credit waits and shutdown propagation),
19+ open / control_transfer / bulk_transfer / interrupt_transfer / close /
20+ list_devices with outbound credit waits and shutdown propagation),
1821 Phase 2d (``UsbAcl `` persistent allow-list, ACL-gated OPEN with
1922 prompt-callback path, audit-log integration via the existing
20- tamper-evident chain).
23+ tamper-evident chain), Phase 2d.1 (ACL file HMAC-SHA256 integrity,
24+ fail-closed on tamper).
2125
22- **Structural-only: ** ``WinusbBackend `` (Phase 2b) and
23- ``IokitBackend `` (Phase 2c) — class scaffolding + platform /
24- dependency validation in place; ``list `` and ``open `` raise
25- ``NotImplementedError `` referencing the in-module TODO list.
26- These need ctypes / pyobjc wiring **plus hardware testing ** to
27- become real.
26+ **Phase 2b — Windows ``WinUSB``: ** SetupAPI enumeration + ``WinUsb_* ``
27+ transfer ctypes wiring complete (**hardware-unverified **).
2828
29- **Process step : ** Phase 2e — see
30- :doc: ` usb_passthrough_security_review ` for the reviewer
31- checklist that must be signed before the feature flag flips
32- to default-on .
29+ **Phase 2c — macOS ``IOKit`` : ** native IOKit enumeration via ctypes
30+ complete; device claim / transfers delegate to libusb (the
31+ hardware-proven path on macOS). See the `` iokit_backend `` module
32+ docstring (** hardware-unverified **) .
3333
34- Open questions stay flagged inline as ``OPEN `` for reviewers.
34+ **Backend selection: ** ``default_passthrough_backend() `` picks
35+ WinUSB / IOKit / libusb by OS automatically.
36+
37+ **Remaining process step: ** Phase 2e — see
38+ :doc: `usb_passthrough_security_review ` for the reviewer checklist
39+ that must be signed by an external reviewer before the feature flag
40+ flips to default-on, plus the per-backend hardware test matrix.
3541
3642.. contents ::
3743 :local:
@@ -76,8 +82,13 @@ than they tolerate loss; the existing video/audio channels already
7682demonstrate that the underlying SCTP transport handles ordered
7783reliable streams adequately.
7884
79- OPEN: Should we use ``maxPacketLifeTime `` instead, with a generous
80- budget (~5 s)? Worth measuring on real WAN links before shipping.
85+ **Resolved (OQ1): ** keep ``ordered=True `` + ``maxRetransmits=None ``
86+ (fully reliable, ordered). USB control transfers (WebAuthn signing,
87+ descriptor reads) have zero tolerance for loss — a partially-lost
88+ stream corrupts the device state machine — so reliable-ordered
89+ semantics matter more than shaving a few ms of retransmit latency.
90+ The bounded-loss ``maxPacketLifeTime `` model is left for a future
91+ loss-tolerant high-throughput use case (YAGNI today).
8192
8293Framing
8394-------
@@ -96,10 +107,13 @@ Each channel message is one length-prefixed protocol frame::
96107- payload: opcode-specific. Bounded to 16 KiB to keep DataChannel
97108 message sizes reasonable.
98109
99- OPEN: Do we need fragmentation above 16 KiB? Most USB transfers fit;
100- control transfers are bounded by the device's wMaxPacketSize. A
101- follow-up frame with the same ``claim_id `` and a continuation flag
102- would be a low-cost addition.
110+ **Resolved (OQ2): ** fragmentation implemented.
111+ ``protocol.fragment_payload() `` splits a payload larger than 16 KiB
112+ into multiple same-``claim_id `` frames, clearing ``FLAG_EOF `` on all
113+ but the last; the receiver concatenates consecutive frame payloads
114+ until it sees EOF. Both transfer replies and ``LIST `` replies use this
115+ path; the common single-frame case still sends exactly one EOF-flagged
116+ frame, so existing behaviour is unchanged.
103117
104118Operations
105119----------
@@ -119,10 +133,13 @@ Op (hex) Direction Purpose
119133``0xFF ERROR `` either Protocol error / unsupported op
120134================ ========================================= ==============
121135
122- OPEN: Should ``LIST `` go through the channel at all, or should the
123- viewer use the existing REST ``/usb/devices `` endpoint and only use
124- the channel for transfers? The latter is simpler but couples the
125- two transports.
136+ **Resolved (OQ3): ** ``LIST `` goes over the channel. The session
137+ handles a ``LIST `` frame and returns the devices the ACL would not
138+ deny (a denied device is never even revealed to the viewer); the
139+ viewer calls ``UsbPassthroughClient.list_devices() ``. Enumeration and
140+ transfers share one already-authenticated channel instead of coupling
141+ in a second REST transport, and ACL filtering reuses the same logic as
142+ the claim decision.
126143
127144Backpressure
128145------------
@@ -132,9 +149,13 @@ Each side starts with a credit window of 16 outstanding frames per
132149message with a positive integer replenishes. Without flow control
133150a slow remote USB device would balloon DataChannel send buffers.
134151
135- OPEN: Should credits be per-endpoint (IN/OUT separately) instead of
136- per-claim? Bulk endpoints are independent, so per-endpoint is more
137- faithful to the hardware. Costs more state.
152+ **Resolved (OQ4): ** keep per-claim credits. They already meet the core
153+ goal — stopping a slow remote device from ballooning the host send
154+ buffer — with the least state and the simplest reasoning.
155+ Per-endpoint credits would track IN/OUT and each bulk endpoint
156+ separately for a meaningful complexity jump that only matters when
157+ multiple endpoints on one claim saturate at once; that is YAGNI until
158+ real measurement shows head-of-line blocking.
138159
139160
140161Per-OS driver wrappers
@@ -166,25 +187,33 @@ Windows — WinUSB
166187- ``ctypes `` wrappers around ``winusb.dll `` are public API; no kernel
167188 driver authoring required.
168189
169- OPEN: WinUSB requires the device to be *not already claimed * by another
170- driver. This rules out devices that the host OS thinks it owns
171- (printers, hubs, keyboards). We will need an in-app prompt explaining
172- why a particular device cannot be claimed.
190+ **Resolved (OQ5): ** WinUSB requires the device to be *not already
191+ claimed * by another driver, and only devices already bound to
192+ ``winusb.sys `` appear in ``WinusbBackend.list() ``. Devices the host OS
193+ owns (printers, hubs, keyboards) therefore never list — the viewer
194+ only ever sees the claimable subset and cannot mistake a system device
195+ for one it could claim. An OPEN for a vid/pid not in the list returns a
196+ clear ``no device matches `` error; the operator guide explains binding
197+ a device to WinUSB via Zadig / libwdi.
173198
174199macOS — IOKit
175200-------------
176201
177- - ``IOUSBHostInterface `` (modern, since 10.12) or ``IOUSBInterfaceInterface ``
178- (older but ubiquitous) via ``pyobjc ``.
179- - Requires entitlement signing if shipped through the App Store; for
180- dev / direct distribution this is fine but the binary must be
181- notarised.
182- - IOKit's ``CompletionMethod `` callbacks integrate with ``CFRunLoop ``,
183- not asyncio. We will need a thread that owns the runloop and
184- marshals completions back to the WebRTC bridge thread.
185-
186- OPEN: System Integrity Protection blocks claiming Apple devices and
187- some USB-C peripherals. Document the limit clearly.
202+ - Enumeration uses native IOKit through ``ctypes `` (no ``pyobjc ``
203+ dependency): ``IOServiceGetMatchingServices `` over ``IOUSBDevice ``
204+ plus ``IORegistryEntryCreateCFProperty `` reads of idVendor / idProduct
205+ / serial / locationID.
206+ - Claim and transfers delegate to libusb (see OQ6) rather than
207+ hand-rolling the COM-style ``IOUSBHostInterface `` plugin vtable.
208+
209+ **Resolved (OQ6): ** enumeration is native IOKit; claim / transfers
210+ delegate to libusb — the hardware-proven USB path on macOS — which
211+ avoids hand-coding an ``IOUSBHostInterface `` plugin vtable that could
212+ not be verified without hardware. A directly distributed (non
213+ App Store) build must be notarised; libusb device access needs no
214+ special entitlement, but System Integrity Protection still hides Apple
215+ internal devices and some USB-C peripherals. The operator guide
216+ documents the SIP exclusion boundary.
188217
189218Linux — libusb
190219--------------
@@ -194,10 +223,13 @@ Linux — libusb
194223- Hot-detach handling: libusb fires ``LIBUSB_TRANSFER_NO_DEVICE ``
195224 on in-flight transfers; we map that to ``CLOSED `` on the channel.
196225
197- OPEN: Some distros default to attaching ``usbhid `` to anything that
198- looks like a HID. We must call ``libusb_detach_kernel_driver `` and,
199- on close, ``libusb_attach_kernel_driver `` to restore — otherwise the
200- host OS loses input devices.
226+ **Resolved (OQ7): ** implemented. ``_LibusbHandle `` calls
227+ ``detach_kernel_driver `` for each interface of the active configuration
228+ that the kernel actually holds on open, remembers which it touched, and
229+ ``attach_kernel_driver `` restores them on close — otherwise the host OS
230+ permanently loses its keyboard / mouse after the session. libusb on
231+ Windows / macOS raises ``NotImplementedError `` for detach, which is
232+ tolerated and skipped (those platforms arbitrate drivers in the OS).
201233
202234
203235Security & ACL
@@ -226,9 +258,18 @@ Stored in ``~/.je_auto_control/usb_acl.json``::
226258- Allow rules can be persisted with a "remember" checkbox in the
227259 prompt.
228260
229- OPEN: Should we sign or HMAC the ACL file so a compromised host
230- process cannot silently grant itself access? Probably yes, with a
231- master key derived from a user passphrase or platform keychain.
261+ **Resolved (OQ8): ** HMAC-SHA256 implemented. The ACL carries a sidecar
262+ ``<acl>.sig `` signature, verified on load; a mismatch fails closed
263+ (default-deny, ``integrity_ok `` False) so a process that silently
264+ rewrites the JSON cannot grant itself access without also forging the
265+ signature. The signing key is pluggable — a deployment can pass a
266+ keychain-derived key via the constructor's ``hmac_key= ``; absent that,
267+ a random key file is generated next to the ACL (``0o600 `` on POSIX).
268+ Note: a same-user process can still read the key file and forge a
269+ signature, so keychain-derived keys are recommended for high-assurance
270+ deployments (see operator guide). Files written before signing existed
271+ are treated as legacy (still load, signed on next save); pass
272+ ``require_signature=True `` to reject unsigned files.
232273
233274Audit
234275-----
@@ -251,28 +292,41 @@ Phasing
2512921. **Done — Phase 1 **: read-only enumeration (``list_usb_devices ``).
2522932. **Done — Phase 1.5 **: hotplug events (``UsbHotplugWatcher ``,
253294 ``/usb/events ``).
254- 3. **Phase 2a (this design) **: protocol skeleton + ``UsbBackend `` ABC
255- + Linux ``libusb `` backend behind a feature flag.
256- 4. **Phase 2b **: Windows ``WinUSB `` backend.
257- 5. **Phase 2c **: macOS ``IOKit `` backend.
258- 6. **Phase 2d **: ACL persistence + host-side prompt UI + audit
259- integration.
260- 7. **Phase 2e **: external security review *before * default-on.
261-
262- Each subphase is its own multi-round project. Estimated effort
263- (experienced contributor): ~1 week per backend, ~1 week for ACL/UI,
264- plus the security review which depends on a reviewer's calendar.
265-
266-
267- Open questions, summarised
268- ==========================
269-
270- 1. ``maxRetransmits=None `` vs ``maxPacketLifeTime `` for the channel.
271- 2. Frame fragmentation above 16 KiB.
272- 3. ``LIST `` over the channel vs. exclusively over REST.
273- 4. Backpressure granularity (per-claim vs per-endpoint).
274- 5. What WinUSB cannot claim, and how to communicate that to the
275- viewer.
276- 6. macOS entitlement story for non-App-Store distribution.
277- 7. Linux kernel-driver detach/reattach lifecycle.
278- 8. ACL file integrity (HMAC vs platform keychain).
295+ 3. **Done — Phase 2a **: protocol + ``UsbBackend `` ABC + Linux
296+ ``libusb `` backend behind a feature flag.
297+ 4. **Done — Phase 2b **: Windows ``WinUSB `` backend (ctypes,
298+ hardware-unverified).
299+ 5. **Done — Phase 2c **: macOS ``IOKit `` backend (native enumeration +
300+ libusb transfers, hardware-unverified).
301+ 6. **Done — Phase 2d / 2d.1 **: ACL persistence + host-side prompt
302+ callback + audit integration + ACL file HMAC integrity.
303+ 7. **In progress — Phase 2e **: external security review *before *
304+ default-on, **plus ** the per-backend real-hardware test matrix.
305+ Both inherently require hardware and an external reviewer and cannot
306+ be completed in code alone.
307+
308+ The feature flag stays default-off until Phase 2e is signed off.
309+
310+
311+ Design decisions (formerly open questions)
312+ ==========================================
313+
314+ All eight original open questions are resolved; see the matching
315+ sections above for the implementation.
316+
317+ 1. **OQ1 — channel reliability **: ``maxRetransmits=None `` (fully
318+ reliable, ordered).
319+ 2. **OQ2 — frame fragmentation **: implemented via ``fragment_payload ``
320+ + EOF reassembly.
321+ 3. **OQ3 — ``LIST`` over the channel **: yes, ACL-filtered, over the
322+ channel.
323+ 4. **OQ4 — backpressure granularity **: per-claim (per-endpoint is
324+ YAGNI).
325+ 5. **OQ5 — what WinUSB cannot claim **: only WinUSB-bound devices list;
326+ a failed claim returns a clear error.
327+ 6. **OQ6 — macOS distribution **: native IOKit enumeration + libusb
328+ transfers; notarisation, no special entitlement, SIP boundary
329+ documented.
330+ 7. **OQ7 — Linux kernel driver **: detach on open, reattach on close.
331+ 8. **OQ8 — ACL integrity **: HMAC-SHA256 sidecar, pluggable
332+ (keychain-capable) key.
0 commit comments