Skip to content

Add browser proxy to automatically forward browser connections to local#281

Open
KevinFairise2 wants to merge 8 commits into
mainfrom
kfairise/browser-proxy-forwarding
Open

Add browser proxy to automatically forward browser connections to local#281
KevinFairise2 wants to merge 8 commits into
mainfrom
kfairise/browser-proxy-forwarding

Conversation

@KevinFairise2
Copy link
Copy Markdown
Member

@KevinFairise2 KevinFairise2 commented Jun 5, 2026

Summary

Automatically forwards browser connections from inside a dev container to the local laptop, including OAuth callback support.

  • Starts a lightweight HTTP daemon on the host when dda env dev start runs, listening on a deterministic port derived from the container name (same scheme as SSH/MCP ports)
  • Mounts a custom xdg-open script into the container at /usr/local/bin/xdg-open and sets BROWSER=xdg-open + DDA_BROWSER_PROXY_PORT, so any tool calling xdg-open or $BROWSER works automatically
  • For OAuth/auth flows: parses the URL for redirect_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)
  • Safe with multiple containers running simultaneously: all ports, PID files, and scripts are scoped to the container name (which includes the instance); a collision on the callback port is detected and handled gracefully
  • Adds --add-host host.docker.internal:host-gateway to docker run for Linux host compatibility (it is automatic on Docker Desktop for Mac/Windows)
  • Daemon lifecycle follows the same spawn_daemon + PID file pattern as the MCP server manager; daemon is started on start and killed on stop

…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>
@datadog-datadog-prod-us1
Copy link
Copy Markdown

datadog-datadog-prod-us1 Bot commented Jun 5, 2026

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 1 Pipeline job failed

docs | build   View in Datadog   GitHub Actions

See error Could not load inventory at https://jcristharif.com/msgspec/objects.inv due to HTTP 404 error.

ℹ️ Info

🎯 Code Coverage (details)
Patch Coverage: 23.68%
Overall Coverage: 70.10% (-0.83%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7bddfc8 | Docs | Datadog PR Page | Give us feedback!

KevinFairise2 and others added 3 commits June 5, 2026 14:15
…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>
@KevinFairise2 KevinFairise2 changed the title Add browser proxy to automatically forward browser connections to loc… Add browser proxy to automatically forward browser connections to local Jun 5, 2026
KevinFairise2 and others added 2 commits June 5, 2026 14:39
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>
@KevinFairise2 KevinFairise2 marked this pull request as ready for review June 5, 2026 12:55
@KevinFairise2 KevinFairise2 requested a review from a team as a code owner June 5, 2026 12:55
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +27 to +29
port = os.environ.get("DDA_BROWSER_PROXY_PORT", "")
if not port:
sys.exit(0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/dda/env/dev/browser_proxy.py Outdated
Comment on lines +128 to +130
"-L",
f"127.0.0.1:{callback_port}:localhost:{callback_port}",
"dd@localhost",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

KevinFairise2 and others added 2 commits June 5, 2026 15:17
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant