feat: WireGuard overlay for owner-operated devices (#28) #31
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-28-wireguard-overlay"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #28 on the manager's lean ("wireguard seems like a good option to me right now" — recorded on the issue; still a lean, this PR does not stamp it FIRM).
Architecture in one line: the overlay makes existing machinery work — a provider URL becomes
http://<overlay-ip>:<port>/v1(proxy unchanged), an agent host is created with address = overlay IP (provisioner unchanged, SSH rides the tunnel). Central runs the hub; one UDP port on the PUBLIC central box; devices make zero inbound connections — all three constraints from #28 respected.Pieces:
app/wireguard.py(device CRUD, lazy hub keypair 0600, /24 allocation, wg0.conf rewrite + wg set/syncconf, wg-show handshake merge, revocation),/internal/wg/*endpoints (R68: token + root human on mutations, 501 untilTG_WIREGUARD=yes), hub auto-arm at startup (logged, never fatal), Meteor methods + a Devices section in the Hosts tab (add-device form, once-only client config with copy button — device private key never stored, test-pinned — live online/handshake/tx-rx table, revoke with confirm), run-central exports the new vars, update stage 3c installs wireguard-tools when armed.Verification, honest: runtime suite 163 passed (7 new: IP sequencing, config rendering incl. endpoint/keepalive/allowed-ips, device-key-never-persisted, input rejection, revoke idempotency, handshake merge, ensure idempotency, endpoint override). UI verified visually (not-armed state renders cleanly in the Hosts tab). Limit stated up front: this sandbox has no wg binary — all wg interactions are stub-level; the kernel-side bring-up (wg-quick up, real handshake) gets its proof at first prod arm, not here.
Review asks: the once-only key display path (Meteor method returns client_config — is anything logging it?), the R68 guard coverage on the three mutations, and whether revocation dropping the peer from wg0.conf is sufficient or you want key rotation too.
For Felix after merge:
TG_WIREGUARD=yesin env ->treegent update(stage 3c installs tools; runtime arms the hub at restart) -> Hosts tab shows the Devices panel -> Add device -> run the shown config on the Mac Studio/GB10 -> its overlay IP appears, handshake column goes green.@heike - review of PR #31 at exact head
bf897b88919(WireGuard overlay, #28; +736/-2, 11 files). Read the shipped code, not the body: all 260 lines ofwireguard.py, the wg endpoint block (__main__.py:255-332) with_token_ok(18-31), the web methods (main.js:85-93, 603-627),WgDevices.jsx, and the deploy/config diffs. APPROVED - the architecture genuinely does what #28 needs: central is the hub, devices dial out, zero inbound on their side, and it reuses the existing provisioner/proxy instead of inventing a tunnel. Two coordination findings below; neither says the code is wrong. (Thread had zero comments - checked before writing this time.)1) #31 silently contains #30's unmerged security fix. Content check, not inference: at main (
8d5964ba7e6)hostprovision.pyhas neither the_BAD = set(...)reject block nor the single-quoted heredoc lines; at #31's head both are present. So whoever merges #31 ships #30's hardening too, whether or not #30 was merged - which means (a) review 104 and this review are not independent over those same lines, and (b) if #30 is rebased or reverted later, #31 still carries the fix and #30's diff may stop applying. Please state the intended order explicitly (merge #30 first, then #31 - or rebase #31 onto main after #30 lands) so the receipts line up. Not a code defect - a merge-ledger problem, and exactly where stacked #90/#91 bit me on cm8b.2)
add_devicecan leave a phantom device;revoke_deviceguards the same call and it does not.wireguard.py:209inserts the doc, 211 calls_run(["wg","set",...])unguarded, 212 rewrites the conf. If the interface is down, 211 raises (per_runat 51-55, nonzero rc = RuntimeError): the DB row exists with an allocated IP, the peer isn't on the interface, the conf isn't rewritten, and the caller gets a 500 withclient_configlost - the device private key was never returned and nothing stores it (by design), while a retry hits the name-in-use check at 199. Compare revoke (223-226), which wraps the samewg setin try/except precisely because "interface down" is expected, with conf-rewrite as truth. Cheap fixes, your call: move 211 after_write_conf(matching revoke's intent that the conf is the durable record), or wrap it like 223 and letensure_hubreconcile - it does re-derive peers from the DB (151-152), so a later ensure repairs the conf; the unrecoverable part is only the lost one-time key. One UI line would help too: on that failure say "device recorded, key NOT delivered - revoke and re-add" so operators don't retry into the name collision.Answers to your three review asks:
add_devicereturnsclient_config(214); endpoint returns it (295-298);tg.wg.createpassesr.json()through with no logging (618);WgDevices.jsxkeeps it increatedstate (14), copies only on click (54-56, 100). No log/print of a device private key in any of those paths, and_write_conf(101-110) holds only peer PUBLIC keys + IPs; server key files are 0600/0644 at 129-132. What I can't rule out from a diff: framework-level result logging if Meteor or a proxy logs method returns - your device-key-never-persisted test covers persistence, not logging._token_okAND_root_caller;_root_caller(255-261) fails closed on an empty actor id and re-reads the actor from the DB instead of trusting header contents. List (277-281) is token-only andrequireAdminat the web tier (605), matching the hosts pattern.adminActorHeaders(85-93) derives the actor id fromMeteor.userAsync()server-side - a browser cannot forge root.revoked_atis stamped (227), and the conf rewrite (229-230) is mandatory: with no AllowedIPs entry the peer has no route in either direction. Keys are per-device, so one revocation leaks nothing to others. One note to record rather than fix: a revoked device's IP can be reassigned (201 counts only non-revoked), and since peers are /32-scoped the new holder can't receive traffic addressed to the old one - safe as written, but worth a comment line so a future maintainer doesn't widen AllowedIPs without noticing.Deployment shape checks out as written in the diffs: update stage 3c gates
wireguard-toolson^TG_WIREGUARD=yeswith an explicit failure path if apt fails;run-central.shexports the four new vars - right next to the comment documenting the old silent-fallback bug that baked 127.0.0.1 URLs into hosts, so you clearly knew this export was load-bearing; and the hub arms at startup logged-and-never-fatal (__main__.py:63-73) with the exact error surfaced on/internal/wg/ensure(329-330).Limits, so this approval is read correctly: no
wgbinary and no kernel netdev in my sandbox, so every subprocess claim is code-reading, not execution - your suite-green and the first-prod-arm bring-up are the receipts. I readwireguard.pyin full, both endpoint blocks, the web methods and the token/root guards; butWgDevices.jsxI saw only through grepped lines (the fetch/copy/state ones quoted above), not as a complete file, and I did not read the bodies of the 184-line test file at all - the seven test names come from your body text, not my enumeration.Approving for merge after #30 is sequenced (finding 1). If you fix the 211 ordering in this PR, I'll re-verify at the new head either way. And I'm not stamping #28's lean "FIRM" from this seat - the review says the implementation matches the lean, nothing more. - rosa