Add browser proxy to automatically forward browser connections to local#281
Add browser proxy to automatically forward browser connections to local#281KevinFairise2 wants to merge 8 commits into
Conversation
…al laptop
When running `dda env dev start`, a small HTTP daemon is now started on the
host that listens on a deterministic port (derived from the container name).
Inside the container, /usr/local/bin/xdg-open is replaced with a Python script
that forwards any xdg-open call to the host daemon via host.docker.internal.
For OAuth/auth flows that redirect back to localhost:{port}/callback, the daemon
detects the redirect_uri in the URL query parameters and sets up an SSH local
port forward (host 127.0.0.1:{port} → container localhost:{port}) before
opening the browser, so the auth callback reaches the service running inside
the container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
…other tunnel When two containers run OAuth simultaneously with the same callback port, the second SSH tunnel can't bind the port. Previously _wait_for_port_bound would incorrectly return True (EADDRINUSE detected but from the wrong process). Now it also verifies that our ssh process is still alive after detecting the port is taken, returning False if ssh exited (meaning the port belongs to someone else). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spawn_daemon uses subprocess.Popen which is not mocked in tests. On Windows, the spawned process inherits the test CWD (inside a temp dir), holding a directory handle that blocks pytest cleanup and causes WinError 32 in subsequent tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b4c214c34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| port = os.environ.get("DDA_BROWSER_PROXY_PORT", "") | ||
| if not port: | ||
| sys.exit(0) |
There was a problem hiding this comment.
Do not rely on Docker env inside SSH sessions
When commands are run through this dev env they go through SSH, and the repo's SSH config only sets TERM (src/dda/env/ssh.py), so DDA_BROWSER_PROXY_PORT from docker run -e is not propagated into those sessions. In that common path xdg-open reaches this branch and exits successfully without contacting the proxy, so browser opens from shells/commands inside the container become silent no-ops; the port needs to be embedded in the mounted script or otherwise made available to SSH sessions.
Useful? React with 👍 / 👎.
| "-L", | ||
| f"127.0.0.1:{callback_port}:localhost:{callback_port}", | ||
| "dd@localhost", |
There was a problem hiding this comment.
Fail the tunnel when local forwarding cannot bind
If the callback port is already in use on the host, OpenSSH does not exit by default (ssh -G localhost reports exitonforwardfailure no), so this ssh -N process can stay alive even though -L 127.0.0.1:<callback_port> was not installed. _wait_for_port_bound then treats the pre-existing listener as success and opens the browser, causing the OAuth callback to hit the wrong local service instead of the container; add ExitOnForwardFailure=yes or otherwise verify that this process actually owns the forward.
Useful? React with 👍 / 👎.
Without ExitOnForwardFailure=yes, ssh -N stays alive even if -L fails to bind, so _wait_for_port_bound (which checks proc.poll() is None after seeing EADDRINUSE) incorrectly treated a pre-existing listener as our own tunnel, opening the browser and sending the OAuth callback to the wrong local service. Fix: use -F /dev/null to suppress user SSH configs (the earlier reason for removing ExitOnForwardFailure was that ~/.ssh/workspaces config adds a failing reverse-forward that kills SSH), then re-add ExitOnForwardFailure=yes so SSH exits immediately on bind failure. Also add _active_tunnels dict + lock so concurrent /open requests for the same callback port share the existing tunnel instead of racing to start a second SSH process. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker -e variables are not propagated into SSH sessions (sshd only passes TERM per the SSH config), so DDA_BROWSER_PROXY_PORT was always empty in shells/commands run via `dda env dev shell/run`, causing xdg-open to silently exit without contacting the proxy. Fix: bake the port directly into the generated script at container start time. The script is already per-container, so the port is always correct and available regardless of how the session was opened. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Automatically forwards browser connections from inside a dev container to the local laptop, including OAuth callback support.
dda env dev startruns, listening on a deterministic port derived from the container name (same scheme as SSH/MCP ports)xdg-openscript into the container at/usr/local/bin/xdg-openand setsBROWSER=xdg-open+DDA_BROWSER_PROXY_PORT, so any tool callingxdg-openor$BROWSERworks automaticallyredirect_uri=http://localhost:{port}/...and sets up an SSH local port forward (127.0.0.1:{port}on host →localhost:{port}in container) before opening the browser, so the auth callback reaches the service running inside the container (e.g.ddtool auth gitlab login)--add-host host.docker.internal:host-gatewaytodocker runfor Linux host compatibility (it is automatic on Docker Desktop for Mac/Windows)spawn_daemon+ PID file pattern as the MCP server manager; daemon is started onstartand killed onstop