Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ talks to over sockets.
3. The daemon builds a `machine.Spec` (CPUs, memory, the OCI rootfs disk, the
network, a vsock device, the serial log), asks the `machine` library for
the right backend, and boots the VM. It also brings up gvproxy and, on
macOS, the agent proxy and ssh-agent proxy.
macOS, the agent proxy, the ssh-agent proxy, and the reverse-forward proxy.

`clawk` / `clawk run <runner>` then connect to the **in-guest pty-agent over
vsock** (AF_VSOCK port 1024). Each attach spawns a fresh child in the guest and
Expand Down Expand Up @@ -63,6 +63,17 @@ adds the filter hooks). Two attachment modes, abstracted by `machine.UserMode`:
between gvproxy's unixgram socket and the guest's TAP across an IP-less L2
bridge (`machine/firecracker/usermode_linux.go`).

Port forwards go both ways, by two different mechanisms. Outbound
(`clawk forward add`) is a gvproxy binding on the host's loopback, fixed when
the VM starts. Inbound (`clawk forward add-reverse`) can't be: a guest process
dialling `127.0.0.1` reaches the guest's own loopback, with no route to the
host's. So the guest agent binds the port itself and tunnels each connection
over vsock to the daemon, which validates the requested port against the
sandbox's configured set before dialling the host service (`internal/revfwd`
for the protocol, `internal/cli/reverse_forward.go` for the host end). The
daemon pushes set changes down the same channel, so those edits apply to a
running guest. vz only — firecracker's vsock is one-way.

Live allow-list edits reach the running daemon over a control socket
(`internal/vzdctl`); when the sandbox is down they apply on the next `up`.
The same socket carries the VM lifecycle verbs: `clawk pause` / `resume`
Expand Down Expand Up @@ -93,6 +104,7 @@ because it pins a vendored `gvisor-tap-vsock` fork; everything clawk-specific
| `internal/template` | `clawk.mod` lexer + parser (typed `sandbox` / `policy` / `namespace` blocks). |
| `internal/agentembed` | The in-guest binaries (clawk-init, pty-agent, time-sync), cross-compiled and injected into the rootfs. |
| `internal/vsockproto` / `internal/vsockclient` | The host↔guest vsock framing and the host-side client. |
| `internal/revfwd` | Reverse-forward wire protocol (host loopback services exposed on the guest's loopback), mirrored in the guest agent. |
| `internal/netfilter` | Egress allow-list (IPs/CIDRs/domains, DNS-aware) consumed by gvproxy. |
| `internal/vzdctl` | Daemon control socket (live policy edits, denial ledger, VM pause/resume/suspend). |
| `internal/worktree` / `internal/pr` | Multi-repo branch coordination and PR creation. |
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ tagged.

### Added

- **Reverse port forwarding: host loopback services, reachable in the guest.**
`clawk forward add-reverse <sandbox> 63342` makes whatever is bound to
`127.0.0.1:63342` on your Mac answer at the same address inside the sandbox
(`5432:15432` maps across ports, host-side first, same as `forward add`).
Allow-listing couldn't do this — `127.0.0.1` in the guest is the guest's own
loopback — so the guest agent binds the port and tunnels each connection to
the daemon over vsock, which dials the host service. Only the ports you list
are reachable; the rest of your loopback isn't.

Unlike outbound forwards these apply to a running sandbox immediately, which
matters for the case that motivated it: the Claude Code IDE plugins
advertise a per-window websocket port in `~/.claude/ide/<port>.lock`, so
reconnecting after an IDE restart is one `add-reverse`, not a VM cycle.
Share `~/.claude/ide` into the guest and `/ide` works from inside the
sandbox — recipe in [docs/networking.md](docs/networking.md#recipe-the-claude-code-ide-plugin).

Declarable in `clawk.mod` as a `reverse` entry inside `forwards ( … )`.
`clawk status` and `forward list --json` show both directions. vz only:
firecracker's vsock is one-way, and the CLI says so rather than silently
doing nothing.

- **Linux firecracker sandboxes boot without sudo.** Each sandbox's network
now lives in its own unprivileged user + network namespace, where clawk has
`CAP_NET_ADMIN` over its own bridge and TAPs without asking the host for
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ Anthropic, …) are pre-allowed, and the filter is DNS-aware, so allowing
clawk network allow my-project api.stripe.com '*.internal.mycorp.com' 10.0.0.5
clawk network denials my-project # what the agent tried that got blocked
clawk forward add my-project 3000 # localhost:3000 → the guest's dev server
clawk forward add-reverse my-project 63342 # and the other way: a service on YOUR
# localhost, reachable inside the guest
```

Denials are recorded by the *hostname the guest resolved*, so `clawk network
Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ without notice.
| Provider | Host | Notes |
|--------------------|-------|-----------------------------------------------------------------------|
| `vz` (default) | macOS | Apple Virtualization.framework; no sudo. Live-mounts your worktree. |
| `firecracker` (experimental) | Linux | KVM microVM; no sudo on hosts that allow unprivileged user namespaces. Carries the worktree on its own disk (host edits don't propagate live), and skips host-file push, ssh-agent forwarding, and per-phase hooks today. |
| `firecracker` (experimental) | Linux | KVM microVM; no sudo on hosts that allow unprivileged user namespaces. Carries the worktree on its own disk (host edits don't propagate live), and skips host-file push, ssh-agent forwarding, reverse port forwards, and per-phase hooks today. |

Pick one with `--provider`; the choice persists with the sandbox. Both run the
same OCI rootfs, vsock agent, and egress allow-list — see
Expand Down
7 changes: 6 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ sandbox my-project (
forwards (
3000
5432:5432
reverse 63342 # the host's localhost:63342, inside the guest
)

files (
Expand Down Expand Up @@ -117,7 +118,11 @@ sandbox my-project (
- `network ( … )` — egress policy: `allow` / `deny` a domain or `ip <addr>`,
plus `use <policy>…` chains — see
[Networking](networking.md#policies-and-use-chains).
- `forwards ( … )` — port forwards (`PORT` or `HOST:GUEST`).
- `forwards ( … )` — port forwards (`PORT` or `HOST:GUEST`). An entry
prefixed with `reverse` points the other way: a service on the host's
`127.0.0.1` becomes reachable at the same address inside the guest. Same
host-first spelling either way — see
[Networking](networking.md#reverse-forwarding-host-loopback--guest).
- `env ( … )` — environment variables to export inside the VM. Secret
*values* come from your shell at boot and are never written to disk on the
host; only names, defaults, and literals live in the file. Each entry uses
Expand Down
3 changes: 3 additions & 0 deletions docs/linux-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ The egress allow-list is enforced in gvproxy on the host, so the guest cannot
turn it off from the inside — that holds in rootless mode too (verified: a
blocked host is refused and logged, `acl: denied example.com`).

Reverse forwards (`clawk forward add-reverse`) are vz-only: they tunnel over
a host-side vsock listener, and firecracker's vsock only runs the other way.

---

## 5. Limits
Expand Down
59 changes: 59 additions & 0 deletions docs/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,62 @@ interface is not visible to the host.
Note that an idle-stopped VM's port forwards go away until the next boot —
give a sandbox that must keep serving `idle_timeout off` (see
[Commands & resource usage](commands.md#resource-usage)).

### Reverse forwarding (host loopback → guest)

The other direction: a service bound to `127.0.0.1` on your Mac, reachable
at the *same address* inside the guest. Allow-listing its IP doesn't help —
`127.0.0.1` in the guest is the guest's own loopback, and there is no route
from there to yours.

```sh
clawk forward add-reverse <sandbox> 63342 # guest 127.0.0.1:63342 → host 127.0.0.1:63342
clawk forward add-reverse <sandbox> 5432:15432 # guest 127.0.0.1:15432 → host 127.0.0.1:5432
clawk forward remove-reverse <sandbox> 63342
```

Specs read host-side first in both directions, so `5432:15432` names the
same pair of ports whichever verb you use — only who dials whom changes.

Two things differ from outbound forwards:

- **They apply immediately.** Outbound forwards are a gvproxy binding fixed
at VM start; reverse forwards are tunnelled over vsock by the daemon and
pushed to the running guest, so no `down`/`up` cycle is needed.
- **Only the listed ports are reachable.** The guest names a port, never an
address, and the host refuses one that isn't configured — so this opens
exactly the holes you asked for, not your whole loopback.

vz only. firecracker's vsock is one-way (guest listens, host dials), so
there is nothing for the guest to connect back through; the CLI says so
rather than silently doing nothing.

Reverse forwards can also be declared in `clawk.mod` — see
[Configuration](configuration.md#reference).

### Recipe: the Claude Code IDE plugin

The JetBrains and VS Code plugins run a websocket server on the host's
loopback and advertise it in `~/.claude/ide/<port>.lock`. A `claude` running
inside a sandbox needs two things to find it — the lock file, and a route to
the port:

```sh
# 1. share the host's lock-file dir into the guest (clawk.mod, or clawk apply)
# shares (
# ~/.claude/ide /home/agent/.claude/ide ro
# )

# 2. reverse-forward the port the lock file names
ls ~/.claude/ide # 63342.lock
clawk forward add-reverse my-project 63342
```

Then `/ide` inside the sandbox connects as it would on the host. The port is
per-IDE-window and changes when the IDE restarts; because reverse forwards
apply live, re-running `add-reverse` with the new port is enough — no
sandbox restart.

Note that the guest's `~/.claude` is the sandbox's own state directory, not
your host `~/.claude`; the share above is what puts the host's lock files
where the guest's `claude` looks.
Loading
Loading