Menu
Commit 54a1c16d
54a1c16dd2ce3b19115d018e216aaf2f742e3d74 by Claude Code
- CHANGELOG.md +5 -0
- Cargo.lock +8 -8
- Cargo.toml +1 -1
- VERSION +1 -1
- crates/akurai-node/Cargo.toml +1 -1
- crates/akurai-node/src/error.rs +9 -5
- crates/akurai-node/src/gateway.rs +0 -57
- crates/akurai-node/src/main.rs +190 -35
- crates/akurai-node/src/peermap.rs +6 -11
- crates/akurai-node/src/route.rs +0 -49
- crates/akurai-node/src/tun.rs +6 -24
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(¤t_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");
}