use std::net::IpAddr;
use serde::{Deserialize, Serialize};
pub use crate::nat_wire::{NatCandidateWire, NatConfigSpec};
pub use crate::overlay::{OverlayConfig, OverlayMode};
pub const PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "frame", rename_all = "snake_case")]
pub enum OverlaydFrame {
Request { id: u64, request: OverlaydRequest },
Response { id: u64, response: OverlaydResponse },
Event(OverlaydEvent),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "platform", rename_all = "snake_case")]
pub enum AttachHandle {
LinuxPid { pid: u32 },
WindowsContainer {
container_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
ip: Option<IpAddr>,
},
GuestManaged { id: String },
HostShared { id: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum OverlaydRequest {
SetLocalNodeId { node_id: u64 },
SetLocalWgPubkey { pubkey: String },
SetupGlobalOverlay {
deployment: String,
instance_id: String,
cluster_cidr: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
slice_cidr: Option<String>,
wg_port: u16,
#[serde(default)]
host_adapter_mandatory: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
nat: Option<NatConfigSpec>,
},
TeardownGlobalOverlay,
SetupServiceOverlay { service: String, mode: OverlayMode },
TeardownServiceOverlay { service: String },
AllocateIp { service: String, join_global: bool },
ReleaseIp { ip: IpAddr },
AttachContainer {
handle: AttachHandle,
service: String,
join_global: bool,
#[serde(default)]
ephemeral: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
isolation_network: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
dns_server: Option<IpAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
dns_domain: Option<String>,
},
DetachContainer { handle: AttachHandle },
AddPeer {
#[serde(flatten)]
peer: PeerSpec,
#[serde(default)]
scope: PeerScope,
},
RemovePeer {
pubkey: String,
#[serde(default)]
scope: PeerScope,
},
AddAllowedIp {
pubkey: String,
cidr: String,
#[serde(default)]
scope: PeerScope,
},
RemoveAllowedIp {
pubkey: String,
cidr: String,
#[serde(default)]
scope: PeerScope,
},
RegisterDns { name: String, ip: IpAddr },
UnregisterDns { name: String },
WriteScopedResolver {
zone: String,
node_ip: IpAddr,
#[serde(default, skip_serializing_if = "Option::is_none")]
port: Option<u16>,
},
RemoveScopedResolver { zone: String },
PruneOrphanBridges { live_bridge_names: Vec<String> },
Status,
NatTick,
NatStatus,
Shutdown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "result", rename_all = "snake_case")]
pub enum OverlaydResponse {
Ok,
Ip { ip: IpAddr },
Attached(AttachResult),
GuestConfig(GuestOverlayConfig),
BridgeName { name: String },
Status(StatusSnapshot),
NatStatus(NatStatusWire),
PrunedBridges { reclaimed: Vec<String> },
ServiceOverlay(ServiceOverlayInfo),
Err { message: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceOverlayInfo {
pub name: String,
pub mode: crate::overlay::OverlayMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wg_public_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wg_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overlay_ip: Option<std::net::IpAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subnet: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttachResult {
pub ip: IpAddr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace_guid: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuestOverlayConfig {
pub overlay_ip: IpAddr,
pub prefix_len: u8,
pub private_key: String,
pub public_key: String,
pub listen_port: u16,
pub peers: Vec<PeerSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns_server: Option<IpAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns_domain: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "scope", rename_all = "snake_case")]
pub enum PeerScope {
#[default]
Global,
Service {
service: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerSpec {
pub public_key: String,
pub endpoint: String,
pub allowed_ips: String,
pub persistent_keepalive_secs: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub candidates: Vec<NatCandidateWire>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum OverlaydEvent {
PeerHealthChanged { pubkey: String, healthy: bool },
NatEndpointChanged { pubkey: String, endpoint: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusSnapshot {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_ip: Option<IpAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overlay_cidr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slice_cidr: Option<String>,
pub peer_count: u32,
pub service_count: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub peers: Vec<PeerStatus>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dedicated_services: Vec<DedicatedServiceStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DedicatedServiceStatus {
pub service: String,
pub interface: String,
pub public_key: String,
pub listen_port: u16,
pub overlay_ip: std::net::IpAddr,
pub subnet: String,
pub peer_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerStatus {
pub public_key: String,
pub endpoint: String,
pub allowed_ips: String,
pub last_handshake_unix_secs: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NatStatusWire {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub candidates: Vec<NatCandidateWire>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub peers: Vec<NatPeerWire>,
#[serde(default)]
pub last_refresh: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NatPeerWire {
pub node_id: String,
pub connection_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_endpoint: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nat_wire::{RelayServerSpec, TurnServerSpec};
fn roundtrip(frame: &OverlaydFrame) {
let json = serde_json::to_string(frame).expect("serialize");
let back: OverlaydFrame = serde_json::from_str(&json).expect("deserialize");
assert_eq!(frame, &back, "frame must round-trip; json was {json}");
}
#[test]
fn request_frames_round_trip() {
roundtrip(&OverlaydFrame::Request {
id: 1,
request: OverlaydRequest::SetupGlobalOverlay {
deployment: "prod".into(),
instance_id: "42".into(),
cluster_cidr: "10.200.0.0/16".into(),
slice_cidr: Some("10.200.0.0/28".into()),
wg_port: 51820,
host_adapter_mandatory: true,
nat: Some(NatConfigSpec {
enabled: true,
stun_servers: vec!["stun.l.google.com:19302".into()],
turn_servers: vec![TurnServerSpec {
addr: "turn.example.com:3478".into(),
username: "u".into(),
credential: "p".into(),
}],
hole_punch_timeout_secs: 15,
stun_refresh_interval_secs: 60,
max_candidate_pairs: 10,
relay_server: Some(RelayServerSpec {
listen_port: 3478,
external_addr: "1.2.3.4:3478".into(),
max_sessions: 100,
auth_credential: Some("cluster-secret".into()),
}),
}),
},
});
roundtrip(&OverlaydFrame::Request {
id: 2,
request: OverlaydRequest::AttachContainer {
handle: AttachHandle::WindowsContainer {
container_id: "ctr-abc".into(),
ip: Some("10.200.0.5".parse().unwrap()),
},
service: "web".into(),
join_global: false,
ephemeral: false,
isolation_network: None,
dns_server: Some("10.200.0.1".parse().unwrap()),
dns_domain: Some("overlay".into()),
},
});
roundtrip(&OverlaydFrame::Request {
id: 3,
request: OverlaydRequest::AttachContainer {
handle: AttachHandle::LinuxPid { pid: 4242 },
service: "web".into(),
join_global: true,
ephemeral: false,
isolation_network: Some("job-net".into()),
dns_server: None,
dns_domain: None,
},
});
}
#[test]
fn response_and_event_frames_round_trip() {
roundtrip(&OverlaydFrame::Response {
id: 2,
response: OverlaydResponse::Attached(AttachResult {
ip: "10.200.0.5".parse().unwrap(),
namespace_guid: Some("aabbccdd-eeff-0011-2233-445566778899".into()),
}),
});
roundtrip(&OverlaydFrame::Response {
id: 9,
response: OverlaydResponse::Err {
message: "no slice assigned".into(),
},
});
roundtrip(&OverlaydFrame::Event(OverlaydEvent::PeerHealthChanged {
pubkey: "base64key".into(),
healthy: false,
}));
}
#[test]
fn prune_orphan_bridges_round_trips() {
roundtrip(&OverlaydFrame::Request {
id: 30,
request: OverlaydRequest::PruneOrphanBridges {
live_bridge_names: vec!["zl-prod-0-web-b".into(), "zl-prod-0-api-b".into()],
},
});
roundtrip(&OverlaydFrame::Response {
id: 30,
response: OverlaydResponse::PrunedBridges {
reclaimed: vec!["zl-1ca4568944-b".into(), "zl-81c6bc17c7-b".into()],
},
});
}
#[test]
fn status_snapshot_round_trips_and_defaults() {
roundtrip(&OverlaydFrame::Response {
id: 7,
response: OverlaydResponse::Status(StatusSnapshot {
interface: Some("zl-overlay0".into()),
node_ip: Some("10.200.0.1".parse().unwrap()),
peer_count: 2,
service_count: 1,
peers: vec![PeerStatus {
public_key: "k".into(),
endpoint: "1.2.3.4:51820".into(),
allowed_ips: "10.200.0.2/32".into(),
last_handshake_unix_secs: 0,
}],
..StatusSnapshot::default()
}),
});
}
fn sample_peer() -> PeerSpec {
PeerSpec {
public_key: "base64key".into(),
endpoint: "1.2.3.4:51820".into(),
allowed_ips: "10.200.0.2/32".into(),
persistent_keepalive_secs: 25,
candidates: vec![
NatCandidateWire {
candidate_type: "host".into(),
address: "192.168.1.5:51820".into(),
priority: 100,
},
NatCandidateWire {
candidate_type: "server-reflexive".into(),
address: "203.0.113.5:51820".into(),
priority: 50,
},
],
}
}
#[test]
fn peer_ops_round_trip_both_scopes() {
roundtrip(&OverlaydFrame::Request {
id: 1,
request: OverlaydRequest::AddPeer {
peer: sample_peer(),
scope: PeerScope::Global,
},
});
roundtrip(&OverlaydFrame::Request {
id: 2,
request: OverlaydRequest::AddPeer {
peer: sample_peer(),
scope: PeerScope::Service {
service: "web".into(),
},
},
});
roundtrip(&OverlaydFrame::Request {
id: 3,
request: OverlaydRequest::RemovePeer {
pubkey: "k".into(),
scope: PeerScope::Global,
},
});
roundtrip(&OverlaydFrame::Request {
id: 4,
request: OverlaydRequest::RemovePeer {
pubkey: "k".into(),
scope: PeerScope::Service {
service: "web".into(),
},
},
});
roundtrip(&OverlaydFrame::Request {
id: 5,
request: OverlaydRequest::AddAllowedIp {
pubkey: "k".into(),
cidr: "10.200.1.0/24".into(),
scope: PeerScope::Global,
},
});
roundtrip(&OverlaydFrame::Request {
id: 6,
request: OverlaydRequest::AddAllowedIp {
pubkey: "k".into(),
cidr: "10.200.1.0/24".into(),
scope: PeerScope::Service {
service: "web".into(),
},
},
});
roundtrip(&OverlaydFrame::Request {
id: 7,
request: OverlaydRequest::RemoveAllowedIp {
pubkey: "k".into(),
cidr: "10.200.1.0/24".into(),
scope: PeerScope::Global,
},
});
roundtrip(&OverlaydFrame::Request {
id: 8,
request: OverlaydRequest::RemoveAllowedIp {
pubkey: "k".into(),
cidr: "10.200.1.0/24".into(),
scope: PeerScope::Service {
service: "web".into(),
},
},
});
}
#[test]
fn add_peer_without_scope_defaults_to_global() {
let json = r#"{
"frame": "request",
"id": 11,
"request": {
"op": "add_peer",
"public_key": "base64key",
"endpoint": "1.2.3.4:51820",
"allowed_ips": "10.200.0.2/32",
"persistent_keepalive_secs": 25
}
}"#;
let frame: OverlaydFrame = serde_json::from_str(json).expect("deserialize");
match frame {
OverlaydFrame::Request {
request: OverlaydRequest::AddPeer { scope, peer },
..
} => {
assert_eq!(scope, PeerScope::Global);
assert_eq!(peer.public_key, "base64key");
assert!(peer.candidates.is_empty());
}
other => panic!("expected AddPeer request, got {other:?}"),
}
}
#[test]
fn setup_global_overlay_without_nat_field_defaults_to_none() {
let json = r#"{
"frame": "request",
"id": 12,
"request": {
"op": "setup_global_overlay",
"deployment": "prod",
"instance_id": "1",
"cluster_cidr": "10.200.0.0/16",
"wg_port": 51820
}
}"#;
let frame: OverlaydFrame = serde_json::from_str(json).expect("deserialize");
match frame {
OverlaydFrame::Request {
request: OverlaydRequest::SetupGlobalOverlay { nat, .. },
..
} => assert!(nat.is_none(), "missing nat field must default to None"),
other => panic!("expected SetupGlobalOverlay, got {other:?}"),
}
}
#[test]
fn add_peer_with_candidates_round_trips() {
roundtrip(&OverlaydFrame::Request {
id: 40,
request: OverlaydRequest::AddPeer {
peer: sample_peer(),
scope: PeerScope::Global,
},
});
}
#[test]
fn nat_status_request_and_response_round_trip() {
roundtrip(&OverlaydFrame::Request {
id: 41,
request: OverlaydRequest::NatStatus,
});
roundtrip(&OverlaydFrame::Response {
id: 41,
response: OverlaydResponse::NatStatus(NatStatusWire {
candidates: vec![NatCandidateWire {
candidate_type: "host".into(),
address: "192.168.1.5:51820".into(),
priority: 100,
}],
peers: vec![NatPeerWire {
node_id: "base64peerkey".into(),
connection_type: "hole-punched".into(),
remote_endpoint: Some("203.0.113.9:51820".into()),
}],
last_refresh: 1_700_000_000,
}),
});
roundtrip(&OverlaydFrame::Response {
id: 42,
response: OverlaydResponse::NatStatus(NatStatusWire::default()),
});
}
#[test]
fn service_overlay_response_round_trips_both_shapes() {
roundtrip(&OverlaydFrame::Response {
id: 20,
response: OverlaydResponse::ServiceOverlay(ServiceOverlayInfo {
name: "web".into(),
mode: crate::overlay::OverlayMode::Shared,
wg_public_key: None,
wg_port: None,
overlay_ip: None,
subnet: None,
}),
});
roundtrip(&OverlaydFrame::Response {
id: 21,
response: OverlaydResponse::ServiceOverlay(ServiceOverlayInfo {
name: "web".into(),
mode: crate::overlay::OverlayMode::Dedicated,
wg_public_key: Some("svc-key".into()),
wg_port: Some(51821),
overlay_ip: Some("10.201.0.1".parse().unwrap()),
subnet: Some("10.201.0.0/24".into()),
}),
});
}
#[test]
fn status_snapshot_with_dedicated_service_round_trips() {
roundtrip(&OverlaydFrame::Response {
id: 22,
response: OverlaydResponse::Status(StatusSnapshot {
interface: Some("zl-overlay0".into()),
node_ip: Some("10.200.0.1".parse().unwrap()),
peer_count: 1,
service_count: 1,
dedicated_services: vec![DedicatedServiceStatus {
service: "web".into(),
interface: "zl-svc-web0".into(),
public_key: "svc-key".into(),
listen_port: 51821,
overlay_ip: "10.201.0.1".parse().unwrap(),
subnet: "10.201.0.0/24".into(),
peer_count: 3,
}],
..StatusSnapshot::default()
}),
});
}
}