AkurAI Build
Menu

AkurAI-VPNCommit ab4282ef

Commit ab4282ef

ab4282efdc91026beb4921d684f283d908f1b5de by Claude Code

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91e5a3c..72a9718 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.8] - 2026-06-28
+
+- Require per-user VPN node networks
+
+
 ## [0.0.7] - 2026-06-28
 
 - Add host-only node install defaults
diff --git a/Cargo.lock b/Cargo.lock
index 99f63fb..55e9106 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,47 +4,47 @@ version = 4
 
 [[package]]
 name = "akurai-admin"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-common"
-version = "0.0.6"
+version = "0.0.7"
 
 [[package]]
 name = "akurai-control"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-crypto"
-version = "0.0.6"
+version = "0.0.7"
 
 [[package]]
 name = "akurai-dns"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-node"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "akurai-common",
 ]
 
 [[package]]
 name = "akurai-relay"
-version = "0.0.6"
+version = "0.0.7"
 
 [[package]]
 name = "akurai-transport"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "akurai-crypto",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 80b046d..d94d552 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.7"
+version = "0.0.8"
 edition = "2021"
 license = "MIT"
 repository = "https://github.com/olibuijr/AkurAI-VPN"
diff --git a/VERSION b/VERSION
index 5a5831a..d169b2f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.7
+0.0.8
diff --git a/crates/akurai-control/src/listener.rs b/crates/akurai-control/src/listener.rs
index b3bb877..8c11451 100644
--- a/crates/akurai-control/src/listener.rs
+++ b/crates/akurai-control/src/listener.rs
@@ -445,18 +445,25 @@ fn handle_dashboard(req: &Request, state: &SharedState) -> Response {
     let endpoints = state
         .lock()
         .ok()
-        .map(|s| s.endpoints.clone())
+        .map(|s| {
+            s.endpoints
+                .iter()
+                .filter(|e| e.added_by == user.user.email)
+                .cloned()
+                .collect::<Vec<_>>()
+        })
         .unwrap_or_default();
     Response::ok_html(render_dashboard(&user.user, &user.csrf_token, &endpoints))
 }
 
-fn handle_list_endpoints(_user: &AuthSession, state: &SharedState) -> Response {
+fn handle_list_endpoints(session: &AuthSession, state: &SharedState) -> Response {
     let json = state
         .lock()
         .ok()
         .map(|s| {
             s.endpoints
                 .iter()
+                .filter(|e| e.added_by == session.user.email)
                 .map(|e| e.to_json())
                 .collect::<Vec<_>>()
                 .join(",")
@@ -569,7 +576,8 @@ fn delete_endpoint(
         Err(_) => return Response::error_html("Internal state lock error"),
     };
     let before = st.endpoints.len();
-    st.endpoints.retain(|e| e.id != id);
+    st.endpoints
+        .retain(|e| !(e.id == id && e.added_by == session.user.email));
     if st.endpoints.len() == before {
         return Response::not_found();
     }
@@ -826,4 +834,70 @@ mod tests {
         let html = render_dashboard(&user, "csrf-value", &[]);
         assert!(html.contains(r#"name="csrf_token" value="csrf-value""#));
     }
+
+    #[test]
+    fn endpoint_list_is_scoped_to_authenticated_user() {
+        let state = crate::state::new_shared();
+        {
+            let mut st = state.lock().unwrap();
+            st.endpoints.push(crate::vpn_endpoint::VpnEndpoint {
+                id: "mine".to_string(),
+                name: "midget".to_string(),
+                public_key: "pk1".to_string(),
+                endpoint_addr: String::new(),
+                allowed_ips: Vec::new(),
+                added_by: "user@example.com".to_string(),
+                added_at: 1,
+            });
+            st.endpoints.push(crate::vpn_endpoint::VpnEndpoint {
+                id: "other".to_string(),
+                name: "other-host".to_string(),
+                public_key: "pk2".to_string(),
+                endpoint_addr: String::new(),
+                allowed_ips: Vec::new(),
+                added_by: "other@example.com".to_string(),
+                added_at: 1,
+            });
+        }
+        let session = AuthSession {
+            user: AuthUser {
+                sub: "sub".to_string(),
+                email: "user@example.com".to_string(),
+                name: "User".to_string(),
+            },
+            csrf_token: "csrf".to_string(),
+        };
+        let response = handle_list_endpoints(&session, &state);
+        assert!(response.body.contains("mine"));
+        assert!(!response.body.contains("other"));
+    }
+
+    #[test]
+    fn delete_endpoint_cannot_remove_another_users_node() {
+        let state = crate::state::new_shared();
+        {
+            let mut st = state.lock().unwrap();
+            st.endpoints.push(crate::vpn_endpoint::VpnEndpoint {
+                id: "other".to_string(),
+                name: "other-host".to_string(),
+                public_key: "pk2".to_string(),
+                endpoint_addr: String::new(),
+                allowed_ips: Vec::new(),
+                added_by: "other@example.com".to_string(),
+                added_at: 1,
+            });
+        }
+        let session = AuthSession {
+            user: AuthUser {
+                sub: "sub".to_string(),
+                email: "user@example.com".to_string(),
+                name: "User".to_string(),
+            },
+            csrf_token: "csrf".to_string(),
+        };
+        let req = request(&[], "csrf_token=csrf");
+        let response = delete_endpoint("other", &session, &req, &state);
+        assert_eq!(response.status, "404 Not Found");
+        assert_eq!(state.lock().unwrap().endpoints.len(), 1);
+    }
 }
diff --git a/crates/akurai-node/src/main.rs b/crates/akurai-node/src/main.rs
index 5c2ba2b..4a668bd 100644
--- a/crates/akurai-node/src/main.rs
+++ b/crates/akurai-node/src/main.rs
@@ -64,7 +64,9 @@ fn install(args: &[String]) -> Result<(), NodeError> {
 
     let current_exe = std::env::current_exe()?;
     let installed_exe = dirs.bin.join(NAME);
-    fs::copy(&current_exe, &installed_exe)?;
+    if current_exe.canonicalize().ok() != installed_exe.canonicalize().ok() {
+        fs::copy(&current_exe, &installed_exe)?;
+    }
 
     if !dirs.config_file.exists() {
         let hostname = std::env::var("HOSTNAME")