feat(forward): reverse loopback forwarding - #13
Merged
Conversation
`clawk forward add` exposes a guest port on the host; the other direction had no answer. A guest process that dials 127.0.0.1 reaches the guest's own loopback, and no allow-list entry can route that to the host's — so tools that assume both ends share a loopback cannot work in a sandbox at all. The case that prompted it (#10): the Claude Code IDE plugins advertise a websocket port in ~/.claude/ide/<port>.lock and claude connects to ws://127.0.0.1:<port>, with no knob to point it elsewhere. `clawk forward add-reverse <sandbox> <spec>` fills it in. The in-guest agent binds the port on the guest's loopback and tunnels each connection over vsock (port 1028) to the daemon, which checks the requested port against the sandbox's configured set before dialling the host service. The guest names a port and never an address, so only the listed ports are reachable — reaching host loopback through the gvproxy gateway NAT instead would have been all-or-nothing, which is the wrong default for an agent sandbox. Unlike outbound forwards these apply to a running sandbox: the daemon holds a control stream to the agent and pushes the complete set on every edit (POST /v1/reload-forwards on the existing control socket). That matters because the IDE port is per-window. Host dials try both loopback families — "localhost" resolves to ::1 first on macOS, so a server told to bind the name often listens on [::1] alone (`python3 -m http.server --bind localhost`, much Node tooling), and an IPv4-only dial made those look dead: the guest accepted the connection and immediately reset it. Adds internal/revfwd (wire protocol, mirrored by hand in the guest agent and guarded by a reflection-based lock-step test), the host proxy, the guest forwarder, Sandbox.ReverseForwards, a `reverse` entry inside clawk.mod's forwards block, status/JSON surfacing, and docs including the IDE recipe. The host proxy is transport-agnostic so its accept/handshake/bridge path is tested over TCP on any platform. vz only: firecracker's vsock is one-way, so the endpoint 404s and the CLI reports that rather than an apply that never happened. Fixes #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
clawk forward addexposes a guest port on the host; the other directionhad no answer. A guest process that dials 127.0.0.1 reaches the guest's own
loopback, and no allow-list entry can route that to the host's — so tools
that assume both ends share a loopback cannot work in a sandbox at all. The
case that prompted it (#10): the Claude Code IDE plugins
advertise a websocket port in ~/.claude/ide/.lock and claude connects
to ws://127.0.0.1:, with no knob to point it elsewhere.
clawk forward add-reverse <sandbox> <spec>fills it in. The in-guest agentbinds the port on the guest's loopback and tunnels each connection over
vsock (port 1028) to the daemon, which checks the requested port against the
sandbox's configured set before dialling the host service. The guest names a
port and never an address, so only the listed ports are reachable — reaching
host loopback through the gvproxy gateway NAT instead would have been
all-or-nothing, which is the wrong default for an agent sandbox.
Unlike outbound forwards these apply to a running sandbox: the daemon holds
a control stream to the agent and pushes the complete set on every edit
(POST /v1/reload-forwards on the existing control socket). That matters
because the IDE port is per-window. Host dials try both loopback families —
"localhost" resolves to ::1 first on macOS, so a server told to bind the
name often listens on [::1] alone (
python3 -m http.server --bind localhost, much Node tooling), and an IPv4-only dial made those look dead:the guest accepted the connection and immediately reset it.
Adds internal/revfwd (wire protocol, mirrored by hand in the guest agent
and guarded by a reflection-based lock-step test), the host proxy, the guest
forwarder, Sandbox.ReverseForwards, a
reverseentry inside clawk.mod'sforwards block, status/JSON surfacing, and docs including the IDE recipe.
The host proxy is transport-agnostic so its accept/handshake/bridge path is
tested over TCP on any platform. vz only: firecracker's vsock is one-way, so
the endpoint 404s and the CLI reports that rather than an apply that never
happened.
Fixes #10