Skip to content

Commit 7fd79bb

Browse files
committed
Document operations layer + USB passthrough chain in /docs
Operations layer reference (operations_layer_doc.rst, Eng + Zh): - Folder sync, coturn TURN bundle, hardened REST API + endpoint table, Prometheus exposition, multi-host admin console, tamper-evident audit log, WebRTC packet inspector, USB device enumeration, USB hotplug events, system diagnostics, web admin dashboard, OpenAPI 3.1 + Swagger UI, configuration bundle. USB passthrough docs (Eng + Zh): - usb_passthrough_design.rst — protocol (10 opcodes, framing, credit flow control), per-OS backend ABCs, ACL + security model, phasing roadmap, 8 OPEN questions for reviewers - usb_passthrough_security_review.rst — Phase 2e reviewer checklist: threat model, ACL / audit / protocol-hardening / resource-bounds / lifecycle / per-OS items each cross-referenced to a test, plus 8 pen-test scenarios + sign-off block Both Eng and Zh toctrees register the four new operations_layer docs (operations_layer_doc, usb_passthrough_design, usb_passthrough_security_review, usb_passthrough_operator_guide). Sphinx -E build clean: 7 pre-existing warnings in older docs, 0 new from this set.
1 parent bee283f commit 7fd79bb

8 files changed

Lines changed: 1912 additions & 0 deletions

File tree

docs/source/Eng/doc/operations_layer/operations_layer_doc.rst

Lines changed: 514 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
================================================
2+
USB Passthrough — Phase 2 Design (DRAFT)
3+
================================================
4+
5+
.. warning::
6+
**DRAFT — Linux-libusb path complete; cross-platform backends are
7+
structural skeletons only.**
8+
9+
**Shipped (rounds 27 / 34 / 37 / 39 / 40 / 41 / 42):**
10+
Phase 1 (read-only enumeration), Phase 1.5 (hotplug events),
11+
Phase 2a (protocol + ABCs + ``LibusbBackend`` lifecycle +
12+
``FakeUsbBackend`` for tests + feature flag, default off),
13+
Phase 2a.1 (full ``LibusbBackend`` transfers + CREDIT-based
14+
inbound flow control + audit hooks),
15+
**viewer-side ``UsbPassthroughClient``** (blocking
16+
open / control_transfer / bulk_transfer / interrupt_transfer / close
17+
with outbound credit waits and shutdown propagation),
18+
Phase 2d (``UsbAcl`` persistent allow-list, ACL-gated OPEN with
19+
prompt-callback path, audit-log integration via the existing
20+
tamper-evident chain).
21+
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.
28+
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.
33+
34+
Open questions stay flagged inline as ``OPEN`` for reviewers.
35+
36+
.. contents::
37+
:local:
38+
:depth: 2
39+
40+
41+
Goals
42+
=====
43+
44+
Allow a remote AutoControl viewer to use a USB device that is
45+
physically attached to the host. Concrete user stories:
46+
47+
- Plug a USB security key into the host machine; have it sign a
48+
WebAuthn challenge initiated by the viewer.
49+
- Plug a USB-serial debug board into a lab host; let a remote
50+
developer talk to it via their local terminal.
51+
- Plug a printer into the host; let the viewer's OS see the printer
52+
as if it were locally attached.
53+
54+
Non-Goals
55+
=========
56+
57+
- **High-throughput isochronous transfers** (USB webcams, audio
58+
interfaces). The latency budget across WebRTC + DataChannel +
59+
driver round-trips is not compatible with isochronous USB. Use the
60+
existing audio/video tracks for those.
61+
- **Automatic kernel-level device redirection** like USB/IP. We are
62+
building a userspace forwarder, not replacing a kernel driver.
63+
- **Phase 2 will not ship without an explicit security review.**
64+
65+
66+
Transport
67+
=========
68+
69+
Channel
70+
-------
71+
72+
A dedicated WebRTC ``DataChannel`` named ``usb`` per session, with
73+
``ordered=True`` and ``maxRetransmits=None`` (full reliability).
74+
Bulk and interrupt USB transfers tolerate the latency far better
75+
than they tolerate loss; the existing video/audio channels already
76+
demonstrate that the underlying SCTP transport handles ordered
77+
reliable streams adequately.
78+
79+
OPEN: Should we use ``maxPacketLifeTime`` instead, with a generous
80+
budget (~5 s)? Worth measuring on real WAN links before shipping.
81+
82+
Framing
83+
-------
84+
85+
Each channel message is one length-prefixed protocol frame::
86+
87+
+----+--------+----------+--------------------+
88+
| 1B | 1B | 2B | payload |
89+
| op | flags | claim_id | (op-specific body) |
90+
+----+--------+----------+--------------------+
91+
92+
- ``op``: 1-byte opcode (see *Operations* below)
93+
- ``flags``: 8 bits, currently only ``EOF`` (bit 0) for chunked reads
94+
- ``claim_id``: 16-bit identifier for one open device claim within
95+
the session. Allocated by the host at OPEN time, recycled at CLOSE.
96+
- payload: opcode-specific. Bounded to 16 KiB to keep DataChannel
97+
message sizes reasonable.
98+
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.
103+
104+
Operations
105+
----------
106+
107+
================ ========================================= ==============
108+
Op (hex) Direction Purpose
109+
================ ========================================= ==============
110+
``0x01 LIST`` viewer → host, host → viewer (response) Enumerate devices the viewer is permitted to claim
111+
``0x02 OPEN`` viewer → host Request claim of (vendor_id, product_id, serial)
112+
``0x03 OPENED`` host → viewer Reply: success + claim_id, or error
113+
``0x04 CTRL`` viewer ↔ host Control transfer (bmRequestType, bRequest, wValue, wIndex, data)
114+
``0x05 BULK`` viewer ↔ host Bulk IN/OUT transfer on a specific endpoint
115+
``0x06 INT`` viewer ↔ host Interrupt IN/OUT transfer
116+
``0x07 CREDIT`` viewer ↔ host Backpressure window update
117+
``0x08 CLOSE`` viewer → host Release the claim
118+
``0x09 CLOSED`` host → viewer Acknowledgement (or unsolicited on host-side disconnect)
119+
``0xFF ERROR`` either Protocol error / unsupported op
120+
================ ========================================= ==============
121+
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.
126+
127+
Backpressure
128+
------------
129+
130+
Each side starts with a credit window of 16 outstanding frames per
131+
``claim_id``. Receiving a frame consumes one credit; a ``CREDIT``
132+
message with a positive integer replenishes. Without flow control
133+
a slow remote USB device would balloon DataChannel send buffers.
134+
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.
138+
139+
140+
Per-OS driver wrappers
141+
======================
142+
143+
The driver layer is hidden behind a single ``UsbBackend`` ABC::
144+
145+
class UsbBackend(abc.ABC):
146+
def open(self, vendor_id, product_id, serial) -> "UsbHandle": ...
147+
def list(self) -> list[UsbDevice]: ...
148+
149+
class UsbHandle(abc.ABC):
150+
def control_transfer(self, ...): ...
151+
def bulk_transfer(self, endpoint, data, timeout_ms): ...
152+
def interrupt_transfer(self, endpoint, data, timeout_ms): ...
153+
def close(self): ...
154+
155+
This isolates the OS-specific bits and lets us write the protocol /
156+
session layer without committing to a backend choice up front.
157+
158+
Windows — WinUSB
159+
----------------
160+
161+
- Best path for HID-class devices we don't already own a driver for:
162+
install ``WinUSB`` via libwdi or have the user manually associate
163+
the device with WinUSB through Zadig.
164+
- Use ``CreateFile`` + ``WinUsb_Initialize`` + ``WinUsb_ControlTransfer``
165+
/ ``WinUsb_ReadPipe`` / ``WinUsb_WritePipe``.
166+
- ``ctypes`` wrappers around ``winusb.dll`` are public API; no kernel
167+
driver authoring required.
168+
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.
173+
174+
macOS — IOKit
175+
-------------
176+
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.
188+
189+
Linux — libusb
190+
--------------
191+
192+
- ``pyusb`` over ``libusb-1.0`` works without root if ``udev`` rules
193+
grant the user access; we will document a sample rule.
194+
- Hot-detach handling: libusb fires ``LIBUSB_TRANSFER_NO_DEVICE``
195+
on in-flight transfers; we map that to ``CLOSED`` on the channel.
196+
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.
201+
202+
203+
Security & ACL
204+
==============
205+
206+
Per-device allow-list
207+
---------------------
208+
209+
Stored in ``~/.je_auto_control/usb_acl.json``::
210+
211+
{
212+
"version": 1,
213+
"rules": [
214+
{"vendor_id": "1050", "product_id": "0407", "label": "YubiKey 5",
215+
"allow": true, "prompt_on_open": true},
216+
...
217+
],
218+
"default": "deny"
219+
}
220+
221+
- Default policy is **deny**. A device the user has not explicitly
222+
allowed cannot be claimed.
223+
- ``prompt_on_open`` triggers a host-side modal each time a viewer
224+
requests OPEN. The modal shows the vendor/product/serial and the
225+
viewer ID requesting access.
226+
- Allow rules can be persisted with a "remember" checkbox in the
227+
prompt.
228+
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.
232+
233+
Audit
234+
-----
235+
236+
Every OPEN, OPENED, CLOSE, and ERROR is appended to the existing
237+
audit log under event_type ``"usb_passthrough"``. Frame-level
238+
transfer logging is too noisy and is logged only on ERROR.
239+
240+
Privilege
241+
---------
242+
243+
The host process must run with whatever privilege the chosen
244+
backend requires (Linux udev rules, macOS entitlements, Windows
245+
maybe nothing for WinUSB). The README will spell this out per-OS.
246+
247+
248+
Phasing
249+
=======
250+
251+
1. **Done — Phase 1**: read-only enumeration (``list_usb_devices``).
252+
2. **Done — Phase 1.5**: hotplug events (``UsbHotplugWatcher``,
253+
``/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).

0 commit comments

Comments
 (0)