From 2d7a507cf26ae1c971c123c0d0dd34adbbfc7699 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 30 Mar 2026 09:42:04 +0000 Subject: [PATCH] feat(client-registry): separate trusted server-defined client tags from client-reported tags with legacy tag compatibility --- changelog.md | 7 +++++++ rust/src/client_registry.rs | 15 +++++++++++++-- rust/src/server.rs | 16 +++++++++++++--- ts/00_commitinfo_data.ts | 2 +- ts/smartvpn.interfaces.ts | 8 +++++++- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index f41c583..1388930 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 1.13.0 - feat(client-registry) +separate trusted server-defined client tags from client-reported tags with legacy tag compatibility + +- Adds distinct serverDefinedClientTags and clientDefinedClientTags fields to client registry and TypeScript interfaces. +- Treats legacy tags values as serverDefinedClientTags during deserialization and server-side create/update flows for backward compatibility. +- Clarifies that only server-defined tags are trusted for access control while client-defined tags are informational only. + ## 2026-03-30 - 1.12.0 - feat(server) add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding diff --git a/rust/src/client_registry.rs b/rust/src/client_registry.rs index 18867c8..113e8a3 100644 --- a/rust/src/client_registry.rs +++ b/rust/src/client_registry.rs @@ -44,7 +44,12 @@ pub struct ClientEntry { pub priority: Option, /// Whether this client is enabled (default: true). pub enabled: Option, - /// Tags for grouping. + /// Tags assigned by the server admin — trusted, used for access control. + pub server_defined_client_tags: Option>, + /// Tags reported by the connecting client — informational only. + pub client_defined_client_tags: Option>, + /// Legacy tags field — treated as serverDefinedClientTags during deserialization. + #[serde(default)] pub tags: Option>, /// Optional description. pub description: Option, @@ -90,7 +95,11 @@ impl ClientRegistry { /// Build a registry from a list of client entries. pub fn from_entries(entries: Vec) -> Result { let mut registry = Self::new(); - for entry in entries { + for mut entry in entries { + // Migrate legacy `tags` → `serverDefinedClientTags` + if entry.server_defined_client_tags.is_none() && entry.tags.is_some() { + entry.server_defined_client_tags = entry.tags.take(); + } registry.add(entry)?; } Ok(registry) @@ -193,6 +202,8 @@ mod tests { security: None, priority: None, enabled: None, + server_defined_client_tags: None, + client_defined_client_tags: None, tags: None, description: None, expires_at: None, diff --git a/rust/src/server.rs b/rust/src/server.rs index 77e347b..a5b8ca8 100644 --- a/rust/src/server.rs +++ b/rust/src/server.rs @@ -551,9 +551,16 @@ impl VpnServer { ).ok(), priority: partial.get("priority").and_then(|v| v.as_u64()).map(|v| v as u32), enabled: partial.get("enabled").and_then(|v| v.as_bool()).or(Some(true)), - tags: partial.get("tags").and_then(|v| { + server_defined_client_tags: partial.get("serverDefinedClientTags").and_then(|v| { v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect()) + }).or_else(|| { + // Legacy: accept "tags" as serverDefinedClientTags + partial.get("tags").and_then(|v| { + v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect()) + }) }), + client_defined_client_tags: None, // Only set by connecting client + tags: None, // Legacy field — not used for new entries description: partial.get("description").and_then(|v| v.as_str()).map(String::from), expires_at: partial.get("expiresAt").and_then(|v| v.as_str()).map(String::from), assigned_ip: Some(assigned_ip.to_string()), @@ -648,8 +655,11 @@ impl VpnServer { if let Some(enabled) = update.get("enabled").and_then(|v| v.as_bool()) { entry.enabled = Some(enabled); } - if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) { - entry.tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect()); + if let Some(tags) = update.get("serverDefinedClientTags").and_then(|v| v.as_array()) { + entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect()); + } else if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) { + // Legacy: accept "tags" as serverDefinedClientTags + entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect()); } if let Some(desc) = update.get("description").and_then(|v| v.as_str()) { entry.description = Some(desc.to_string()); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 6742895..726e1a7 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartvpn', - version: '1.12.0', + version: '1.13.0', description: 'A VPN solution with TypeScript control plane and Rust data plane daemon' } diff --git a/ts/smartvpn.interfaces.ts b/ts/smartvpn.interfaces.ts index 6600fa5..67db08a 100644 --- a/ts/smartvpn.interfaces.ts +++ b/ts/smartvpn.interfaces.ts @@ -57,6 +57,8 @@ export interface IVpnClientConfig { wgEndpoint?: string; /** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */ wgAllowedIps?: string[]; + /** Client-defined tags reported to the server after connection (informational, not for access control) */ + clientDefinedClientTags?: string[]; } export interface IVpnClientOptions { @@ -290,7 +292,11 @@ export interface IClientEntry { priority?: number; /** Whether this client is enabled (default: true) */ enabled?: boolean; - /** Tags for grouping (e.g. ["engineering", "office"]) */ + /** Tags assigned by the server admin — trusted, used for access control (e.g. ["engineering", "office"]) */ + serverDefinedClientTags?: string[]; + /** Tags reported by the connecting client — informational only, never used for access control */ + clientDefinedClientTags?: string[]; + /** @deprecated Use serverDefinedClientTags instead. Legacy field kept for backward compatibility. */ tags?: string[]; /** Optional description */ description?: string;