use std::net::IpAddr;
use serde::{Deserialize, Serialize};
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 },
}
#[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,
nat_enabled: bool,
},
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, 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 },
Status,
NatTick,
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),
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,
}
#[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,
}
#[cfg(test)]
mod tests {
use super::*;
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,
nat_enabled: true,
},
});
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,
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,
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 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,
}
}
#[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");
}
other => panic!("expected AddPeer request, got {other:?}"),
}
}
#[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()
}),
});
}
}