proxy-lab.sh
See your app's HTTPS traffic with one command. proxy-lab.sh starts mitmproxy for the Android emulator and iOS Simulator. It avoids /system remounts and macOS proxy settings; certificate setup remains explicit where the platform requires it.
# iOS
uvx proxy-lab start ios
# Android
uvx proxy-lab start android
These commands install the published release. From a checkout, use
uvx --from . proxy-lab start ios (or android) to run the current source.
Pinning the wrapper does not pin mitmproxy.
Contents
- Quickstart — Android, iOS
- Choose the domains to log
- Options
- Run from a clone
- Requirements
- Troubleshooting
- How it works
- Why Android needs this
- Scope and alternatives
- Versions and releases
- Project layout
- Contributing
- License
- Support
Quickstart
Install uv to run the commands below. The scripts prefer a host-installed mitmdump; without one, they use uv to resolve mitmproxy@latest.
brew install uv
# optional: use a host binary instead of the uv fallback
brew install --cask mitmproxy
Then follow the path for your platform. If no host mitmdump is installed, the first run resolves the latest compatible mitmproxy release, downloading it when needed. Later runs are usually quick.
Android
1. Let your debug build trust user-installed certificates. Most apps do not trust user-installed CAs by default. For a debug build, opt in with res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
Point to it from the <application> tag in AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
<debug-overrides> applies only to debuggable builds, so it does not weaken a release build. Keep the file in the debug source set. See the network security config docs and why this is necessary.
2. Start the proxy:
uvx proxy-lab start android
The script checks your tools, reuses a running emulator or boots one, installs the mitmproxy CA into the user trust store, and sets the emulator proxy to 10.0.2.2:$PORT (default 8080). The first CA install reboots the emulator once per AVD.
3. Start your debug build. Requests print in the terminal as they happen.
4. Press Ctrl-C. The proxy stops and its cleanup path clears the emulator's proxy setting. The emulator keeps running.
Use a Google APIs AVD image. "Google APIs Play Store" images refuse
adb root, and the CA install needs it.
iOS
1. Start a simulator. Launch it in Xcode or Device Hub. The launcher does not boot one.
2. Start the proxy:
uvx proxy-lab start ios
3. Allow mitmproxy's network extension. The launcher requests local capture with the Simulator process filter. It is intended to cover simulators launched from Xcode or Device Hub. If macOS has not approved the redirector yet, approve the prompt. Local capture is outbound-only; no system or app proxy setting is required. If you previously configured a manual system proxy, turn it off so it does not duplicate the capture path.
4. Trust the CA, one time per simulator:
- Open
mitm.itin the simulator's Safari and download the profile. The page is served through local capture, so extension approval must work first. - Install it: Settings → General → VPN & Device Management.
- Turn on full trust: Settings → General → About → Certificate Trust Settings.
5. Start your app. Press Ctrl-C to stop the proxy.
No root or reboot is required. HTTPS interception still requires the simulator to trust the CA.
Choose the domains to log
Requests appear in the mitmdump output. Matching hosts also get a [local_router] line, which makes your own API easy to find in a busy log.
Write your list in a YAML file:
domains:
- ".example.com" # subdomains only: api.example.com yes, example.com no
- "acme.dev" # literal suffix; use ".acme.dev" for domain boundaries
Entries are literal suffix matches. A leading dot is safest for subdomains; without one, acme.dev also matches names such as notacme.dev.
Pass the file as the last argument:
uvx proxy-lab start android my-domains.yml
Without an argument you get the bundled domains.yaml, which lists .example.com only. Keep a custom file with the project if the team should share the same filter.
Options
The command surface is one line:
proxy-lab start <android|ios> [domains.yml]
Environment variables cover the rest:
| Variable | Default | Effect |
|---|---|---|
PORT |
8080 |
Android mitmdump port. The emulator points at 10.0.2.2:$PORT; iOS local capture has no proxy port. |
AVD |
first entry of emulator -list-avds |
AVD to boot when none is running. Android only. |
BOOT_TIMEOUT |
240 |
Seconds to wait for the emulator to finish booting. Android only. |
PROXY_LAB_CONFIG |
bundled domains.yaml |
Path to your domains file. Same effect as the argument above. |
PORT=8081 AVD=Pixel_10a uvx proxy-lab start android
If you run this daily from a checkout, install the command once:
uv tool install .
proxy-lab start android
For the published release, use uv tool install --force proxy-lab.
Run from a clone
Use a clone when you change the scripts or the addon:
git clone https://github.com/kibotu/proxy-lab.sh
cd proxy-lab.sh
./android/start-proxy.sh # or ./ios/start-proxy.sh
The scripts are the same code that uvx runs. Edit domains.yaml in place, or set PROXY_LAB_CONFIG. PORT, AVD, and BOOT_TIMEOUT affect Android only.
Requirements
- macOS. The iOS Simulator needs Xcode.
- mitmproxy: Optional host
mitmdump; otherwise the launchers requestmitmproxy@latestthrough uv. See Quickstart for install commands. - curl — optional; when available, preflight uses it to check whether a newer mitmproxy release is available. A failed or unavailable check is ignored.
- Android:
adbandemulatoron your$PATH(Android Studio's SDK provides both), plus a Google APIs AVD. Your debug build must trust user CAs, as shown in the Android quickstart. - iOS: Xcode and a local-mode-capable mitmproxy (
10.1.5+; see macOS local capture). The fallback requestsmitmproxy@latest.
The Android script also uses openssl and lsof, which macOS ships. Preflight reports missing tools with a PATH hint.
Troubleshooting
The scripts fail loudly, and the error line usually contains the answer. These are the recurring ones:
adbd cannot run as root in production builds— the AVD uses a Play Store image. Check withgrep image.sysdir ~/.android/avd/<AVD>.avd/config.iniand create a Google APIs AVD instead.net::ERR_CERT_AUTHORITY_INVALID— the app does not trust the CA. Confirm the network security config is in the build you are running, and that it is a debug build. To reinstall the certificate:adb root && adb shell rm /data/misc/user/0/cacerts-added/<hash>.0, then run the script again. Restart the app afterwards, because a running process keeps its trust anchors.- Requests time out — the proxy stopped while the emulator still points at it.
adb shell settings get global http_proxyprints10.0.2.2:$PORTwhile the script runs, andnullafter a clean exit ($PORTdefaults to8080). If it prints an address and nothing listens, start the script again, or clear it withadb shell settings delete global http_proxy. - "No internet connection" while proxied — Android's connectivity check may report partial connectivity because it does not trust user CAs. App traffic may still work.
- Nothing shows up on iOS — make sure a simulator is running (from Xcode or Device Hub), the network extension was allowed, and the CA is trusted. Do not configure a system proxy.
- No
[local_router]lines — the host is not in the domains file in use. See Choose the domains to log.
Still stuck? Open an issue with the exact error line.
How it works
Android emulator ──▶ 10.0.2.2:$PORT ─┐
├──▶ mitmdump on the host ──▶ upstream via host DNS/hosts
iOS Simulator ──▶ macOS local capture ┘
mitmdump terminates TLS with its own CA, prints what it sees, and forwards the request. On Android, 10.0.2.2 is the host address as the emulator sees it (emulator networking). On iOS, mitmproxy's signed network extension selects the Simulator process and feeds it to local capture; there is no system proxy or client-side proxy port. Name resolution happens on the host, so an /etc/hosts entry can point a development domain at a local server. local_router.py only logs matching requests.
For local debugging, both launchers set ssl_insecure=true, which disables upstream certificate verification. Remove that option when testing upstream certificate validation.
local_router.py is a mitmproxy addon that logs matching hosts. It does not route traffic. Both platforms load it.
Why Android needs this
Since Android 7, apps that target API 24 and higher ignore user-installed CAs unless they opt in (Android Developers Blog). The common answer is to put the CA in the system store. That answer keeps getting more expensive:
- The mitmproxy guide hashes the certificate by hand, remounts
/system, and needs-writable-systemon every boot. - Android 14 moved the store into the immutable Conscrypt APEX (AOSP). That mount is private per process, so even root edits stay invisible to apps (HTTP Toolkit). The known workarounds are a Magisk module, or
nsenterinto Zygote's mount namespace.
proxy-lab.sh takes the other door: your debug build opts into the user store, and the script installs the CA there (/data/misc/user/0/cacerts-added/). The first install uses adb root and reboots the AVD; the script may call adb root again afterward. This flow is intended for debug builds and does not change release trust policy.
The second half of the problem is routing. The emulator must point at 10.0.2.2, not localhost, through a setting that goes stale in silence. The script writes that setting after the port check succeeds, and its cleanup path clears it on normal shutdown.
The Android emulator is a guest, not a Mac process, so macOS local capture does not replace this flow. The current mitmproxy documentation still treats Android proxying and CA setup as separate concerns. WireGuard mode avoids the explicit proxy setting, but requires a WireGuard client/configuration and does not remove the CA-trust requirement.
What you get for the Android run:
| Step | Behaviour |
|---|---|
| Tools | Checks adb, openssl, lsof and the addon, then selects a host mitmdump or warms uv's mitmproxy@latest fallback. It logs the resolved version and, when available, a newer PyPI release. |
| Host CA | Generates ~/.mitmproxy/ on the first run. |
| Emulator | Reuses a running emulator, or boots one and waits for it. |
| Device CA | Installs the certificate if missing. The first install reboots once per AVD. If chmod/restorecon fails, the partial file is removed; later verification failures are reported. |
| Port | Refuses a non-mitmdump listener and stops commands whose arguments contain mitmdump; it does not prove ownership. |
| Proxy setting | Writes 10.0.2.2:$PORT after the port check. Clears it during normal cleanup. |
Repeated runs reuse the existing CA and emulator state.
Scope and alternatives
Out of scope, on purpose:
- Physical devices. Emulator and simulator only.
- Release builds. They do not trust user CAs, and that is correct.
- Certificate pinning. A pinning app rejects the proxy CA. Turn pinning off in debug builds, or use a pin bypass.
- Response rewriting and mocking. mitmproxy does all of that. Write your own addon next to
local_router.py.
For a GUI or broader device support, use HTTP Toolkit, Proxyman, or Charles. This project stays small, scriptable, and reviewable.
Versions and releases
- mitmproxy uses the host
mitmdumpwhen available. Otherwise the scripts request the intentionally unpinnedmitmproxy@latestthrough uv. Preflight logs the resolved version and, whencurlis available, queries PyPI for a newer release. - proxy-lab.sh resolves the published package by name. Pin the wrapper yourself when the team needs reproducibility; pinning it does not pin mitmproxy, so put the command in your project README or Makefile.
- Tags are
X.Y.Z, with novprefix. A tag push builds the wheel and sdist at that version and publishes both a GitHub Release and the same artifacts to PyPI. CHANGELOG.md has the per-version detail.
Project layout
| Path | What it is |
|---|---|
android/start-proxy.sh |
The full Android flow: checks, CA, emulator, port, proxy setting, mitmdump. |
ios/start-proxy.sh |
mitmdump in macOS local-capture mode with the shared addon. |
local_router.py |
mitmproxy addon. Logs hosts from the domains file. |
domains.yaml |
Default host list. |
proxy_lab/cli.py |
The proxy-lab entry point for uvx. Dispatches to the scripts. |
.github/workflows/ci.yml |
Shellcheck, iOS argument validation, and proxy smoke tests on macOS and Ubuntu. |
Contributing
Issues and pull requests are welcome, in particular real failure modes the pre-flight checks miss.
Run shellcheck on the scripts before you push, because CI does. Add notable changes to CHANGELOG.md under Unreleased.
License
Apache License 2.0. See LICENSE.
Support
If proxy-lab.sh saved you an afternoon, or one ERR_CERT_AUTHORITY_INVALID hunt, consider buying me a coffee.
Release files for proxy-lab 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| proxy_lab-1.1.1.tar.gz | 919.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| proxy_lab-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 940.0 kB
Release files / proxy_lab-1.1.1.tar.gz
| Download URL | proxy_lab-1.1.1.tar.gz |
|---|---|
| Size | 919.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b0b70ae7cb0445fe2e7a441445580bdcc93b032d6e47b6fb5dbd629ef99d5b4b
|
|
BLAKE2b-256 checksum How to use checksums |
1b3d48ec7b39aca1ce52f9017ee1ae006ff4375d52e11a864ecb1662f41f1a15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / proxy_lab-1.1.1-py3-none-any.whl
| Download URL | proxy_lab-1.1.1-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c3ffa55bba9b2d5bc3050ebfd4eaff844af47256ae5fd6d95a8e0ff85a698c13
|
|
BLAKE2b-256 checksum How to use checksums |
206c05a161bf7e3b8b4f0279ea0caf4aebe65e7671efeb0055708b83d126f9e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log