How to Route Dataimpulse Proxies Through mitmproxy on macOS for Web Scraping
A single local mitmproxy listener forwards every scraper, browser, and emulator on macOS to Dataimpulse residential or mobile proxies over HTTP or SOCKS5, with rotation, throttling, and optional TLS inspection under your control.
Overview
mitmproxy is a scriptable, open-source intercepting proxy that runs as a normal macOS process. In upstream mode it accepts connections from local clients on one port (127.0.0.1:8080) and forwards that traffic to a second proxy — in this case, your Dataimpulse residential or mobile endpoint.
The advantage is centralization: you configure the proxy once, and then any browser, CLI tool, Python script, or mobile emulator on the machine can be routed through Dataimpulse without editing that tool's own proxy settings. Everything passes through a single choke point where you can cap concurrency, watch failures, and confirm which exit IP you are actually using.
Dataimpulse is a reasonable fit for this pattern because it offers residential and mobile endpoints over both HTTP and SOCKS5, with pay-as-you-go billing and a large IP pool — useful for bursty, high-volume data collection where a monthly commitment is unnecessary. You can compare it with other proxy providers before committing.
When this approach makes sense
| Approach | What you configure | Best for |
|---|---|---|
| mitmproxy upstream mode (this guide) | One local port for all clients | Several tools at once, central throttling and logging |
Per-tool proxy flags (curl -x, requests proxies=) |
Each tool separately | A single script with minimal moving parts |
| Desktop proxy managers (for example Proxifier) | Per-application rules | GUI apps that ignore system proxy settings |
What you will build
- A local mitmproxy listener on
127.0.0.1:8080 - An upstream connection to Dataimpulse over HTTP or SOCKS5
- Optional TLS interception for the specific hosts you want to inspect
- A concurrency gate addon that keeps request pressure predictable
- Working examples for
curl,requests, Playwright, and an Android emulator
Prerequisites
- macOS 13 or later with Homebrew installed
- A Dataimpulse account, plus the username, password, gateway host, and HTTP/SOCKS5 ports shown in your dashboard
- Python 3.9 or newer for the client examples (
python3 --version) - Comfort with the Terminal and a basic understanding of HTTP proxies and TLS
How the Setup Works
- mitmproxy listens on
127.0.0.1:8080. - Your client sends its request to mitmproxy instead of directly to the target site.
- mitmproxy opens a connection to the Dataimpulse gateway.
- Dataimpulse assigns an exit IP from its pool.
- The response travels back through mitmproxy to your client.
Two modes matter, and choosing between them early saves time:
- Pass-through (tunneling): mitmproxy forwards TLS bytes without decrypting them. No certificate changes are needed anywhere. Enabled with
--ignore-hosts '.*'. - Interception: mitmproxy terminates TLS so it can read or rewrite requests and responses. Requires trusting the mitmproxy CA certificate on macOS and in each client runtime.
For scraping that only needs a different exit IP, pass-through is simpler and faster, and it avoids certificate work entirely. Turn on interception only for hosts you genuinely need to inspect.
Steps
-
Install mitmproxy.
brew install mitmproxy mitmdump --version -
Export your Dataimpulse credentials as environment variables so they never appear in shell history or committed files.
export DI_USER='your-dataimpulse-username' export DI_PASS='your-dataimpulse-password' export DI_HOST='gateway-host-from-your-dashboard' export DI_HTTP_PORT='http-port-from-your-dashboard' export DI_SOCKS_PORT='socks5-port-from-your-dashboard' export DI_SESSION='scrape-macos-01'Add the same lines to
~/.zshrcif you want them available in every new shell, and confirm the host and port without echoing the password:echo "${DI_HOST}:${DI_HTTP_PORT}" -
Percent-encode credentials that contain reserved characters. Characters such as
@,:,/, or#inside a password will break the upstream URL. Encode only the value you place in the URL.python3 -c "import os, urllib.parse; print(urllib.parse.quote(os.environ['DI_PASS'], safe=''))" -
Start mitmproxy in pass-through upstream mode over HTTP. In pass-through mode, mitmproxy tunnels traffic without decrypting it, so no client certificate changes are required.
mitmdump \ --mode "upstream:http://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_HTTP_PORT}" \ --listen-port 8080 \ --ignore-hosts '.*' \ --set flow_detail=1 -
Verify the exit IP. Leave mitmproxy running in its own terminal window and test from a second window.
curl -s -x http://127.0.0.1:8080 https://api.ipify.org echo curl -s https://api.ipify.org echoThe two addresses should differ. If they match, the request is not reaching Dataimpulse — see the troubleshooting section.
-
Switch the upstream to SOCKS5 when you want the proxy to resolve DNS remotely, which is often preferable for geo-sensitive targets.
mitmdump \ --mode "upstream:socks5://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_SOCKS_PORT}" \ --listen-port 8080 \ --ignore-hosts '.*' -
Enable interception only for the hosts you need.
--allow-hostsrestricts decryption to matching hosts; everything else stays tunneled and untouched.mitmdump \ --mode "upstream:http://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_HTTP_PORT}" \ --listen-port 8080 \ --allow-hosts 'example\.com' \ -s ./gate.py -
Trust the mitmproxy CA certificate before scraping HTTPS hosts through interception. The certificate files are created the first time mitmproxy starts.
# First run generates ~/.mitmproxy/mitmproxy-ca-cert.pem mitmdump --listen-port 8081 & sleep 2 kill %1 sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain \ ~/.mitmproxy/mitmproxy-ca-cert.pemCommand-line runtimes often ignore the macOS keychain, so point them at an explicit bundle:
cat "$(python3 -c 'import certifi; print(certifi.where())')" \ ~/.mitmproxy/mitmproxy-ca-cert.pem > ~/.mitmproxy/ca-bundle.pem export REQUESTS_CA_BUNDLE=~/.mitmproxy/ca-bundle.pem export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem -
Add a concurrency gate addon so a burst from a fast scraper does not flood the gateway. Save this as
gate.pynext to the directory you launch mitmproxy from.# gate.py — cap in-flight requests passing through mitmproxy import asyncio from mitmproxy import http class Gate: def __init__(self, limit: int = 8) -> None: self._limit = limit self._sem: asyncio.Semaphore | None = None def running(self) -> None: self._sem = asyncio.Semaphore(self._limit) async def request(self, flow: http.HTTPFlow) -> None: await self._sem.acquire() flow.metadata["gate_held"] = True def _release(self, flow: http.HTTPFlow) -> None: if flow.metadata.pop("gate_held", False): self._sem.release() def response(self, flow: http.HTTPFlow) -> None: self._release(flow) def error(self, flow: http.HTTPFlow) -> None: self._release(flow) addons = [Gate(limit=8)]Load it with
-s ./gate.py. Note that addons only see intercepted flows: if you run with--ignore-hosts '.*', connections are tunneled and these hooks never fire. In that case, limit concurrency inside your scraper instead. -
Point your tools at the local listener. For shell tools, export the standard variables in the terminal you scrape from — but not in the terminal running mitmproxy, to avoid a proxy loop.
export HTTP_PROXY=http://127.0.0.1:8080 export HTTPS_PROXY=http://127.0.0.1:8080 export NO_PROXY=localhost,127.0.0.1 curl -s https://httpbin.org/ipFor a Python script, no CA configuration is needed while you stay in pass-through mode:
import requests LOCAL_PROXY = "http://127.0.0.1:8080" proxies = {"http": LOCAL_PROXY, "https": LOCAL_PROXY} response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=30) response.raise_for_status() print(response.json())For a headless browser, pass the same local proxy to the browser context:
from playwright.sync_api import sync_playwright LOCAL_PROXY = "http://127.0.0.1:8080" with sync_playwright() as p: browser = p.chromium.launch(headless=True, proxy={"server": LOCAL_PROXY}) page = browser.new_page() page.goto("https://httpbin.org/ip", wait_until="domcontentloaded") print(page.inner_text("body")) browser.close()If you also need to scrape a service running on your own machine, Chromium bypasses loopback addresses by default. Adding
args=["--proxy-bypass-list=<-loopback>"]to the launch call removes that exemption. -
Capture emulator traffic (optional). Dataimpulse mobile endpoints supply carrier-grade exit IPs; they do not capture device traffic themselves. To route an emulator through that pool, point the emulator at the host listener.
# Android emulator: 10.0.2.2 is the host machine adb shell settings put global http_proxy 10.0.2.2:8080 adb shell settings get global http_proxy # Reset when finished adb shell settings put global http_proxy :0For the iOS Simulator, open Settings, choose Wi-Fi, then Configure Proxy, Manual, and set the server to
127.0.0.1on port8080. Simulator traffic shares the Mac's network stack, so interception requires the CA to be installed in the simulator as well. -
Clean up when you are done. Remove the interception certificate and reset any client settings so unrelated traffic stops flowing through mitmproxy.
sudo security delete-certificate -c mitmproxy /Library/Keychains/System.keychain unset HTTP_PROXY HTTPS_PROXY
Troubleshooting
407 Proxy Authentication Required
The upstream URL is missing credentials, or a reserved character in the password broke the URL. Re-run the percent-encoding command in step 3 and rebuild the --mode string with the encoded value.
SSLCertVerificationError, CERT_AUTHORITY_INVALID, or unable to get local issuer certificate
You enabled interception but the client does not trust the mitmproxy CA. Install the certificate as shown in step 8, then set the runtime-specific bundle variables (REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS). Alternatively, drop back to pass-through mode with --ignore-hosts '.*' if you do not need to read the traffic.
Address already in use
Another process already owns the port. Find it, then change --listen-port instead of killing unrelated services.
lsof -nP -iTCP:8080 -sTCP:LISTEN
All requests hang or time out
You are most likely in a proxy loop: HTTP_PROXY is exported in the same shell that launched mitmproxy, so mitmproxy's own upstream connection is being sent back to itself. Launch mitmproxy in a clean terminal, or unset HTTP_PROXY HTTPS_PROXY before starting it.
Requests succeed but the exit IP never changes
Pass-through mode does not rotate anything on its own. Rotation behavior is determined by the session type you configured in your dashboard and by how your client opens connections. Confirm you are not pinning a single sticky session across the whole run, and check the exit IP with the command in step 5.
The emulator ignores the proxy
On Android emulators, 127.0.0.1 refers to the emulator itself, not the Mac. Use 10.0.2.2 and confirm the value with adb shell settings get global http_proxy.
Targets return 403 or 429
This is rate limiting at the destination, not a proxy failure. Reduce the gate limit in gate.py, add delays between requests, and make sure your scraping complies with the site's terms and robots.txt.
Summary
You now have a single local mitmproxy listener on macOS that forwards any client to Dataimpulse residential or mobile proxies over HTTP or SOCKS5, with pass-through mode for simple IP rotation and selective interception for deeper inspection. Credentials live in environment variables, concurrency is capped by an addon, and certificate trust can be added or removed cleanly when you no longer need to read encrypted traffic.