feat(client-registry): separate trusted server-defined client tags from client-reported tags with legacy tag compatibility
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-30 - 1.12.0 - feat(server)
|
||||||
add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding
|
add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ pub struct ClientEntry {
|
|||||||
pub priority: Option<u32>,
|
pub priority: Option<u32>,
|
||||||
/// Whether this client is enabled (default: true).
|
/// Whether this client is enabled (default: true).
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
/// Tags for grouping.
|
/// Tags assigned by the server admin — trusted, used for access control.
|
||||||
|
pub server_defined_client_tags: Option<Vec<String>>,
|
||||||
|
/// Tags reported by the connecting client — informational only.
|
||||||
|
pub client_defined_client_tags: Option<Vec<String>>,
|
||||||
|
/// Legacy tags field — treated as serverDefinedClientTags during deserialization.
|
||||||
|
#[serde(default)]
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
/// Optional description.
|
/// Optional description.
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@@ -90,7 +95,11 @@ impl ClientRegistry {
|
|||||||
/// Build a registry from a list of client entries.
|
/// Build a registry from a list of client entries.
|
||||||
pub fn from_entries(entries: Vec<ClientEntry>) -> Result<Self> {
|
pub fn from_entries(entries: Vec<ClientEntry>) -> Result<Self> {
|
||||||
let mut registry = Self::new();
|
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)?;
|
registry.add(entry)?;
|
||||||
}
|
}
|
||||||
Ok(registry)
|
Ok(registry)
|
||||||
@@ -193,6 +202,8 @@ mod tests {
|
|||||||
security: None,
|
security: None,
|
||||||
priority: None,
|
priority: None,
|
||||||
enabled: None,
|
enabled: None,
|
||||||
|
server_defined_client_tags: None,
|
||||||
|
client_defined_client_tags: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
description: None,
|
description: None,
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
|
|||||||
@@ -551,9 +551,16 @@ impl VpnServer {
|
|||||||
).ok(),
|
).ok(),
|
||||||
priority: partial.get("priority").and_then(|v| v.as_u64()).map(|v| v as u32),
|
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)),
|
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())
|
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),
|
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),
|
expires_at: partial.get("expiresAt").and_then(|v| v.as_str()).map(String::from),
|
||||||
assigned_ip: Some(assigned_ip.to_string()),
|
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()) {
|
if let Some(enabled) = update.get("enabled").and_then(|v| v.as_bool()) {
|
||||||
entry.enabled = Some(enabled);
|
entry.enabled = Some(enabled);
|
||||||
}
|
}
|
||||||
if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
|
if let Some(tags) = update.get("serverDefinedClientTags").and_then(|v| v.as_array()) {
|
||||||
entry.tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
|
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()) {
|
if let Some(desc) = update.get("description").and_then(|v| v.as_str()) {
|
||||||
entry.description = Some(desc.to_string());
|
entry.description = Some(desc.to_string());
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartvpn',
|
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'
|
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export interface IVpnClientConfig {
|
|||||||
wgEndpoint?: string;
|
wgEndpoint?: string;
|
||||||
/** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */
|
/** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */
|
||||||
wgAllowedIps?: string[];
|
wgAllowedIps?: string[];
|
||||||
|
/** Client-defined tags reported to the server after connection (informational, not for access control) */
|
||||||
|
clientDefinedClientTags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVpnClientOptions {
|
export interface IVpnClientOptions {
|
||||||
@@ -290,7 +292,11 @@ export interface IClientEntry {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
/** Whether this client is enabled (default: true) */
|
/** Whether this client is enabled (default: true) */
|
||||||
enabled?: boolean;
|
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[];
|
tags?: string[];
|
||||||
/** Optional description */
|
/** Optional description */
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user