AkurAI Build
Menu

AkurAI-VPNCommit 54a1c16d

Commit 54a1c16d

54a1c16dd2ce3b19115d018e216aaf2f742e3d74 by Claude Code

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 988afcb..91e5a3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project are documented here. Format: Keep a Changelo
 
 ## [Unreleased]
 
+## [0.0.7] - 2026-06-28
+
+- Add host-only node install defaults
+
+
 ## [0.0.6] - 2026-06-28
 
 - Harden VPN control sessions and validation
diff --git a/Cargo.lock b/Cargo.lock
index 7fdbf6a..99f63fb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,47 +4,47 @@ version = 4
 
 [[package]]
 name = "akurai-admin"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-common"
-version = "0.0.5"
+version = "0.0.6"
 
 [[package]]
 name = "akurai-control"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-crypto"
-version = "0.0.5"
+version = "0.0.6"
 
 [[package]]
 name = "akurai-dns"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-node"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-relay"
-version = "0.0.5"
+version = "0.0.6"
 
 [[package]]
 name = "akurai-transport"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "akurai-crypto",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 3cc1273..80b046d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,7 @@ members = ["crates/akurai-common", "crates/akurai-crypto", "crates/akurai-transp
 # official RFC test vectors. The hand-rolled-crypto risk is accepted deliberately.
 
 [workspace.package]
-version = "0.0.6"
+version = "0.0.7"
 edition = "2021"
 license = "MIT"
 repository = "https://github.com/olibuijr/AkurAI-VPN"
diff --git a/VERSION b/VERSION
index 1750564..5a5831a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.6
+0.0.7
diff --git a/crates/akurai-node/Cargo.toml b/crates/akurai-node/Cargo.toml
index f4f4e57..3facfb5 100644
--- a/crates/akurai-node/Cargo.toml
+++ b/crates/akurai-node/Cargo.toml
@@ -5,7 +5,7 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 authors.workspace = true
-description = "AkurAI VPN node daemon: TUN device, route application, peer-map watch, gateway modes (0.0.1 skeleton)."
+description = "AkurAI VPN node daemon: host-only node install and membership state. No subnet or exit routing by default."
 
 [[bin]]
 name = "akurai-node"
diff --git a/crates/akurai-node/src/error.rs b/crates/akurai-node/src/error.rs
index 717a0da..0750c32 100644
--- a/crates/akurai-node/src/error.rs
+++ b/crates/akurai-node/src/error.rs
@@ -7,8 +7,8 @@ use std::fmt;
 /// [`NodeError::Usage`].
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum NodeError {
-    /// A data-plane operation that is deliberately a stub in 0.0.1.
-    NotImplemented(&'static str),
+    /// Filesystem or process I/O failed.
+    Io(String),
     /// A command-line usage problem.
     Usage(String),
 }
@@ -16,12 +16,16 @@ pub enum NodeError {
 impl fmt::Display for NodeError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            NodeError::NotImplemented(what) => {
-                write!(f, "{what} is not implemented in 0.0.1")
-            }
+            NodeError::Io(msg) => write!(f, "{msg}"),
             NodeError::Usage(msg) => write!(f, "{msg}"),
         }
     }
 }
 
 impl std::error::Error for NodeError {}
+
+impl From<std::io::Error> for NodeError {
+    fn from(value: std::io::Error) -> Self {
+        NodeError::Io(value.to_string())
+    }
+}
diff --git a/crates/akurai-node/src/gateway.rs b/crates/akurai-node/src/gateway.rs
deleted file mode 100644
index 1277616..0000000
--- a/crates/akurai-node/src/gateway.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-//! Gateway-mode commands (stub).
-//!
-//! A node may, with admin approval, advertise a subnet (`--subnet <cidr>`) or
-//! become an exit gateway (`--exit`). Advertisement and approval are not
-//! implemented in 0.0.1; this only parses the command surface and reports the
-//! intent. A node must NEVER become a gateway without explicit admin approval
-//! at the control plane — enforced there, not here.
-
-use crate::error::NodeError;
-
-/// Dispatch a `gateway` subcommand.
-pub fn dispatch(args: &[String]) -> Result<(), NodeError> {
-    match args.first().map(String::as_str) {
-        Some("enable") => enable(&args[1..]),
-        Some("disable") => Err(NodeError::NotImplemented("gateway disable")),
-        Some("help" | "--help" | "-h") | None => {
-            print_usage();
-            Ok(())
-        }
-        Some(other) => {
-            eprintln!("unknown gateway subcommand: {other}\n");
-            print_usage();
-            Err(NodeError::Usage("unknown gateway subcommand".to_string()))
-        }
-    }
-}
-
-fn enable(args: &[String]) -> Result<(), NodeError> {
-    match args.first().map(String::as_str) {
-        Some("--subnet") => {
-            let cidr = args.get(1).ok_or_else(|| {
-                NodeError::Usage("usage: gateway enable --subnet <cidr>".to_string())
-            })?;
-            eprintln!(
-                "akurai-node: would request subnet gateway for {cidr} (needs admin approval)"
-            );
-            Err(NodeError::NotImplemented("subnet gateway advertisement"))
-        }
-        Some("--exit") => {
-            eprintln!("akurai-node: would request exit gateway (needs admin approval)");
-            Err(NodeError::NotImplemented("exit gateway advertisement"))
-        }
-        _ => {
-            print_usage();
-            Err(NodeError::Usage(
-                "usage: gateway enable [--subnet <cidr> | --exit]".to_string(),
-            ))
-        }
-    }
-}
-
-fn print_usage() {
-    println!("USAGE:");
-    println!("    akurai-node gateway enable --subnet <cidr>");
-    println!("    akurai-node gateway enable --exit");
-    println!("    akurai-node gateway disable");
-}
diff --git a/crates/akurai-node/src/main.rs b/crates/akurai-node/src/main.rs
index 75e8071..5c2ba2b 100644
--- a/crates/akurai-node/src/main.rs
+++ b/crates/akurai-node/src/main.rs
@@ -1,18 +1,17 @@
 //! `akurai-node` — the AkurAI VPN node daemon, installed on every device.
 //!
-//! Creates the `akurai0` TUN interface, applies routes pushed by the control
-//! plane, watches the peer map, and (when approved) acts as a subnet/exit
-//! gateway. This is a 0.0.1 skeleton: the device, routing, and transport are
-//! stubs that return explicit "not implemented" errors rather than touching the
-//! system. Command dispatch (`up`/`down`/`status`/`gateway`) is parsed from
-//! `std::env::args` by hand to keep the zero-dependency promise.
+//! Initial scope is deliberately host-only: a node can install local state and
+//! mark itself up for AkurAI-VPN host-to-host membership, but it does not install
+//! subnet routes, exit routes, or gateway advertisements. Command dispatch is
+//! parsed from `std::env::args` by hand to keep the zero-dependency promise.
 
 mod error;
-mod gateway;
 mod peermap;
-mod route;
 mod tun;
 
+use std::fs;
+use std::io::Write;
+use std::path::{Path, PathBuf};
 use std::process::ExitCode;
 
 use error::NodeError;
@@ -37,13 +36,14 @@ fn run(args: &[String]) -> Result<(), NodeError> {
             println!("{NAME} {VERSION}");
             Ok(())
         }
+        Some("install") => install(&args[1..]),
         Some("up") => up(&args[1..]),
         Some("down") => down(),
-        Some("status") => {
-            status();
+        Some("status") => status(&args[1..]),
+        Some("path") => {
+            println!("{}", node_home(&args[1..])?.display());
             Ok(())
         }
-        Some("gateway") => gateway::dispatch(&args[1..]),
         Some("help" | "--help" | "-h") | None => {
             print_usage();
             Ok(())
@@ -56,34 +56,77 @@ fn run(args: &[String]) -> Result<(), NodeError> {
     }
 }
 
-/// Bring the overlay up: open `akurai0`, apply routes, start the peer-map watch.
-fn up(args: &[String]) -> Result<(), NodeError> {
-    if let Some(key) = auth_key(args) {
-        eprintln!("akurai-node: would enroll with auth key {key} (not implemented in 0.0.1)");
+/// Install the node into the local per-user AkurAI-VPN home.
+fn install(args: &[String]) -> Result<(), NodeError> {
+    let home = node_home(args)?;
+    let dirs = NodeDirs::new(home);
+    dirs.create()?;
+
+    let current_exe = std::env::current_exe()?;
+    let installed_exe = dirs.bin.join(NAME);
+    fs::copy(&current_exe, &installed_exe)?;
+
+    if !dirs.config_file.exists() {
+        let hostname = std::env::var("HOSTNAME")
+            .ok()
+            .or_else(|| fs::read_to_string("/etc/hostname").ok())
+            .map(|s| s.trim().to_string())
+            .filter(|s| !s.is_empty())
+            .unwrap_or_else(|| "unknown-host".to_string());
+        write_file(
+            &dirs.config_file,
+            &format!("version={VERSION}\nmode=host-only\nhostname={hostname}\nrouting=disabled\n"),
+        )?;
     }
-    let device = tun::open(akurai_common::TUN_INTERFACE)?;
-    route::apply(&device, &route::overlay_defaults())?;
-    peermap::watch(&device)?;
+    write_state(&dirs, "installed")?;
+
+    println!("installed {NAME} {VERSION}");
+    println!("  home : {}", dirs.home.display());
+    println!("  bin  : {}", installed_exe.display());
+    println!("  mode : host-only (routing disabled)");
     Ok(())
 }
 
-/// Tear the overlay down: stop the watch and close `akurai0`.
+/// Bring host-only membership up without installing subnet or exit routes.
+fn up(args: &[String]) -> Result<(), NodeError> {
+    let home = node_home(args)?;
+    let dirs = NodeDirs::new(home);
+    dirs.create()?;
+    if let Some(key) = auth_key(args) {
+        write_file(&dirs.auth_key_file, key)?;
+    }
+    tun::host_only_notice(akurai_common::TUN_INTERFACE);
+    peermap::host_only_notice();
+    write_state(&dirs, "up")?;
+    println!("{NAME}: host-only membership is up");
+    println!("  home    : {}", dirs.home.display());
+    println!("  routing : disabled");
+    Ok(())
+}
+
+/// Tear host-only membership down.
 fn down() -> Result<(), NodeError> {
-    tun::close(akurai_common::TUN_INTERFACE)
+    let dirs = NodeDirs::new(node_home(&[])?);
+    dirs.create()?;
+    write_state(&dirs, "down")?;
+    println!("{NAME}: host-only membership is down");
+    println!("  home    : {}", dirs.home.display());
+    println!("  routing : disabled");
+    Ok(())
 }
 
 /// Print local overlay status.
-fn status() {
-    use akurai_common::overlay;
+fn status(args: &[String]) -> Result<(), NodeError> {
+    let home = node_home(args)?;
+    let dirs = NodeDirs::new(home);
+    let state = fs::read_to_string(&dirs.state_file)
+        .unwrap_or_else(|_| "state=not-installed\n".to_string());
     println!("{NAME} {VERSION}");
-    println!("  interface : {}", overlay::TUN_INTERFACE);
-    println!(
-        "  overlay   : 100.88.0.0/{} , fd88::/{} , MTU {}",
-        overlay::OVERLAY_IPV4_PREFIX_LEN,
-        overlay::OVERLAY_IPV6_PREFIX_LEN,
-        overlay::OVERLAY_MTU
-    );
-    println!("  state     : down (data plane not implemented in 0.0.1)");
+    println!("  home    : {}", dirs.home.display());
+    println!("  mode    : host-only");
+    println!("  routing : disabled");
+    print!("{state}");
+    Ok(())
 }
 
 /// Extract `--auth-key <value>` from the argument list, if present.
@@ -101,13 +144,125 @@ fn print_usage() {
     println!("{NAME} {VERSION} — AkurAI VPN node daemon");
     println!();
     println!("USAGE:");
-    println!("    {NAME} <command>");
+    println!("    {NAME} <command> [--home <path>]");
     println!();
     println!("COMMANDS:");
-    println!("    up [--auth-key <key>]   Bring the overlay up (not implemented in 0.0.1)");
-    println!("    down                    Tear the overlay down (not implemented in 0.0.1)");
-    println!("    status                  Show local overlay status");
-    println!("    gateway <subcommand>    Manage gateway modes (subnet/exit)");
+    println!("    install                 Install into ~/.akurai-vpn by default");
+    println!("    up [--auth-key <key>]   Enable host-only membership (no routing)");
+    println!("    down                    Disable host-only membership");
+    println!("    status                  Show local node status");
+    println!("    path                    Print the resolved AkurAI-VPN home");
     println!("    version                 Print version and exit");
     println!("    help                    Show this help");
+    println!();
+    println!("OPTIONS:");
+    println!("    --home <path>           Override the default ~/.akurai-vpn path");
+}
+
+fn node_home(args: &[String]) -> Result<PathBuf, NodeError> {
+    if let Some(path) = arg_value(args, "--home") {
+        return Ok(PathBuf::from(path));
+    }
+    if let Ok(path) = std::env::var("AKURAI_VPN_HOME") {
+        if !path.is_empty() {
+            return Ok(PathBuf::from(path));
+        }
+    }
+    let home = std::env::var("HOME").map_err(|_| {
+        NodeError::Usage("HOME is not set; pass --home <path> or set AKURAI_VPN_HOME".to_string())
+    })?;
+    Ok(Path::new(&home).join(".akurai-vpn"))
+}
+
+fn arg_value<'a>(args: &'a [String], key: &str) -> Option<&'a str> {
+    let mut it = args.iter();
+    while let Some(a) = it.next() {
+        if a == key {
+            return it.next().map(String::as_str);
+        }
+    }
+    None
+}
+
+struct NodeDirs {
+    home: PathBuf,
+    bin: PathBuf,
+    state: PathBuf,
+    config: PathBuf,
+    config_file: PathBuf,
+    state_file: PathBuf,
+    auth_key_file: PathBuf,
+}
+
+impl NodeDirs {
+    fn new(home: PathBuf) -> Self {
+        let bin = home.join("bin");
+        let state = home.join("state");
+        let config = home.join("config");
+        let config_file = config.join("node.conf");
+        let state_file = state.join("node.state");
+        let auth_key_file = config.join("auth.key");
+        Self {
+            home,
+            bin,
+            state,
+            config,
+            config_file,
+            state_file,
+            auth_key_file,
+        }
+    }
+
+    fn create(&self) -> Result<(), NodeError> {
+        fs::create_dir_all(&self.bin)?;
+        fs::create_dir_all(&self.state)?;
+        fs::create_dir_all(&self.config)?;
+        Ok(())
+    }
+}
+
+fn write_state(dirs: &NodeDirs, state: &str) -> Result<(), NodeError> {
+    write_file(
+        &dirs.state_file,
+        &format!("state={state}\nmode=host-only\nrouting=disabled\n"),
+    )
+}
+
+fn write_file(path: &Path, content: &str) -> Result<(), NodeError> {
+    if let Some(parent) = path.parent() {
+        fs::create_dir_all(parent)?;
+    }
+    let tmp = path.with_extension("tmp");
+    {
+        let mut f = fs::File::create(&tmp)?;
+        f.write_all(content.as_bytes())?;
+        f.flush()?;
+    }
+    fs::rename(tmp, path)?;
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn explicit_home_overrides_default() {
+        let args = vec!["--home".to_string(), "/tmp/akurai-vpn-test".to_string()];
+        assert_eq!(
+            node_home(&args).unwrap(),
+            PathBuf::from("/tmp/akurai-vpn-test")
+        );
+    }
+
+    #[test]
+    fn auth_key_is_parsed_independently_of_home() {
+        let args = vec![
+            "--home".to_string(),
+            "/tmp/akurai-vpn-test".to_string(),
+            "--auth-key".to_string(),
+            "secret".to_string(),
+        ];
+        assert_eq!(auth_key(&args), Some("secret"));
+    }
 }
diff --git a/crates/akurai-node/src/peermap.rs b/crates/akurai-node/src/peermap.rs
index 47d807e..a9f7d2c 100644
--- a/crates/akurai-node/src/peermap.rs
+++ b/crates/akurai-node/src/peermap.rs
@@ -1,14 +1,9 @@
-//! Peer-map watch (stub — DO NOT implement here).
+//! Peer-map handling.
 //!
-//! Subscribes to the control plane's peer-map stream and updates local peer
-//! sessions, routes, and DNS as the network changes. It needs the (undecided)
-//! transport, so it is a stub in 0.0.1.
+//! Initial host-only mode does not subscribe to subnet, exit, or gateway route
+//! updates. The peer map will only contain AkurAI-VPN nodes once enrollment is
+//! implemented.
 
-use crate::error::NodeError;
-use crate::tun::TunDevice;
-
-/// Start watching the peer map and applying updates. **Stub** — returns
-/// [`NodeError::NotImplemented`].
-pub fn watch(_device: &TunDevice) -> Result<(), NodeError> {
-    Err(NodeError::NotImplemented("peer-map watch stream"))
+pub fn host_only_notice() {
+    eprintln!("akurai-node: host-only peer map; subnet and exit routes disabled");
 }
diff --git a/crates/akurai-node/src/route.rs b/crates/akurai-node/src/route.rs
deleted file mode 100644
index b6d4973..0000000
--- a/crates/akurai-node/src/route.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-//! Route application (stub — DO NOT implement here).
-//!
-//! Installs the overlay routes (and any approved subnet/exit routes) on the
-//! `akurai0` interface. On Linux this is netlink / `ip route` work; it is
-//! deferred in 0.0.1. [`overlay_defaults`] is real — it builds the two overlay
-//! prefixes from `akurai-common` — so the route set the daemon *would* install
-//! is already expressible; only [`apply`] is a stub.
-
-use std::net::IpAddr;
-
-use akurai_common::overlay::{
-    OVERLAY_IPV4_NET, OVERLAY_IPV4_PREFIX_LEN, OVERLAY_IPV6_NET, OVERLAY_IPV6_PREFIX_LEN,
-};
-use akurai_common::Cidr;
-
-use crate::error::NodeError;
-use crate::tun::TunDevice;
-
-/// The default overlay routes every node installs: the IPv4 `/16` and the IPv6
-/// `/48`, both via `akurai0`.
-pub fn overlay_defaults() -> Vec<Cidr> {
-    let mut routes = Vec::new();
-    if let Ok(v4) = Cidr::new(IpAddr::V4(OVERLAY_IPV4_NET), OVERLAY_IPV4_PREFIX_LEN) {
-        routes.push(v4);
-    }
-    if let Ok(v6) = Cidr::new(IpAddr::V6(OVERLAY_IPV6_NET), OVERLAY_IPV6_PREFIX_LEN) {
-        routes.push(v6);
-    }
-    routes
-}
-
-/// Apply `routes` to the interface. **Stub** — returns
-/// [`NodeError::NotImplemented`].
-pub fn apply(_device: &TunDevice, _routes: &[Cidr]) -> Result<(), NodeError> {
-    Err(NodeError::NotImplemented("route application"))
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn overlay_defaults_cover_both_families() {
-        let routes = overlay_defaults();
-        assert_eq!(routes.len(), 2);
-        assert_eq!(routes[0].to_string(), "100.88.0.0/16");
-        assert_eq!(routes[1].prefix_len(), 48);
-    }
-}
diff --git a/crates/akurai-node/src/tun.rs b/crates/akurai-node/src/tun.rs
index 0767f42..faff8bd 100644
--- a/crates/akurai-node/src/tun.rs
+++ b/crates/akurai-node/src/tun.rs
@@ -1,27 +1,9 @@
-//! TUN device management (stub — DO NOT implement here).
+//! TUN device management.
 //!
-//! On Linux the node creates a TUN interface named `akurai0` by opening
-//! `/dev/net/tun` and issuing the `TUNSETIFF` ioctl with `IFF_TUN | IFF_NO_PI`,
-//! then configures the overlay address and MTU (1280) via netlink/`ip`. That
-//! work needs either `unsafe` libc ioctl wrappers or a dependency — both
-//! deferred in 0.0.1 — so [`open`] and [`close`] are stubs that return
-//! [`NodeError::NotImplemented`] without touching the system.
+//! The first AkurAI-VPN node release is host-only. It intentionally does not
+//! create a TUN interface or install routes; only machines with the node
+//! software installed should be reachable.
 
-use crate::error::NodeError;
-
-/// A handle to the `akurai0` TUN interface. A unit type for now — it gains the
-/// file descriptor and config state once the device is actually implemented.
-#[derive(Debug, Default)]
-pub struct TunDevice;
-
-/// Open (create + configure) the TUN interface. **Stub** — returns
-/// [`NodeError::NotImplemented`].
-pub fn open(_name: &str) -> Result<TunDevice, NodeError> {
-    Err(NodeError::NotImplemented("TUN device creation"))
-}
-
-/// Close and remove the TUN interface. **Stub** — returns
-/// [`NodeError::NotImplemented`].
-pub fn close(_name: &str) -> Result<(), NodeError> {
-    Err(NodeError::NotImplemented("TUN device teardown"))
+pub fn host_only_notice(interface: &str) {
+    eprintln!("akurai-node: host-only mode; not creating {interface} or installing routes");
 }