feat(server): add bridge forwarding mode and per-client destination policy overrides

This commit is contained in:
2026-03-31 21:34:49 +00:00
parent 17af7ab289
commit fdeba5eeb5
12 changed files with 583 additions and 25 deletions

View File

@@ -26,6 +26,9 @@ pub struct ClientSecurity {
pub max_connections: Option<u32>,
/// Per-client rate limiting.
pub rate_limit: Option<ClientRateLimit>,
/// Per-client destination routing policy override.
/// When set, overrides the server-level DestinationPolicy for this client's traffic.
pub destination_policy: Option<crate::server::DestinationPolicyConfig>,
}
/// A registered client entry — the server-side source of truth.
@@ -76,12 +79,14 @@ impl ClientEntry {
}
}
/// In-memory client registry with dual-key indexing.
/// In-memory client registry with triple-key indexing.
pub struct ClientRegistry {
/// Primary index: clientId → ClientEntry
entries: HashMap<String, ClientEntry>,
/// Secondary index: publicKey (base64) → clientId (fast lookup during handshake)
key_index: HashMap<String, String>,
/// Tertiary index: assignedIp → clientId (fast lookup during NAT destination policy)
ip_index: HashMap<String, String>,
}
impl ClientRegistry {
@@ -89,6 +94,7 @@ impl ClientRegistry {
Self {
entries: HashMap::new(),
key_index: HashMap::new(),
ip_index: HashMap::new(),
}
}
@@ -114,6 +120,9 @@ impl ClientRegistry {
anyhow::bail!("Public key already registered to another client");
}
self.key_index.insert(entry.public_key.clone(), entry.client_id.clone());
if let Some(ref ip) = entry.assigned_ip {
self.ip_index.insert(ip.clone(), entry.client_id.clone());
}
self.entries.insert(entry.client_id.clone(), entry);
Ok(())
}
@@ -123,6 +132,9 @@ impl ClientRegistry {
let entry = self.entries.remove(client_id)
.ok_or_else(|| anyhow::anyhow!("Client '{}' not found", client_id))?;
self.key_index.remove(&entry.public_key);
if let Some(ref ip) = entry.assigned_ip {
self.ip_index.remove(ip);
}
Ok(entry)
}
@@ -137,6 +149,12 @@ impl ClientRegistry {
self.entries.get(client_id)
}
/// Get a client by assigned IP (used for per-client destination policy in NAT engine).
pub fn get_by_assigned_ip(&self, ip: &str) -> Option<&ClientEntry> {
let client_id = self.ip_index.get(ip)?;
self.entries.get(client_id)
}
/// Check if a public key is authorized (exists, enabled, not expired).
pub fn is_authorized(&self, public_key: &str) -> bool {
match self.get_by_key(public_key) {
@@ -153,12 +171,22 @@ impl ClientRegistry {
let entry = self.entries.get_mut(client_id)
.ok_or_else(|| anyhow::anyhow!("Client '{}' not found", client_id))?;
let old_key = entry.public_key.clone();
let old_ip = entry.assigned_ip.clone();
updater(entry);
// If public key changed, update the index
// If public key changed, update the key index
if entry.public_key != old_key {
self.key_index.remove(&old_key);
self.key_index.insert(entry.public_key.clone(), client_id.to_string());
}
// If assigned IP changed, update the IP index
if entry.assigned_ip != old_ip {
if let Some(ref old) = old_ip {
self.ip_index.remove(old);
}
if let Some(ref new_ip) = entry.assigned_ip {
self.ip_index.insert(new_ip.clone(), client_id.to_string());
}
}
Ok(())
}
@@ -362,6 +390,7 @@ mod tests {
bytes_per_sec: 1_000_000,
burst_bytes: 2_000_000,
}),
destination_policy: None,
});
let mut reg = ClientRegistry::new();
reg.add(entry).unwrap();