# open-proxy Forward `open` / `xdg-open` from a remote VM to the host machine that actually has the browser and GUI apps. You SSH/mosh into a VM, run `open report.html` or `open https://…`, and it pops open on your laptop instead of failing in a headless shell. ## How it works ``` VM SSH reverse tunnel Host (your laptop) ┌─────────────────┐ 127.0.0.1:7777 ───▶ 127.0.0.1:7777 │ open │ HTTP POST ─────────────────────▶ │ open-proxy serve │ │ (open-proxy) │ │ → xdg-open/open │ └─────────────────┘ └──────────────────┘ ``` - The **host** runs `open-proxy serve`, listening on `127.0.0.1:7777`. - The **VM** runs the `open-proxy open` client, which connects to `127.0.0.1:7777` _on the VM_. - An SSH **reverse tunnel** maps the VM's `127.0.0.1:7777` back to the host's server, so nothing is exposed on the network. Per argument, the client does the cheapest thing that works: | Argument | Behavior | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Existing **regular file** | Streams the bytes to the host; host writes it to a temp dir (keeping the filename/extension) and opens that copy. | | **URL** (`https://…`, `mailto:…`) | Forwarded as a string; host opens it. | | Anything else | Forwarded as a string; host's `open` decides. | > **Best effort, by design.** A copied `report.html` that references sibling > assets (`./style.css`, images) won't find them on the host — only the single > file is copied. URLs and self-contained files work great. ## Build ```sh go build -o open-proxy . ``` Produces one static binary used for both roles. ## Host setup ```sh # Optional but recommended if the VM is multi-user: require a shared secret. openssl rand -hex 16 > ~/.config/open-proxy-token export OPEN_PROXY_TOKEN_FILE=~/.config/open-proxy-token open-proxy serve # listens on 127.0.0.1:7777 ``` Run it under your login session (launchd/systemd user service) so it has access to the GUI. ## VM setup 1. Drop the binary on the VM and symlink it **early on `$PATH`** under the opener names so it shadows the system ones transparently: ```sh install -m755 open-proxy ~/.local/bin/open-proxy ln -sf open-proxy ~/.local/bin/xdg-open # most Linux apps call this ln -sf open-proxy ~/.local/bin/open # convenience / macOS habit ``` `~/.local/bin` must come _before_ `/usr/bin` in `$PATH`. The binary checks `argv[0]`, so a symlink named `xdg-open` or `open` runs in client mode. 2. If you set a token on the host, set the same one on the VM (e.g. via a secret-managed file): ```sh export OPEN_PROXY_TOKEN_FILE=~/.config/open-proxy-token ``` ## The tunnel The client just talks to `127.0.0.1:7777` on the VM, so you need a reverse tunnel from the host: ```sh ssh -R 7777:127.0.0.1:7777 vm ``` Or persist it in `~/.ssh/config`: ``` Host vm RemoteForward 7777 127.0.0.1:7777 ``` ### mosh mosh does **not** forward ports itself. Keep a plain SSH connection alive alongside mosh to carry the tunnel: ```sh ssh -fN -R 7777:127.0.0.1:7777 vm # background the tunnel mosh vm # then mosh as usual ``` (Or use `autossh` to keep the tunnel up.) ## Fallback when the host is unreachable If the tunnel is down or the host server isn't running, the client doesn't just fail — it falls back to the **real** opener. It walks `$PATH` for the next `xdg-open`/`open` that isn't this binary and runs that, so a shadowed VM still behaves sanely offline. Fallback fires only on a _connection_ failure. An HTTP rejection (e.g. a 403 from a token mismatch) is surfaced as an error, not silently opened locally. Both the fallback and the host's own opener resolve the _real_ `xdg-open`/`open` by skipping this binary on `$PATH`, so it's safe to shadow the opener names on the host too (e.g. a box that is both host and VM) without recursing. ## Usage ```sh open https://example.com # opens in host browser open report.pdf # copied to host, opens in host PDF viewer open ./diagram.png # copied to host, opens in host image viewer ``` ## Configuration | Variable | Default | Meaning | | ----------------------- | ------------------ | ------------------------------------------------------------------------- | | `OPEN_PROXY_ADDR` | `127.0.0.1:7777` | Client target / server listen address. | | `OPEN_PROXY_TOKEN` | _(unset)_ | Shared secret; if set on the server, clients must match. | | `OPEN_PROXY_TOKEN_FILE` | _(unset)_ | File containing the shared secret, used when `OPEN_PROXY_TOKEN` is unset. | | `OPEN_PROXY_MAXSIZE` | `104857600` (100M) | Max file transfer size in bytes (server-side). | ## Security notes - The server only binds loopback; the SSH tunnel is the trust boundary. - On a shared VM, any local user can reach the forwarded port — set `OPEN_PROXY_TOKEN` to gate it. - The server writes received files to a temp dir and opens them. Only forward to hosts/VMs you trust.