Skip to content

Commit bcf681b

Browse files
committed
docs(network): map iOS over debugserver (machomap), not LLDB
1 parent a8e76e1 commit bcf681b

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

‎docs/network-interception.md‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Network capture and interception
2+
3+
mobilecli can observe and intercept the HTTP traffic of a **debuggable app you
4+
own** — an Android app with `android:debuggable="true"`, or an iOS app built with
5+
the `get-task-allow` entitlement (any Debug build). This is the backend for a
6+
Playwright-style `route()` in mobilewright: the host gets a callback when the app
7+
makes a request and decides whether to let it through, change it, fake a
8+
response, or fail it.
9+
10+
It reuses the injected-agent channel that already drives webviews: a JVMTI `.so`
11+
on Android, a dylib on iOS. On iOS the dylib is mapped into the app by the
12+
debugger-assisted Mach-O mapper (`devices/ios/machomap`) over the developer
13+
`debugserver` reached through the go-ios userspace tunnel — no LLDB and no
14+
re-signing, so it runs on real `get-task-allow` devices, not just the simulator.
15+
The network methods are added to that same in-app agent; nothing new is injected.
16+
Only debuggable apps can be attached to at all, so this never applies to App Store
17+
or release builds.
18+
19+
## Model
20+
21+
The contract mirrors the Chrome DevTools "Fetch" domain, because Playwright's
22+
`route()` is a thin layer over it. Porting mobilewright becomes a near-direct
23+
mapping.
24+
25+
- The host **enables** interception with a list of routes (URL glob + optional
26+
method). The agent pauses only requests that match a route, so unmatched
27+
traffic never makes a round trip to the host.
28+
- The agent emits a **paused** event per matched request, carrying a request id
29+
unique within the session.
30+
- The host replies with exactly one decision for that id: **continue**
31+
(optionally with a changed URL, method, headers or body), **fulfill** (a
32+
synthetic status, headers and body), or **abort** (fail with an error code).
33+
- Capture-only events (**request**, **response**, **finished**, **failed**)
34+
never pause; they back `page.on('request')` and friends, and later HAR
35+
recording. (Phase 2.)
36+
37+
### Fail open
38+
39+
This is the rule that keeps the app usable. A paused request continues
40+
**unchanged** whenever a decision cannot be obtained:
41+
42+
- the host stops polling (it disconnected or crashed),
43+
- no decision arrives within the agent's decision timeout (~60s),
44+
- interception is disabled.
45+
46+
Without this, a crashed test script would hang every request the app makes.
47+
48+
## Transport
49+
50+
The host talks to the in-app agent over the existing forwarded RPC socket
51+
(`agentRequest` in `devices/android_rpc.go`). The methods:
52+
53+
| Method | Params | Returns |
54+
|---------------------|--------------------------------------------------------------|-------------------------|
55+
| `network.enable` | `{routes: [{url, method?}]}` | `{status:"ok"}` |
56+
| `network.poll` | `{}` — long-polls, returns when an event is ready or ~30s | `{events: [ ... ]}` |
57+
| `network.continue` | `{requestId, url?, method?, headers?, body?}` | `{status:"ok"}` |
58+
| `network.fulfill` | `{requestId, status, headers?, body?}` | `{status:"ok"}` |
59+
| `network.abort` | `{requestId, errorCode?}` | `{status:"ok"}` |
60+
| `network.disable` | `{}` | `{status:"ok"}` |
61+
62+
`network.poll` long-polls so events arrive promptly without the host busy-waiting.
63+
The host runs one poll loop per `listen` and relays each event object to the CLI
64+
as a `stream.data` notification. Decisions are separate, idempotent calls keyed
65+
by `requestId`.
66+
67+
Bodies are base64 in both directions. A paused/response body larger than the
68+
agent's cap (256 KiB) is truncated in the event (with `bodyTruncated: true`) but
69+
never truncated in the app itself.
70+
71+
### Event shapes
72+
73+
```jsonc
74+
// matched request, paused, awaiting a decision
75+
{ "type": "paused", "requestId": "r12", "method": "GET",
76+
"url": "https://api.example.com/user", "headers": {"accept": "*/*"},
77+
"body": null, "stack": "okhttp" }
78+
```
79+
80+
## CLI
81+
82+
```
83+
# stream paused requests as NDJSON on stdout; decide from another terminal or a coprocess
84+
mobilecli network listen --device <id> --pause '**/api/**' [--pause 'POST **/login']
85+
86+
# decisions, keyed by the requestId from a paused event
87+
mobilecli network continue --device <id> --id r12 [--url ... --method ... --header K=V --body @file]
88+
mobilecli network fulfill --device <id> --id r12 --status 200 [--header K=V --body @file]
89+
mobilecli network abort --device <id> --id r12 [--error failed]
90+
```
91+
92+
`--pause` is a glob matched against the full URL. `PREFIX URL` form (`'POST **/x'`)
93+
restricts to a method. Repeat `--pause` for multiple routes.
94+
95+
## Scope / status
96+
97+
- **Phase 1 (this doc):** pause + continue/fulfill/abort on Android (OkHttp) and
98+
iOS (URLSession), on the iOS simulator and real `get-task-allow` devices.
99+
- **Phase 2:** capture-only events, response-stage interception, HAR recording,
100+
`HttpURLConnection`, webviews via CDP Fetch.
101+
102+
The in-app hooks live in `agents/android/java/` and `agents/ios/`. The host side
103+
— daemon stream method, CLI, device plumbing — lives in `devices/network.go`,
104+
`commands/network.go`, `cli/network.go`, and `server/daemon_streams.go`.

0 commit comments

Comments
 (0)