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
- Addons and examples
- Options
- Lifecycle and diagnostics
- 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 or close the terminal. The proxy stops and restores the Android proxy setting that existed before the run. The emulator keeps running. If the launcher is killed with SIGKILL or the machine loses power, use uvx proxy-lab status android followed by uvx proxy-lab stop android or uvx proxy-lab reset android.
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
The launcher selects the first booted Simulator, or you can select one explicitly:
uvx proxy-lab start ios --udid 11111111-1111-1111-1111-111111111111
It also attempts to install the mitmproxy CA into the Simulator keychain with simctl. To perform that step explicitly:
uvx proxy-lab trust ios --udid 11111111-1111-1111-1111-111111111111
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 manually if needed. If automatic simctl installation is unavailable, open mitm.it in the simulator's Safari and download the profile. The page is served through local capture, so extension approval must work first. Install it under Settings → General → VPN & Device Management, then enable full trust under Settings → General → About → Certificate Trust Settings.
5. Start your app. Press Ctrl-C or close the terminal to stop the proxy. If the process is killed with SIGKILL, use uvx proxy-lab stop ios.
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. The loader validates the file, requires a domains list, and rejects malformed entries before mitmproxy starts.
Addons and examples
Load one or more mitmproxy addons after the built-in domain logger. They run in the order supplied:
uvx proxy-lab start android \
--script examples/modify_response.py \
--script examples/mock_response.py
Only load addons you trust; they can modify traffic and expose sensitive data. Generic examples live in examples/, including response modification, mocking, and blocking.
Options
The main command surface is:
proxy-lab start <android|ios> [domains.yml] [options]
Common options:
| Option | Environment fallback | Effect |
|---|---|---|
--port PORT |
PORT |
Android mitmdump port. |
--avd AVD |
AVD |
Android AVD to boot when none is running. |
--serial SERIAL |
ANDROID_SERIAL |
Require a specific running Android emulator. |
--boot-timeout SECONDS |
BOOT_TIMEOUT |
Android boot timeout. |
--udid UDID |
— | Select an iOS Simulator for discovery and CA trust. |
--script FILE |
PROXY_LAB_SCRIPTS |
Add a mitmproxy addon; repeatable. |
--state-dir DIR |
PROXY_LAB_STATE_DIR |
Store owner-scoped session state in DIR. |
Environment variables remain supported for automation:
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.
Lifecycle and diagnostics
The launcher records its owning process and the Android proxy value before changing the device. Lifecycle commands never kill an unknown process by name:
uvx proxy-lab status android
uvx proxy-lab stop android
uvx proxy-lab reset android
uvx proxy-lab doctor
stop signals only the recorded owner and restores the previous Android proxy value. reset is the explicit recovery command for stale state or proxy settings. doctor reports the same pre-flight inputs used by start—versions, tools, SDK paths, devices, CA, config, and state—without starting a proxy.
Use uvx proxy-lab --version to print the wrapper version.
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, SERIAL, and BOOT_TIMEOUT affect Android only. Run the local checks with:
uv run python -m unittest discover -s tests -v
shellcheck android/start-proxy.sh ios/start-proxy.sh proxy_lab/common.sh proxy_lab/control.sh
Requirements
- macOS. The iOS Simulator needs Xcode.
- Python: Python 3.9+ is used by the packaged CLI/configuration loader.
uvxsupplies it automatically. - 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. Run
uvx proxy-lab status android, thenuvx proxy-lab stop androidoruvx proxy-lab reset android. The launcher restores the exact proxy value that existed before the run;resetis the explicit recovery path for stale state. - "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 any existing listener; it never kills a process it cannot prove it owns. |
| Proxy setting | Writes 10.0.2.2:$PORT after the port check and restores the previous value during 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. Pass trusted addons with
--script; see Addons and examples.
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 keeps the package version in
pyproject.toml;proxy-lab --versionreads installed package metadata. The release workflow refuses to publish a tag that disagrees with that version. - Tags are
X.Y.Z, with novprefix. A matching tag builds the wheel and sdist 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 state, and mitmdump. |
ios/start-proxy.sh |
mitmdump in macOS local-capture mode with automatic Simulator CA trust. |
proxy_lab/common.sh |
Shared pre-flight, mitmproxy, configuration, and session-state helpers. |
proxy_lab/control.sh |
Owner-scoped status, stop, reset, and doctor commands. |
local_router.py |
Strict mitmproxy domain-filter addon. |
proxy_lab/config.py |
Pure configuration loading, matching, and URL redaction helpers. |
proxy_lab/cli.py |
The proxy-lab entry point for uvx. |
examples/ |
Generic mitmproxy addon examples. |
tests/ |
Unit and fake-tool integration tests. |
domains.yaml |
Default host list. |
.github/workflows/ci.yml |
Shellcheck, unit tests, package smoke tests, 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 the unit and fake-tool tests plus shellcheck before pushing:
uv run python -m unittest discover -s tests -v
shellcheck android/start-proxy.sh ios/start-proxy.sh proxy_lab/common.sh proxy_lab/control.sh
Add notable changes to CHANGELOG.md under Unreleased. Release tags must match the version in pyproject.toml.
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 2.0.0
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-2.0.0.tar.gz | 934.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| proxy_lab-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 967.1 kB
Release files / proxy_lab-2.0.0.tar.gz
| Download URL | proxy_lab-2.0.0.tar.gz |
|---|---|
| Size | 934.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1a32c2fb70475c57cb6abec9e5bccfe9c6a9f904e568c4e1f5dc2c841ddec982
|
|
BLAKE2b-256 checksum How to use checksums |
d375fa4d1ddbc2ea981bcf9f7e17a005b4f0c4858c2774af10fe7eaeb24bbfaf
|
| 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-2.0.0-py3-none-any.whl
| Download URL | proxy_lab-2.0.0-py3-none-any.whl |
|---|---|
| Size | 32.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c590d192a4a2d5729de128000f1dc109f76308de1058b696e34c9321a2f37262
|
|
BLAKE2b-256 checksum How to use checksums |
75069a18b1d4eeae176626fe7b31e09ec1d0979805c77d139591a65a4a9c9c4f
|
| 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