proxy-lab.sh
See your app's HTTPS traffic with one command. proxy-lab.sh starts mitmproxy for the Android emulator and the iOS simulator, and it does the certificate work for you. No /system remount, no Magisk, no stale proxy setting.
# iOS
uvx --from git+https://github.com/kibotu/proxy-lab.sh proxy-lab start ios domains.yml
# android
uvx --from git+https://github.com/kibotu/proxy-lab.sh proxy-lab start android domains.yml
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. It runs mitmproxy at a pinned version for you, so there is no Python setup and nothing global to install:
brew install uv
Then follow the path for your platform. The first run takes a few minutes, because it downloads mitmproxy and prepares the device. Later runs start in seconds.
Android
1. Let your debug build trust user-installed certificates. Android apps ignore them by default, so your app must opt in. Add res/xml/network_security_config.xml to your debug source set:
<?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 when the build is debuggable, so it cannot weaken a release build. Keep it in the debug source set anyway. See the network security config docs and why this is necessary.
2. Start the proxy:
uvx --from git+https://github.com/kibotu/proxy-lab.sh@ 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:8080. The CA install reboots the emulator one time per AVD.
3. Start your debug build. Requests print in the terminal as they happen.
4. Press Ctrl-C. The proxy stops and the emulator's proxy setting is cleared. 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 the proxy:
uvx --from git+https://github.com/kibotu/proxy-lab.sh@ proxy-lab start ios
2. Send the simulator's traffic through it. The simulator uses your Mac's network stack, so it has no proxy setting of its own. Pick one:
- System proxy — System Settings → Network → (your interface) → Details → Proxies. Turn on Web proxy and Secure web proxy, both
127.0.0.1port8080. Every app on the Mac goes through the proxy while this is on. - App only — point your debug build at
localhost:8080, for example withURLSessionConfiguration.connectionProxyDictionary.
3. Trust the CA, one time per simulator:
- Open
mitm.itin the simulator's Safari and download the profile. The page is served by the proxy, so step 2 must work first. - Install it: Settings → General → VPN & Device Management.
- Turn on full trust: Settings → General → About → Certificate Trust Settings.
4. Start your app. Press Ctrl-C to stop the proxy.
No root, no reboot, nothing to undo in the app.
Choose the domains to log
Every request goes through the proxy and appears in the mitmdump output. On top of that, hosts you list 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" # the host itself and its subdomains
Entries match the end of the host name. A leading dot excludes the apex domain.
Pass the file as the last argument:
uvx --from git+https://github.com/kibotu/proxy-lab.sh@ proxy-lab start android my-domains.yml
Without an argument you get the bundled domains.yaml, which lists .example.com only. Keep your own file next to your project and commit it, so the team logs the same hosts.
Options
The command surface is one line:
proxy-lab start <android|ios> [domains.yml]
Environment variables cover the rest:
| Variable | Default | Effect |
|---|---|---|
PORT |
8080 |
Port for mitmdump on the host. Android points the emulator at 10.0.2.2:$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 --from git+https://github.com/kibotu/proxy-lab.sh@ proxy-lab start android
If you run this daily, install the command once and keep the line short:
uv tool install git+https://github.com/kibotu/proxy-lab.sh@
proxy-lab start android
Move to a newer version with uv tool install --force git+https://github.com/kibotu/proxy-lab.sh@<X.Y.Z>.
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. All environment variables above apply.
Requirements
- macOS. The iOS Simulator needs Xcode, and Xcode needs macOS. The Android script uses portable tools only, so Linux probably works, but nobody tests it there.
- uv —
brew install uv. It runs mitmproxy at a pinned version. No Python install of your own is necessary. - Android: Android Studio with
adbandemulatoron your$PATH, plus a Google APIs AVD. Your debug build must trust user CAs, as shown in the Android quickstart. - iOS: Xcode.
The Android script also uses openssl and lsof, which macOS ships. It tells you if something is missing, and it prints the command that fixes it.
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:8080when the script runs, andnullafter a clean exit. 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 does not trust user CAs, so the system reports partial connectivity. Your app traffic works. Ignore it.
- Nothing shows up on iOS — the simulator does not use the proxy. Go back to step 2 of the iOS quickstart.
- 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, or 127.0.0.1 for local dev domains
iOS simulator ─────▶ 127.0.0.1:$PORT ┘
mitmdump terminates TLS with its own CA, prints what it sees, and forwards the request. 10.0.2.2 is the host address as the emulator sees it (emulator networking). Name resolution happens on the host, so an /etc/hosts entry sends a dev domain to a server on your machine.
local_router.py is a mitmproxy addon. The name promises more than it delivers: it logs matching hosts, it does not route. 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/). That needs adb root once and one reboot per AVD. The certificate survives later reboots. Release builds are unaffected.
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 it owns the port, and clears it on exit.
iOS has neither problem. The iOS side is a thin mitmdump wrapper, and this repo will not pretend otherwise.
What you get for the Android run:
| Step | Behaviour |
|---|---|
| Tools | Checks adb, uv, openssl, lsof and the addon, with install hints. Warms the mitmproxy download. |
| 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 it is missing. One reboot, one time per AVD. Rolls back a failed install. |
| Port | Stops stale proxies from earlier runs. Refuses to touch a process it does not own. |
| Proxy setting | Writes 10.0.2.2:$PORT after the port is confirmed. Clears it on exit. |
Re-run it as often as you want. The steps are idempotent.
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.
If you need those, or a GUI, look at HTTP Toolkit, Proxyman, or Charles. proxy-lab.sh stays a small, scriptable, reviewable pile of bash instead.
Versions and releases
- mitmproxy is pinned to
12.2.3inside the scripts, so the whole team sees the same behaviour. - proxy-lab.sh is pinned by you:
@1.0.0in theuvxcommand. Without a tag you getmain. Put the pinned command in your project README or a Makefile, and the team runs one version. - Tags are
X.Y.Z, with novprefix. A tag push builds the wheel and sdist at that version and publishes a GitHub Release. 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 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, plus a proxy smoke test 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.0.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.0.1.tar.gz | 917.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| proxy_lab-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 936.1 kB
Release files / proxy_lab-1.0.1.tar.gz
| Download URL | proxy_lab-1.0.1.tar.gz |
|---|---|
| Size | 917.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
875134468824f293b8f994037b9c1e04c33fa38fff79c38a63a630d86cb6afa3
|
|
BLAKE2b-256 checksum How to use checksums |
5b57723c4071c7d0812e542b3fe3883edd12f2d48288467e7aa5c917f99abf7e
|
| 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 22, 2026.
Transparency logRelease files / proxy_lab-1.0.1-py3-none-any.whl
| Download URL | proxy_lab-1.0.1-py3-none-any.whl |
|---|---|
| Size | 18.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
486b5391bdd1db1b0b082ce26cd5d5650811ea9e3245ac12c9bea491cb53c440
|
|
BLAKE2b-256 checksum How to use checksums |
0a2183c4580a703a0b82865b99a37a256336fe9f32f46ed59398b3410ae6ceda
|
| 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 22, 2026.
Transparency log