Summary
Chrome 100+ refuses CDP HTTP requests where the SOURCE IP is not loopback, even when launched with `--remote-debugging-address=0.0.0.0` AND `--remote-allow-origins=*`. This breaks the obvious approach of "just publish 9222 to the host and curl it."
Repro:
```bash
reach create --name probe --extra-port 9222
docker exec -d -u sandbox -e DISPLAY=:99 probe google-chrome \
--no-sandbox --disable-dev-shm-usage \
--remote-debugging-address=0.0.0.0 \
--remote-debugging-port=9222 \
--remote-allow-origins=* \
--user-data-dir=/tmp/chrome-profile about:blank
From inside the container — works:
docker exec probe curl -s http://127.0.0.1:9222/json/version
{"Browser": "Chrome/146.0.7680.177", ...}
From the host — fails:
curl -sv http://localhost:9222/json/version
Connected to localhost (127.0.0.1) port 9222
> GET /json/version HTTP/1.1
> Host: localhost:9222
* Recv failure: Connection reset by peer
```
The TCP connection succeeds (the docker port mapping works) but Chrome closes the HTTP request mid-flight because the source IP is the docker bridge gateway (172.17.0.1 in my case), not 127.0.0.1.
Workaround in use
Run a localhost forwarder inside the container:
- Launch Chrome bound to loopback only: `--remote-debugging-address=127.0.0.1 --remote-debugging-port=9224`
- Run socat as a 0.0.0.0:9222 → 127.0.0.1:9224 forwarder so the docker-published port reaches Chrome through a loopback hop:
```bash
docker exec -d socat TCP-LISTEN:9222,bind=0.0.0.0,reuseaddr,fork TCP:127.0.0.1:9224
```
This works. The host can now `curl http://localhost:9222/json/version\` and get a real CDP response. But it requires:
Suggested fixes
- Bake socat into the image (separate from net-tools, since this one is load-bearing for any CDP-from-host workflow).
- Add a
reach create --cdp flag (or env var) that, when set, automatically launches Chrome with the loopback bind + spins up the socat forwarder + publishes 9222 to the host. The caller gets a working `http://localhost:9222\` for free.
- Document the loopback bridge pattern in `docs/cdp-access.md` so the next person doesn't burn an hour rediscovering it.
I'd push for (2). The reach pitch is "AI-drivable containerized desktop sandbox." A working CDP endpoint is the single most useful host-facing primitive after VNC.
Discovered while
Wiring todie/revenant's todie-daemon to drive Chrome inside reach via CDP, for posting Threads.com chains. Same workflow PR #4 was scoped for; the auth handoff piece works perfectly, the CDP access piece needed this dance.
The full working invocation is captured in todie/revenant start-daemon.sh for reference.
Summary
Chrome 100+ refuses CDP HTTP requests where the SOURCE IP is not loopback, even when launched with `--remote-debugging-address=0.0.0.0` AND `--remote-allow-origins=*`. This breaks the obvious approach of "just publish 9222 to the host and curl it."
Repro:
```bash
reach create --name probe --extra-port 9222
docker exec -d -u sandbox -e DISPLAY=:99 probe google-chrome \
--no-sandbox --disable-dev-shm-usage \
--remote-debugging-address=0.0.0.0 \
--remote-debugging-port=9222 \
--remote-allow-origins=* \
--user-data-dir=/tmp/chrome-profile about:blank
From inside the container — works:
docker exec probe curl -s http://127.0.0.1:9222/json/version
{"Browser": "Chrome/146.0.7680.177", ...}
From the host — fails:
curl -sv http://localhost:9222/json/version
Connected to localhost (127.0.0.1) port 9222
> GET /json/version HTTP/1.1
> Host: localhost:9222
* Recv failure: Connection reset by peer
```
The TCP connection succeeds (the docker port mapping works) but Chrome closes the HTTP request mid-flight because the source IP is the docker bridge gateway (172.17.0.1 in my case), not 127.0.0.1.
Workaround in use
Run a localhost forwarder inside the container:
```bash
docker exec -d socat TCP-LISTEN:9222,bind=0.0.0.0,reuseaddr,fork TCP:127.0.0.1:9224
```
This works. The host can now `curl http://localhost:9222/json/version\` and get a real CDP response. But it requires:
Suggested fixes
reach create --cdpflag (or env var) that, when set, automatically launches Chrome with the loopback bind + spins up the socat forwarder + publishes 9222 to the host. The caller gets a working `http://localhost:9222\` for free.I'd push for (2). The reach pitch is "AI-drivable containerized desktop sandbox." A working CDP endpoint is the single most useful host-facing primitive after VNC.
Discovered while
Wiring todie/revenant's todie-daemon to drive Chrome inside reach via CDP, for posting Threads.com chains. Same workflow PR #4 was scoped for; the auth handoff piece works perfectly, the CDP access piece needed this dance.
The full working invocation is captured in todie/revenant start-daemon.sh for reference.