WireGuard bring-up: host setup script, in-VM test tunnel, portable repo_root

wg-setup-host.sh sets up the tunnel on the backend host: keys generated
there and kept there, wg0.conf, wg-quick enabled, and the phone's config
printed as a scannable QR. Split tunnel (AllowedIPs is only the backend
subnet), single-address addressing per PLAN.md, and the three things it
can't do for you -- router UDP forward, DDNS, hairpin check -- spelled
out at the end.

test-wg-tunnel.sh stands up a real WireGuard tunnel between two network
namespaces inside one machine, so the production posture (bind wg0 and
nothing else) is testable with no router, phone, or internet exposure.
Verified: real handshake, server listening on 10.66.0.1:8443 only, and
an authorized request from inside the tunnel answering 200 over pinned
TLS -- the leaf's 10.66.0.1 SAN is what a phone will validate too.

repo_root() now resolves from the running executable before falling back
to the compiled-in path: the repo is shared host<->VM over virtiofs at
different absolute paths with a shared target/, so a binary built on one
side and run on the other looked for its config where nothing exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 02:22:00 -04:00
1 parent 8841effc6a
commit 29f8b31f0b
5 files changed
+324 -4

No files matched your search

+20 -4
View File
@@ -35,13 +35,29 @@ const WG_INTERFACE: &str = "wg0";
/// The repo root, one level above this crate. Everything the server reads
/// by default -- the TLS cert, the config, the session data -- resolves
/// from here, so there's one definition of it rather than one per caller.
///
/// Found from the running executable first, and only then from the path
/// compiled in. This repo is shared between a VM and its host over
/// virtiofs at *different* absolute paths (`~/host/repos/ai-app` vs
/// `~/stuff/vm/ai/repos/ai-app`), and `target/` is shared along with it --
/// so a binary built on one side and run on the other would otherwise look
/// for its config under a path that doesn't exist there, which is exactly
/// what happened once. Where both agree (the ordinary case) the answer is
/// identical either way; the flags below override it regardless.
fn repo_root() -> &'static Path {
static ROOT: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
ROOT.get_or_init(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("CARGO_MANIFEST_DIR has a repo-root parent")
.to_path_buf()
// target/{debug,release}/ai-server -> four levels up is the root.
let from_exe = std::env::current_exe().ok().and_then(|exe| {
let root = exe.ancestors().nth(4)?.to_path_buf();
root.join("server/Cargo.toml").is_file().then_some(root)
});
from_exe.unwrap_or_else(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("CARGO_MANIFEST_DIR has a repo-root parent")
.to_path_buf()
})
})
}