controlclaw 0.0.0-stage → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +377 -2
- package/dist/index.js +2501 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,378 @@
|
|
|
1
|
-
#
|
|
1
|
+
# controlclaw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connects a machine that runs OpenClaw to a ControlClaw firewall. After `connect`, OpenClaw's traffic
|
|
4
|
+
goes through the firewall, and the vm-agent lets the firewall manage OpenClaw's models and channels.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
controlclaw doctor # what this machine can do
|
|
8
|
+
# on the firewall: sudo -u controlclaw mitm-agent pair-code --name laptop → 4F7K-92QD-XM3P
|
|
9
|
+
sudo controlclaw connect --mode host --firewall http://fw.example.com:8801 --vm-agent ./vm-agent
|
|
10
|
+
# Pairing code from the firewall: (type 4F7K-92QD-XM3P; it is not shown)
|
|
11
|
+
controlclaw status
|
|
12
|
+
sudo controlclaw disconnect # undo all of it
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Linux only for now.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `doctor [--json]` | Shows the OS, root access, nftables, Docker, redsocks, DNS, Chrome, WireGuard and the vm-agent. Shows OpenClaw's CLI, version, config and how it runs. Says which modes this machine can do and how much each protects. |
|
|
22
|
+
| `connect --mode docker\|host\|proxy --firewall <url>` | Pairs with the firewall, then installs the vm-agent, the firewall's CA and the capture for the mode, and checks that it works. |
|
|
23
|
+
| `disconnect` | Undoes everything `connect` did and puts back what it changed. |
|
|
24
|
+
| `status [--json]` | Shows whether the capture, the vm-agent, and the firewall's proxy and DNS are up. Exits 1 if one is not. |
|
|
25
|
+
|
|
26
|
+
Options for `connect`:
|
|
27
|
+
|
|
28
|
+
| Option | Default | What it is |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `--firewall <url>` | | The firewall's pairing address, such as `http://fw.example.com:8801`. Its host is also the last default for `--proxy` and `--dns`. |
|
|
31
|
+
| (asked) | | The one-time code from the firewall (`mitm-agent pair-code`), typed when `connect` asks. It brings the firewall's CA and key, and the agent's id. |
|
|
32
|
+
| `--code -` / `--code-file <file>` | | The code from stdin, or from a file (keep it 0600). `--code <code>` is refused: every user of the machine can read a command line. |
|
|
33
|
+
| `--name <name>` | this machine's hostname | The agent's name on the firewall, when the code has none (`pair-code --name` wins). |
|
|
34
|
+
| `--ca <file>` | | Without a code: the firewall's CA certificate (PEM). The firewall re-signs HTTPS with it. |
|
|
35
|
+
| `--firewall-key <pem>` | none | Without a code: the firewall's public key, as a file or the PEM itself. Without it the vm-agent answers nothing. |
|
|
36
|
+
| `--proxy <host:port>` | what pairing says, else the firewall's host, port 8080 | The firewall's proxy. |
|
|
37
|
+
| `--dns <ip>` | what pairing says, else the firewall's host | The firewall's DNS server (host and docker modes). |
|
|
38
|
+
| `--user <name>` | found | The user OpenClaw runs as. By default the CLI uses the owner of the running gateway, or else the one user with `~/.openclaw/openclaw.json`. |
|
|
39
|
+
| `--vm-agent <path>` | `$CC_VM_AGENT`, then PATH | The vm-agent binary. There is no release to download yet. |
|
|
40
|
+
|
|
41
|
+
## Pairing
|
|
42
|
+
|
|
43
|
+
On the firewall, as its user, make a code for one machine:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sudo -u controlclaw mitm-agent pair-code --name laptop # or: docker compose exec -u controlclaw firewall mitm-agent pair-code
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It prints a code such as `4F7K-92QD-XM3P`. It works once, within 10 minutes (`--ttl` up to 60).
|
|
50
|
+
At most 20 codes wait at a time. The admin API makes them too (`POST /api/pairing/codes`).
|
|
51
|
+
|
|
52
|
+
What only the admin decides, on the code:
|
|
53
|
+
- `--address <ip>`: the address the proxy will see the machine at, when it is not the one pairing
|
|
54
|
+
comes from (a NAT, a container on the firewall's machine). Without it, the agent's address is the
|
|
55
|
+
one its request comes from, and that must be the address the machine says it has.
|
|
56
|
+
- `--replace`: the machine may take the place of an agent already at its address. Without it,
|
|
57
|
+
pairing at an address that has an agent is refused.
|
|
58
|
+
|
|
59
|
+
`connect` checks first that the machine can do the mode, so a code is not spent on a machine
|
|
60
|
+
that cannot. Then it pairs:
|
|
61
|
+
- it makes the vm-agent's key pair;
|
|
62
|
+
- it sends the public half to the firewall's `/pair` (port 8801);
|
|
63
|
+
- the firewall records the agent (its id, name, key, and the address the proxy will see it at);
|
|
64
|
+
- the firewall answers with its own public key, its CA, the agent's id and the proxy's and DNS
|
|
65
|
+
addresses.
|
|
66
|
+
|
|
67
|
+
`connect` writes the key and the id into the vm-agent's config, and installs the CA. It prints the
|
|
68
|
+
CA's SHA-256, which the firewall's log shows at start, so you can compare them. The proxy knows the
|
|
69
|
+
agent at its next sync, within a minute.
|
|
70
|
+
|
|
71
|
+
Where the firewall calls the vm-agent (to set a bot or a model, or for the emergency stop): pairing
|
|
72
|
+
tells the firewall (`agent_listen`), and the vm-agent listens there for the firewall's address alone
|
|
73
|
+
(`allow_from`; any other connection is closed). The calls are plain HTTP with the firewall's signed
|
|
74
|
+
tokens, as the SaaS sends them over HTTPS.
|
|
75
|
+
|
|
76
|
+
| Mode | The vm-agent listens on | Who may connect |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| host, proxy, docker, the firewall in a container on this machine (behind one of its Docker bridges, or published on a bridge's address such as `172.17.0.1`) | this machine's address on that bridge, port 3100 | the address the firewall calls from (pairing's `calls_from`: its container's) |
|
|
79
|
+
| host, proxy, the firewall installed on this machine itself (`install.sh`) | loopback only: the vm-agent would listen on the firewall's own address, which pairing refuses | nobody |
|
|
80
|
+
| host, proxy, docker, the firewall elsewhere | loopback only: the firewall does not call it (plain HTTP across a network) | nobody |
|
|
81
|
+
| remote | this machine's tunnel address, port 3100 | the firewall's tunnel address |
|
|
82
|
+
| `up --local` | `172.31.252.1:3100`, this machine on the `outside` network | the firewall's container (`172.31.252.2`) |
|
|
83
|
+
|
|
84
|
+
Gotchas:
|
|
85
|
+
- With the firewall on another machine, use `--mode remote` for bots, models and the emergency stop:
|
|
86
|
+
the other modes capture traffic, but the firewall cannot reach the vm-agent there. `connect` says so.
|
|
87
|
+
- Agents paired before this release have no address for the firewall's calls: run `connect` again.
|
|
88
|
+
- `allow_from` is the firewall container's address at pairing. The Docker setup in
|
|
89
|
+
`standalone/docker/compose.yaml` gives the firewall a fixed one (`172.31.249.2`); a firewall
|
|
90
|
+
started with a plain `docker run` on Docker's default bridge can get another address when it is
|
|
91
|
+
recreated: run `connect` again then.
|
|
92
|
+
- MCP servers and app connections go to the firewall's gates directly (3940 and 3900, the
|
|
93
|
+
firewall's default ports): host and remote capture let OpenClaw's user reach them on the proxy's
|
|
94
|
+
address, docker capture relays them in its gate container (to a firewall whose gates are on an
|
|
95
|
+
address it reaches: not `standalone/docker/compose.yaml`'s, which does not publish them). Not in proxy mode (everything goes
|
|
96
|
+
through the proxy there, which never connects to the firewall itself), and app connections not for
|
|
97
|
+
an OpenClaw in a container yet.
|
|
98
|
+
- The emergency stop cannot start again an OpenClaw that was started by hand (not systemd, Docker
|
|
99
|
+
or pm2): start it yourself after the release.
|
|
100
|
+
- `allow_from` is the firewall's address as `connect` dialled it. When the firewall reaches this
|
|
101
|
+
machine from another address (a NAT, a port forward, a second network), every call is closed:
|
|
102
|
+
the vm-agent logs `closed a connection from <ip>`. Set `firewall.allow_from` in the config by hand.
|
|
103
|
+
- OpenClaw can still ask the proxy for the vm-agent's address; it then arrives from the firewall's.
|
|
104
|
+
An egress rule decides, as for any address, and the vm-agent still wants the firewall's signed
|
|
105
|
+
token.
|
|
106
|
+
|
|
107
|
+
How the code is safe on plain HTTP:
|
|
108
|
+
- The code is never sent. The request carries an HMAC made with it. The firewall's answer carries an
|
|
109
|
+
HMAC made with it too, and the answer must repeat a random value from the request.
|
|
110
|
+
`connect` takes the firewall's key and CA only from such an answer. Someone in the middle sees two
|
|
111
|
+
HMACs, and 60 bits cannot be tried offline within the code's lifetime.
|
|
112
|
+
- Guessing online is limited to ten wrong codes a minute from one address; after that the endpoint
|
|
113
|
+
answers 429 for the rest of the minute. There is no limit over all addresses, which anyone could
|
|
114
|
+
use to block every pairing: even a million guesses a minute for 10 minutes find a 60-bit code with
|
|
115
|
+
odds of about 1 in 10^11.
|
|
116
|
+
- Once a code has matched, any refusal spends it (make a new one) and counts as a wrong try, so a
|
|
117
|
+
refusal never tells a guesser that a code was right.
|
|
118
|
+
- The firewall keeps the plain code until it is used or expires, in its database, which only its
|
|
119
|
+
user can read.
|
|
120
|
+
- The request says the machine's own address toward the firewall, under the HMAC. A request that
|
|
121
|
+
arrives from another address is refused: someone who copied it on the way cannot pair from their
|
|
122
|
+
own machine. Behind a NAT or a reverse proxy the two differ, so the admin names the address on
|
|
123
|
+
the code (`--address`). The CLI pairs over the IPv4 address it measured its own address toward
|
|
124
|
+
(for `http://`), so the two match.
|
|
125
|
+
- The agent cannot choose its address, and cannot take another agent's place unless the code says
|
|
126
|
+
`--replace`. OpenClaw on that machine (in host mode it still has the internet until the capture
|
|
127
|
+
is in) could read a code from a command line and pair first; it would get the same address and
|
|
128
|
+
nothing more, and the real `connect` would then fail. That is why the code is never taken from
|
|
129
|
+
the command line.
|
|
130
|
+
- Two machines behind one NAT look like one to the proxy. Give each its own address the proxy sees
|
|
131
|
+
(WireGuard remote mode will do that).
|
|
132
|
+
- The address must be one the proxy can see an agent at: not loopback, unspecified, multicast,
|
|
133
|
+
broadcast or the firewall's own.
|
|
134
|
+
- As root, `connect` writes nothing into a folder a user owns (OpenClaw's home: the vm-agent's
|
|
135
|
+
keys, the systemd drop-in) and follows no link a user owns: those writes, and their undo, run as
|
|
136
|
+
that user. A root folder on the way that the group or everyone may write to (a shared project
|
|
137
|
+
folder) stops `connect`, which says which folder to `chmod go-w`.
|
|
138
|
+
|
|
139
|
+
Unpairing: pairing leaves a random secret in `connect.json` (the firewall keeps only its hash).
|
|
140
|
+
`disconnect`, and a `connect` that fails after pairing, send it to the firewall's `/unpair`, which
|
|
141
|
+
forgets the agent, so the machine can pair again later without `--replace`. When the firewall cannot
|
|
142
|
+
be reached, they say so; remove the agent there by hand (`PUT /api/agents`).
|
|
143
|
+
|
|
144
|
+
## Modes
|
|
145
|
+
|
|
146
|
+
| Mode | How | Protection |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `docker` | OpenClaw's container moves to a network with no way out except the firewall. | Strong: the container cannot reach the network without the firewall. |
|
|
149
|
+
| `host` | nftables catch every packet from OpenClaw's user. | Limited: anyone with root or sudo on the machine can remove the rules. |
|
|
150
|
+
| `proxy` | OpenClaw's own proxy setting. | Lets you see OpenClaw's own requests and block them. Shell commands and other programs go around it. |
|
|
151
|
+
| `remote` | WireGuard to the firewall, then host mode's rules through the tunnel. | Like host mode on the machine (root or sudo can remove the rules), from any network, with the tunnel address as its identity. |
|
|
152
|
+
|
|
153
|
+
### docker (needs root, Docker 28 and Compose 2.24.4 or newer)
|
|
154
|
+
|
|
155
|
+
For an OpenClaw started with Docker Compose, for example OpenClaw's own `docker-compose.yml`.
|
|
156
|
+
`connect` stops in these cases:
|
|
157
|
+
- other services of that Compose project run with the gateway: the gateway leaves its network, so
|
|
158
|
+
they would lose it;
|
|
159
|
+
- the gateway could get out on its own:
|
|
160
|
+
- it mounts a container runtime's socket (`docker.sock`, containerd, podman) or a folder that holds
|
|
161
|
+
one (`/`, `/run`, `/var`, a user's `/run/user/<uid>` for rootless Docker or podman);
|
|
162
|
+
- it is `privileged`;
|
|
163
|
+
- it has `NET_ADMIN`, `NET_RAW` or `SYS_ADMIN`;
|
|
164
|
+
- it uses the host's network.
|
|
165
|
+
|
|
166
|
+
The error says which; remove that from its compose file first.
|
|
167
|
+
|
|
168
|
+
- It writes an override, `/etc/controlclaw/docker/compose.controlclaw.yml`, and runs
|
|
169
|
+
`docker compose ... up -d` for the gateway service only. Your compose files are not changed.
|
|
170
|
+
- The gateway container goes onto a new network, `<project>_controlclaw`, with `internal: true`.
|
|
171
|
+
Its bridge gives this machine no address (`gateway_mode_ipv4: isolated`, Docker 28): an internal
|
|
172
|
+
network otherwise still lets its containers reach every service of the host that listens on
|
|
173
|
+
`0.0.0.0` (sshd, a resolver, a dev server). `status` checks it.
|
|
174
|
+
It has no route out.
|
|
175
|
+
- A small `controlclaw-gate` container (socat) sits on that network and on the normal one. It sends
|
|
176
|
+
port 8080 to the firewall's proxy and port 53 to the firewall's DNS. It also publishes the ports
|
|
177
|
+
the gateway published before, such as the Control UI on 18789. Docker publishes no ports for a
|
|
178
|
+
container that is only on an internal network, so the gate has to.
|
|
179
|
+
- The gateway gets `OPENCLAW_PROXY_URL`, `HTTP(S)_PROXY` and `NODE_USE_ENV_PROXY=1`. The CA is
|
|
180
|
+
added in two ways:
|
|
181
|
+
- `NODE_EXTRA_CA_CERTS`;
|
|
182
|
+
- a CA bundle mounted over `/etc/ssl/certs/ca-certificates.crt`: the image's own bundle plus the
|
|
183
|
+
firewall's CA.
|
|
184
|
+
- Anything that ignores the proxy (a direct IP, another DNS server, UDP) has nowhere to go.
|
|
185
|
+
- After a reboot, Docker brings both containers back the same way.
|
|
186
|
+
- `docker compose up -d` in the project's folder (an OpenClaw update, say) keeps the capture:
|
|
187
|
+
`connect` adds `COMPOSE_FILE=<your files>:<the override>` to the project's `.env`, and
|
|
188
|
+
`disconnect` takes it out.
|
|
189
|
+
- If your `.env` already sets `COMPOSE_FILE` or `COMPOSE_PATH_SEPARATOR`, it is left alone and
|
|
190
|
+
`connect` tells you to add the override yourself.
|
|
191
|
+
- Run with `-f` and without the override, the gateway goes back on its open network. `status`
|
|
192
|
+
shows it.
|
|
193
|
+
|
|
194
|
+
If the firewall runs on the same machine, it must listen where containers can reach it, such as
|
|
195
|
+
the `docker0` address (`172.17.0.1`), and `--proxy` and `--dns` must name that address. `connect`
|
|
196
|
+
refuses `127.0.0.1`: inside the gate it means the gate itself.
|
|
197
|
+
|
|
198
|
+
The network uses `172.31.250.0/24`. If another Docker network already uses it, `connect` stops.
|
|
199
|
+
|
|
200
|
+
### host (needs root and systemd)
|
|
201
|
+
|
|
202
|
+
For an OpenClaw installed on the machine, for example with the official installer. It runs as a
|
|
203
|
+
systemd user unit.
|
|
204
|
+
|
|
205
|
+
- `/etc/controlclaw/capture/capture.nft` holds the rules for OpenClaw's uid only:
|
|
206
|
+
- TCP goes to redsocks, which hands it to the firewall's proxy;
|
|
207
|
+
- DNS (port 53, UDP and TCP) goes to the firewall, whatever server was asked, except
|
|
208
|
+
systemd-resolved's own stub (127.0.0.53), which itself asks only the firewall (below). A query
|
|
209
|
+
from 127.x sent on to another machine would be dropped as martian;
|
|
210
|
+
- IPv6, ICMP and other UDP are refused;
|
|
211
|
+
- on the machine itself (127.0.0.0/8, ::1) only these TCP ports are allowed:
|
|
212
|
+
- redsocks and the vm-agent (3100);
|
|
213
|
+
- OpenClaw's own, derived from the gateway port as OpenClaw 2026.10 does: the gateway, browser
|
|
214
|
+
control (+2), the extension relays (+3 to +10) and the default browser profile's CDP (+11),
|
|
215
|
+
so 18789–18800 by default;
|
|
216
|
+
- the CDP port each other browser profile has in OpenClaw's config. If that config is not
|
|
217
|
+
plain JSON (JSON5), its profiles cannot be read, so the whole range OpenClaw uses (to +110)
|
|
218
|
+
stays open.
|
|
219
|
+
|
|
220
|
+
Anything else listening there is refused, so that sshd with the user's keys, a local proxy or a
|
|
221
|
+
resolver on `::1` cannot be a way out. A root program that listens on one of those ports would
|
|
222
|
+
still be reachable. A browser profile made after `connect` gets a new CDP port, which is refused
|
|
223
|
+
until you run `disconnect` and `connect` again.
|
|
224
|
+
|
|
225
|
+
OpenClaw's Codex relay (the ChatGPT/Codex provider) listens on a random port and does not work in
|
|
226
|
+
host mode. Use docker mode for it.
|
|
227
|
+
- `controlclaw-capture.service` loads the rules at boot, before the network is up. It does not wait
|
|
228
|
+
for the firewall. If the firewall is down, OpenClaw cannot reach anything.
|
|
229
|
+
- `nftables.service` flushes the whole ruleset on start, restart, reload and stop. So ours loads
|
|
230
|
+
after it at boot, and a drop-in (`nftables.service.d/controlclaw.conf`) has it load ours again
|
|
231
|
+
after each of those. Anything else that flushes the ruleset (`nft flush ruleset`, `nft -f` of a file that
|
|
232
|
+
flushes) removes it; `status` shows when the table is missing.
|
|
233
|
+
- `controlclaw-redsocks.service` runs redsocks under its own user. `apt-get install redsocks` runs
|
|
234
|
+
if it is missing, and `disconnect` removes it again.
|
|
235
|
+
- The CA is added to the system store, to OpenClaw's service (a drop-in that sets
|
|
236
|
+
`NODE_EXTRA_CA_CERTS`) and to Chrome's stores, if Chrome and `certutil` are installed. OpenClaw is
|
|
237
|
+
restarted so it reads it.
|
|
238
|
+
|
|
239
|
+
resolved also gets `LLMNR=no` and `MulticastDNS=no`, and its user may not send to ports 5353 and
|
|
240
|
+
5355: otherwise a single-label or `.local` name would go onto the local network.
|
|
241
|
+
|
|
242
|
+
**The whole machine's DNS depends on the firewall.** Most programs look names up through
|
|
243
|
+
systemd-resolved, not port 53, so `connect` points resolved at the firewall
|
|
244
|
+
(`/etc/systemd/resolved.conf.d/controlclaw.conf`). It also allows resolved to ask only the firewall:
|
|
245
|
+
not a DNS server a network link got from DHCP, and not DNS over TLS. If the firewall stops, DNS stops for every
|
|
246
|
+
program on the machine, not only OpenClaw. `doctor` says so before you connect, and `status` shows
|
|
247
|
+
it when the firewall's DNS does not answer.
|
|
248
|
+
|
|
249
|
+
Host mode refuses to run in these cases:
|
|
250
|
+
- OpenClaw runs as root. The rules would then catch every root program. Run OpenClaw as its own user.
|
|
251
|
+
- OpenClaw's user is in the `docker` group (or `lxd`, `incus`, `libvirt`, `podman`). It could start a
|
|
252
|
+
container whose traffic dockerd sends, not the user, so the rules would never see it. Remove the
|
|
253
|
+
user from the group (`gpasswd -d <user> docker`), or use docker mode. If the user's groups cannot
|
|
254
|
+
be read (`id -nG` fails), host mode refuses too.
|
|
255
|
+
- systemd-resolved runs but has no `systemd-resolve` user: its queries could not be limited to the
|
|
256
|
+
firewall's DNS.
|
|
257
|
+
|
|
258
|
+
### remote (needs root, systemd, and a firewall with WireGuard)
|
|
259
|
+
|
|
260
|
+
For a machine that reaches the firewall over the internet: another cloud, a home server. The firewall
|
|
261
|
+
runs WireGuard (`install.sh --wireguard`). Pair with a code; `--ca` is refused here.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
sudo controlclaw connect --mode remote --firewall http://fw.example.com:8801 --vm-agent ./vm-agent
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- Before pairing, `connect` makes this machine's WireGuard key pair. Pairing sends the public half,
|
|
268
|
+
and the firewall answers with a tunnel address (its identity at the proxy, such as `10.77.0.2`),
|
|
269
|
+
its own WireGuard key and endpoint, and its tunnel address (`10.77.0.1`).
|
|
270
|
+
- `/etc/wireguard/cc-wg0.conf` and `cc-wg0.key` (root only), brought up by `wg-quick@cc-wg0` before
|
|
271
|
+
anything else, and `connect` waits (up to about 45 seconds) for the firewall's first answer. When
|
|
272
|
+
none comes (UDP to its endpoint blocked), it stops and undoes all of it. Only the
|
|
273
|
+
firewall's tunnel address goes into the tunnel: the machine's other traffic keeps its routes.
|
|
274
|
+
`wireguard-tools` is installed if missing, and removed by `disconnect`.
|
|
275
|
+
- Then the same capture as host mode, with the firewall at its tunnel address: redsocks to
|
|
276
|
+
`10.77.0.1:8080`, DNS to `10.77.0.1`, resolved pointed there. Two more rules in the table:
|
|
277
|
+
- after routing, nothing for the firewall's tunnel address leaves by another interface. With the
|
|
278
|
+
tunnel down, OpenClaw's traffic, its DNS and resolved's queries go nowhere: they never leave in
|
|
279
|
+
clear on the public interface;
|
|
280
|
+
- the vm-agent listens on this machine's tunnel address, and only the tunnel reaches it (and
|
|
281
|
+
there only the firewall's end, `allow_from`);
|
|
282
|
+
- what goes into the tunnel leaves from this machine's tunnel address (DNS that OpenClaw sends to
|
|
283
|
+
another server is DNATed into the tunnel, and the firewall takes only that address from it);
|
|
284
|
+
- nothing forwarded goes into the tunnel: a container on this machine (which Docker masquerades)
|
|
285
|
+
would otherwise reach the firewall as this agent.
|
|
286
|
+
The rules judge only OpenClaw's user's packets; the kernel's own (WireGuard's UDP) are not refused.
|
|
287
|
+
- Behind a NAT it works as it is: over WireGuard the identity is the tunnel address, not the address
|
|
288
|
+
pairing comes from. (A request copied on the way and replayed elsewhere would register a key whose
|
|
289
|
+
private half only this machine has.)
|
|
290
|
+
- `status` shows the tunnel and its last handshake.
|
|
291
|
+
- `--proxy` and `--dns` are refused: they would send the capture outside the tunnel, in clear.
|
|
292
|
+
- A machine set up again from scratch (its `connect.json` lost) gets a new WireGuard key and a new
|
|
293
|
+
tunnel address. The old peer stays on the firewall until the admin removes that agent (`PUT
|
|
294
|
+
/api/agents`, or the panel): `disconnect` removes it only when it still has the record.
|
|
295
|
+
- The limits of host mode apply: OpenClaw's own user, not root, not in the docker group, no sudo.
|
|
296
|
+
|
|
297
|
+
### proxy (no root needed)
|
|
298
|
+
|
|
299
|
+
For any OpenClaw on the machine, also on a machine without root.
|
|
300
|
+
|
|
301
|
+
- It sets OpenClaw's managed proxy: `proxy.enabled`, `proxy.proxyUrl` (the firewall's proxy) and
|
|
302
|
+
`proxy.loopbackMode: gateway-only`. `disconnect` removes the setting again.
|
|
303
|
+
- `connect` stops if OpenClaw already has a proxy setting, so that `disconnect` never loses one. It
|
|
304
|
+
reads the setting from OpenClaw's config file, because `openclaw config get` hides the URL. If that
|
|
305
|
+
file is not plain JSON (JSON5 with comments), `connect` stops too.
|
|
306
|
+
- `proxy.tls.caFile` is not used: it only verifies an `https://` proxy. To trust the firewall's CA,
|
|
307
|
+
OpenClaw's service gets `NODE_EXTRA_CA_CERTS`.
|
|
308
|
+
- It is part of OpenClaw's config, so it stays after a reboot. When the proxy is down, OpenClaw's
|
|
309
|
+
requests fail; they do not go out directly.
|
|
310
|
+
- `status` runs `openclaw proxy validate`.
|
|
311
|
+
|
|
312
|
+
## up --local: the firewall and OpenClaw on one machine
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
sudo controlclaw up --local --firewall-image controlclaw-firewall:dev-amd64 --vm-agent ./vm-agent
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Two containers in a Compose project, `controlclaw-local`, in `/opt/controlclaw-local` (`--dir`):
|
|
319
|
+
|
|
320
|
+
- **OpenClaw** (`ghcr.io/openclaw/openclaw:2026.10.9`, `--openclaw-image`) is only on an internal
|
|
321
|
+
network, `agents` (`172.31.251.0/24`, OpenClaw at `.10`), whose one other member is the firewall
|
|
322
|
+
(`.2`). Its proxy and DNS are the firewall's, with the CA in Node and in the system store (as in
|
|
323
|
+
docker mode). It has no other way out: the network's bridge gives this machine no address either
|
|
324
|
+
(`gateway_mode_ipv4: isolated`, so Docker 28 or newer), so this machine's own services are out of
|
|
325
|
+
its reach.
|
|
326
|
+
- **The firewall** (the image from `standalone/docker/build.sh`; none is published yet) is also on
|
|
327
|
+
`outside` (`172.31.252.0/24`). Its panel (8800) and pairing (8801) listen there only, so OpenClaw
|
|
328
|
+
cannot reach them. It publishes, on this machine's loopback only, OpenClaw's gateway (18789,
|
|
329
|
+
`CC_FORWARD`: Docker publishes nothing from an internal network), its panel, pairing and pages.
|
|
330
|
+
- It pairs itself:
|
|
331
|
+
- a code from the firewall with OpenClaw's address on it;
|
|
332
|
+
- pairing over the loopback;
|
|
333
|
+
- the vm-agent on this machine, as root, for the container, with the firewall's key and the
|
|
334
|
+
agent's id. It listens on this machine's side of `outside` (`172.31.252.1:3100`, after Docker
|
|
335
|
+
starts), where the firewall calls it, and takes connections from the firewall's container only.
|
|
336
|
+
OpenClaw, on `agents` only, has no route there of its own.
|
|
337
|
+
- OpenClaw is set up with a gateway token (in `.env`, 0600) on first run. Its Control UI's allowed
|
|
338
|
+
origins are set to `http://127.0.0.1:18789` and `http://localhost:18789` at each run: OpenClaw sees
|
|
339
|
+
the browser through the firewall's relay and accepts the page only from an origin it is told.
|
|
340
|
+
- It prints OpenClaw's address with its token, the panel's address, and the panel's first-run setup
|
|
341
|
+
token. From another machine: `ssh -L 8800:127.0.0.1:8800 -L 18789:127.0.0.1:18789 <machine>`.
|
|
342
|
+
- The firewall starts with every request blocked: allow your model provider in the panel first.
|
|
343
|
+
- `disconnect` stops the containers (`compose down`), unpairs, and removes the vm-agent and the files
|
|
344
|
+
it wrote. OpenClaw's data (`openclaw/`, `openclaw-secrets/`) and the firewall's volume stay; `up
|
|
345
|
+
--local` again picks them up.
|
|
346
|
+
- It needs root, Docker 28 and Compose 2.24.4 or newer. The two networks' subnets must be free.
|
|
347
|
+
|
|
348
|
+
## What disconnect does
|
|
349
|
+
|
|
350
|
+
`connect` writes each step to `/etc/controlclaw/connect.json` (`~/.config/controlclaw/` without
|
|
351
|
+
root). After a `connect` with sudo, run `disconnect` and `status` with sudo too; without it they say
|
|
352
|
+
so. `disconnect` undoes the steps in reverse:
|
|
353
|
+
|
|
354
|
+
- it puts back each file it changed, and removes the files and folders it created;
|
|
355
|
+
- it stops and removes the units;
|
|
356
|
+
- it restores OpenClaw's settings;
|
|
357
|
+
- it restarts OpenClaw last.
|
|
358
|
+
|
|
359
|
+
If `connect` fails halfway, it undoes itself the same way. When a step fails, it stays in
|
|
360
|
+
`connect.json` and the rest still run, so you can run `disconnect` again once the cause is fixed. A
|
|
361
|
+
step that can never work again (its file is gone) is given up with `disconnect --force`, which then
|
|
362
|
+
lists what to check by hand.
|
|
363
|
+
|
|
364
|
+
A file `connect` changed (a project's `.env`) gets its content, mode and owner back. If the vm-agent
|
|
365
|
+
binary was already at `/usr/local/bin/controlclaw-vm-agent`, it is used where it is and left there.
|
|
366
|
+
Someone else's binary there is set aside as `.before-controlclaw` and put back; if such a file is
|
|
367
|
+
there already (from a connect whose record was lost), `connect` stops rather than overwrite it.
|
|
368
|
+
|
|
369
|
+
## Gotchas
|
|
370
|
+
|
|
371
|
+
- **The CA needs a restart.** Node reads `NODE_EXTRA_CA_CERTS` only when it starts. `connect` restarts
|
|
372
|
+
OpenClaw. If OpenClaw runs with pm2 or as a plain process, add the variable to how you start it.
|
|
373
|
+
- **sudo.** If OpenClaw's user can use sudo, then in host mode OpenClaw, or its agent, can remove the
|
|
374
|
+
rules. `doctor` warns about it.
|
|
375
|
+
- **The vm-agent runs as OpenClaw's user** in host and proxy mode, and as root for Docker. See
|
|
376
|
+
[`rust/bins/vm-agent/UNMANAGED.md`](../../rust/bins/vm-agent/UNMANAGED.md).
|
|
377
|
+
- **The firewall must serve DNS** on port 53 for host and docker mode. Without it, `connect` still
|
|
378
|
+
runs, but `status` shows the DNS check failing.
|