zlayer_types/overlayd.rs
1//! IPC wire protocol between the main `zlayer` daemon and `zlayer-overlayd`.
2//!
3//! `zlayer-overlayd` is a standalone, long-lived daemon that owns every
4//! mechanism touching the overlay/network plane (the `WireGuard` device +
5//! adapter, peers, `AllowedIPs`/service subnets, IP allocation, DNS, NAT,
6//! Linux bridges + veth/netns attach, and the Windows HCN Internal network +
7//! endpoints). The main `zlayer` daemon keeps the cluster brain (Raft, the
8//! scheduler, the service registry, container/HCS process lifecycle) and
9//! drives overlayd over a length-prefixed-JSON IPC channel (a Unix domain
10//! socket on Unix, a named pipe on Windows).
11//!
12//! This module is that channel's **wire contract** — the only thing both
13//! sides must agree on. It lives in `zlayer-types` (a leaf crate) so the
14//! daemon, the overlayd server, and the overlayd client can all depend on it
15//! without a dependency cycle.
16//!
17//! ## Framing
18//!
19//! One connection multiplexes request/response and server→client event push.
20//! Each frame is a [`OverlaydFrame`] serialized as JSON and written with a
21//! `u32` little-endian length prefix (the framing itself lives in
22//! `zlayer-overlayd`'s transport module, not here). The main daemon sends
23//! [`OverlaydFrame::Request`]s each carrying a client-chosen `id`; overlayd
24//! replies with a [`OverlaydFrame::Response`] echoing that `id`, and may at
25//! any time push an unsolicited [`OverlaydFrame::Event`].
26//!
27//! ## Wire-type conventions
28//!
29//! - Windows HCN GUIDs cross the wire as **bare lowercase strings**
30//! (`aabbccdd-eeff-...`, no braces) — `windows::core::GUID` is not
31//! `serde`-serializable and `zlayer-types` must not depend on `windows`.
32//! - CIDRs cross as `String` (e.g. `"10.200.0.0/28"`); endpoints as `String`
33//! (`"host:port"`, kept textual so an unresolved/hostname endpoint survives).
34//! - Addresses use [`std::net::IpAddr`] (serde-serializable via `std`).
35
36use std::net::IpAddr;
37
38use serde::{Deserialize, Serialize};
39
40pub use crate::overlay::{OverlayConfig, OverlayMode};
41
42/// Wire-protocol version. Bump on any breaking change to the frame/request/
43/// response/event shapes so a version-skewed daemon/overlayd pair can detect
44/// the mismatch instead of silently misparsing.
45pub const PROTOCOL_VERSION: u32 = 1;
46
47/// A multiplexed frame on the overlayd IPC connection.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(tag = "frame", rename_all = "snake_case")]
50pub enum OverlaydFrame {
51 /// Main daemon → overlayd. `id` is echoed back on the matching response.
52 Request { id: u64, request: OverlaydRequest },
53 /// overlayd → main daemon, answering the request with the same `id`.
54 Response { id: u64, response: OverlaydResponse },
55 /// overlayd → main daemon, unsolicited (no `id`).
56 Event(OverlaydEvent),
57}
58
59/// Identifies the container overlayd must wire into the overlay. The agent
60/// owns the container's process/compute-system lifecycle and hands overlayd
61/// just enough to attach it.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(tag = "platform", rename_all = "snake_case")]
64pub enum AttachHandle {
65 /// Linux: the container's PID. overlayd opens `/proc/<pid>/ns/net` and
66 /// creates the veth pair into that network namespace.
67 LinuxPid { pid: u32 },
68 /// Windows: the HCS container id (+ the IP the agent reserved, if any).
69 /// overlayd creates the HCN endpoint + per-container namespace on its HCN
70 /// Internal network and returns the bare-lowercase namespace GUID
71 /// ([`AttachResult::namespace_guid`]) for the agent to embed in the
72 /// compute-system document's `Container.Networking.Namespace`.
73 WindowsContainer {
74 container_id: String,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 ip: Option<IpAddr>,
77 },
78 /// A guest that manages its own overlay interface (the macOS VZ-Linux VM
79 /// runtime). overlayd cannot enter the guest's netns (it is a VM, not a host
80 /// process), so instead of building a veth it **allocates the overlay
81 /// identity** — keypair, address, and the current peer set — registers the
82 /// generated public key in the mesh, and returns it as
83 /// [`OverlaydResponse::GuestConfig`]. The caller ships that config into the
84 /// guest (over vsock) where a kernel `WireGuard` device is brought up. `id` is
85 /// the opaque container id used to scope the allocation + the registered
86 /// peer so `DetachContainer` can release it.
87 GuestManaged { id: String },
88}
89
90/// A request from the main daemon to overlayd.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(tag = "op", rename_all = "snake_case")]
93pub enum OverlaydRequest {
94 /// Push this node's Raft id (cluster-brain context overlayd scopes by).
95 SetLocalNodeId { node_id: u64 },
96 /// Push this node's `WireGuard` public key (base64).
97 SetLocalWgPubkey { pubkey: String },
98
99 /// Bring up (or reuse) this node's base/global overlay. Idempotent: if the
100 /// overlay network already exists (recorded in overlayd's marker), it is
101 /// reused rather than recreated. This is the only place the base overlay
102 /// is created; it is torn down only on a full uninstall.
103 SetupGlobalOverlay {
104 deployment: String,
105 instance_id: String,
106 /// Full cluster CIDR, e.g. `"10.200.0.0/16"`.
107 cluster_cidr: String,
108 /// This node's per-node slice, e.g. `"10.200.0.0/28"`. `None` until the
109 /// leader assigns one.
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 slice_cidr: Option<String>,
112 wg_port: u16,
113 nat_enabled: bool,
114 },
115 /// Tear down the node's base overlay (e.g. on full uninstall).
116 TeardownGlobalOverlay,
117
118 /// Create the per-service overlay segment (Linux bridge / Windows HCN
119 /// Internal network) for `service`. Returns [`OverlaydResponse::BridgeName`].
120 SetupServiceOverlay { service: String, mode: OverlayMode },
121 /// Remove the per-service overlay segment.
122 TeardownServiceOverlay { service: String },
123
124 /// Allocate (or, with `ip` set on a later attach, validate) an overlay IP
125 /// from the node slice for a container on `service`.
126 AllocateIp { service: String, join_global: bool },
127 /// Return an overlay IP to the allocator.
128 ReleaseIp { ip: IpAddr },
129
130 /// Wire a container into the overlay. Returns [`OverlaydResponse::Attached`].
131 AttachContainer {
132 handle: AttachHandle,
133 service: String,
134 join_global: bool,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 dns_server: Option<IpAddr>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 dns_domain: Option<String>,
139 },
140 /// Tear down a container's overlay attachment and release its IP.
141 DetachContainer { handle: AttachHandle },
142
143 /// Add a `WireGuard` peer to the base overlay.
144 AddPeer {
145 #[serde(flatten)]
146 peer: PeerSpec,
147 #[serde(default)]
148 scope: PeerScope,
149 },
150 /// Remove a peer by its base64 public key.
151 RemovePeer {
152 pubkey: String,
153 #[serde(default)]
154 scope: PeerScope,
155 },
156 /// Plumb a service subnet into a peer's `AllowedIPs`.
157 AddAllowedIp {
158 pubkey: String,
159 cidr: String,
160 #[serde(default)]
161 scope: PeerScope,
162 },
163 /// Remove a service subnet from a peer's `AllowedIPs`.
164 RemoveAllowedIp {
165 pubkey: String,
166 cidr: String,
167 #[serde(default)]
168 scope: PeerScope,
169 },
170
171 /// Register an overlay DNS A/AAAA record.
172 RegisterDns { name: String, ip: IpAddr },
173 /// Remove an overlay DNS record.
174 UnregisterDns { name: String },
175
176 /// Snapshot overlay state for diagnostics. Returns [`OverlaydResponse::Status`].
177 Status,
178 /// Run one NAT-traversal maintenance tick (probe/refresh endpoints).
179 NatTick,
180 /// Ask overlayd to shut down gracefully (drops the adapter).
181 Shutdown,
182}
183
184/// overlayd's answer to an [`OverlaydRequest`].
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(tag = "result", rename_all = "snake_case")]
187pub enum OverlaydResponse {
188 /// Generic success with no payload.
189 Ok,
190 /// An allocated/validated overlay IP (`AllocateIp`).
191 Ip { ip: IpAddr },
192 /// A completed container attach.
193 Attached(AttachResult),
194 /// The overlay identity for a guest-managed attach
195 /// ([`AttachHandle::GuestManaged`]): the keypair, allocated address, and the
196 /// peer set the guest should configure on its own `WireGuard` device.
197 GuestConfig(GuestOverlayConfig),
198 /// The interface/bridge/network name created (`SetupServiceOverlay`,
199 /// `SetupGlobalOverlay`).
200 BridgeName { name: String },
201 /// A diagnostics snapshot (`Status`).
202 Status(StatusSnapshot),
203 /// A dedicated per-service overlay device's identity (`SetupServiceOverlay`
204 /// in Dedicated mode). Not yet produced by the server — the server still
205 /// returns [`OverlaydResponse::BridgeName`] for now; this variant is the
206 /// wire contract for a later task that switches Dedicated setup over.
207 ServiceOverlay(ServiceOverlayInfo),
208 /// The request failed; `message` is a human-readable reason.
209 Err { message: String },
210}
211
212/// Identity of a dedicated per-service overlay device, reported by
213/// `SetupServiceOverlay` once Dedicated mode is wired up. Shared-mode setups
214/// leave the `wg_*`/`overlay_ip`/`subnet` fields `None`.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct ServiceOverlayInfo {
217 pub name: String,
218 pub mode: crate::overlay::OverlayMode,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub wg_public_key: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub wg_port: Option<u16>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub overlay_ip: Option<std::net::IpAddr>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub subnet: Option<String>,
227}
228
229/// Result of [`OverlaydRequest::AttachContainer`].
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct AttachResult {
232 /// The container's overlay IP.
233 pub ip: IpAddr,
234 /// Windows only: the bare-lowercase HCN namespace GUID the agent embeds in
235 /// the compute-system document. `None` on Linux (no HCN namespace).
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub namespace_guid: Option<String>,
238}
239
240/// Overlay identity returned for a guest-managed attach
241/// ([`AttachHandle::GuestManaged`] → [`OverlaydResponse::GuestConfig`]).
242///
243/// The host allocated the address from the node slice, generated the keypair,
244/// and registered `public_key` in the mesh (so peers route to the guest). The
245/// caller ships everything except `public_key` into the guest; `public_key` is
246/// echoed back so the caller can record/deregister the peer it represents.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct GuestOverlayConfig {
249 /// The guest's allocated overlay address.
250 pub overlay_ip: IpAddr,
251 /// Prefix length of the overlay network (interface address + on-link route).
252 pub prefix_len: u8,
253 /// Base64 `WireGuard` private key for the guest's overlay endpoint.
254 pub private_key: String,
255 /// Base64 `WireGuard` public key matching `private_key` (registered in the
256 /// mesh by overlayd; echoed for the caller's bookkeeping).
257 pub public_key: String,
258 /// UDP port the guest's `WireGuard` device should listen on.
259 pub listen_port: u16,
260 /// The peers the guest should configure (other nodes/containers).
261 pub peers: Vec<PeerSpec>,
262 /// Overlay DNS resolver IP for the container, if any.
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub dns_server: Option<IpAddr>,
265 /// Overlay DNS search domain, if any.
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub dns_domain: Option<String>,
268}
269
270/// Which overlay device a peer / `AllowedIP` op targets. `Global` (default, and
271/// the only value pre-Dedicated senders emit) = the single cluster transport.
272/// `Service` = that service's dedicated per-service transport.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
274#[serde(tag = "scope", rename_all = "snake_case")]
275pub enum PeerScope {
276 #[default]
277 Global,
278 Service {
279 service: String,
280 },
281}
282
283/// A `WireGuard` peer to add to the base overlay. Mirrors
284/// `zlayer_overlay::PeerInfo` but with wire-safe field types.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct PeerSpec {
287 /// base64 `WireGuard` public key.
288 pub public_key: String,
289 /// `host:port` (textual so an unresolved/hostname endpoint survives).
290 pub endpoint: String,
291 /// Comma-separated CIDR list (e.g. `"10.200.0.5/32,10.200.1.0/24"`).
292 pub allowed_ips: String,
293 /// Persistent-keepalive interval, in seconds (0 = disabled).
294 pub persistent_keepalive_secs: u64,
295}
296
297/// An unsolicited notification pushed from overlayd to the main daemon.
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(tag = "event", rename_all = "snake_case")]
300pub enum OverlaydEvent {
301 /// A peer's liveness changed (handshake seen / lost).
302 PeerHealthChanged { pubkey: String, healthy: bool },
303 /// NAT traversal moved a peer to a new endpoint.
304 NatEndpointChanged { pubkey: String, endpoint: String },
305}
306
307/// Diagnostics snapshot returned by [`OverlaydRequest::Status`].
308#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
309pub struct StatusSnapshot {
310 /// Base overlay interface name (e.g. `"zl-overlay0"`), if up.
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub interface: Option<String>,
313 /// This node's overlay IP, if assigned.
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub node_ip: Option<IpAddr>,
316 /// This node's `WireGuard` public key (base64), if up.
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub public_key: Option<String>,
319 /// Full cluster CIDR.
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub overlay_cidr: Option<String>,
322 /// This node's per-node slice CIDR.
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub slice_cidr: Option<String>,
325 /// Number of base-overlay peers.
326 pub peer_count: u32,
327 /// Number of per-service overlays set up on this node.
328 pub service_count: u32,
329 /// Per-peer status.
330 #[serde(default, skip_serializing_if = "Vec::is_empty")]
331 pub peers: Vec<PeerStatus>,
332 /// Per dedicated per-service overlay device status. Empty unless one or
333 /// more services run in `OverlayMode::Dedicated` on this node.
334 #[serde(default, skip_serializing_if = "Vec::is_empty")]
335 pub dedicated_services: Vec<DedicatedServiceStatus>,
336}
337
338/// Status of a single dedicated per-service overlay device within a
339/// [`StatusSnapshot`].
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct DedicatedServiceStatus {
342 pub service: String,
343 pub interface: String,
344 pub public_key: String,
345 pub listen_port: u16,
346 pub overlay_ip: std::net::IpAddr,
347 pub subnet: String,
348 pub peer_count: u32,
349}
350
351/// Per-peer status within a [`StatusSnapshot`].
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct PeerStatus {
354 pub public_key: String,
355 pub endpoint: String,
356 pub allowed_ips: String,
357 /// Last successful handshake, Unix seconds; `0` if never.
358 pub last_handshake_unix_secs: i64,
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 /// A frame round-trips through JSON unchanged (the core wire guarantee).
366 fn roundtrip(frame: &OverlaydFrame) {
367 let json = serde_json::to_string(frame).expect("serialize");
368 let back: OverlaydFrame = serde_json::from_str(&json).expect("deserialize");
369 assert_eq!(frame, &back, "frame must round-trip; json was {json}");
370 }
371
372 #[test]
373 fn request_frames_round_trip() {
374 roundtrip(&OverlaydFrame::Request {
375 id: 1,
376 request: OverlaydRequest::SetupGlobalOverlay {
377 deployment: "prod".into(),
378 instance_id: "42".into(),
379 cluster_cidr: "10.200.0.0/16".into(),
380 slice_cidr: Some("10.200.0.0/28".into()),
381 wg_port: 51820,
382 nat_enabled: true,
383 },
384 });
385 roundtrip(&OverlaydFrame::Request {
386 id: 2,
387 request: OverlaydRequest::AttachContainer {
388 handle: AttachHandle::WindowsContainer {
389 container_id: "ctr-abc".into(),
390 ip: Some("10.200.0.5".parse().unwrap()),
391 },
392 service: "web".into(),
393 join_global: false,
394 dns_server: Some("10.200.0.1".parse().unwrap()),
395 dns_domain: Some("overlay".into()),
396 },
397 });
398 roundtrip(&OverlaydFrame::Request {
399 id: 3,
400 request: OverlaydRequest::AttachContainer {
401 handle: AttachHandle::LinuxPid { pid: 4242 },
402 service: "web".into(),
403 join_global: true,
404 dns_server: None,
405 dns_domain: None,
406 },
407 });
408 }
409
410 #[test]
411 fn response_and_event_frames_round_trip() {
412 roundtrip(&OverlaydFrame::Response {
413 id: 2,
414 response: OverlaydResponse::Attached(AttachResult {
415 ip: "10.200.0.5".parse().unwrap(),
416 namespace_guid: Some("aabbccdd-eeff-0011-2233-445566778899".into()),
417 }),
418 });
419 roundtrip(&OverlaydFrame::Response {
420 id: 9,
421 response: OverlaydResponse::Err {
422 message: "no slice assigned".into(),
423 },
424 });
425 roundtrip(&OverlaydFrame::Event(OverlaydEvent::PeerHealthChanged {
426 pubkey: "base64key".into(),
427 healthy: false,
428 }));
429 }
430
431 #[test]
432 fn status_snapshot_round_trips_and_defaults() {
433 roundtrip(&OverlaydFrame::Response {
434 id: 7,
435 response: OverlaydResponse::Status(StatusSnapshot {
436 interface: Some("zl-overlay0".into()),
437 node_ip: Some("10.200.0.1".parse().unwrap()),
438 peer_count: 2,
439 service_count: 1,
440 peers: vec![PeerStatus {
441 public_key: "k".into(),
442 endpoint: "1.2.3.4:51820".into(),
443 allowed_ips: "10.200.0.2/32".into(),
444 last_handshake_unix_secs: 0,
445 }],
446 ..StatusSnapshot::default()
447 }),
448 });
449 }
450
451 fn sample_peer() -> PeerSpec {
452 PeerSpec {
453 public_key: "base64key".into(),
454 endpoint: "1.2.3.4:51820".into(),
455 allowed_ips: "10.200.0.2/32".into(),
456 persistent_keepalive_secs: 25,
457 }
458 }
459
460 #[test]
461 fn peer_ops_round_trip_both_scopes() {
462 // AddPeer, global (default) + service scope.
463 roundtrip(&OverlaydFrame::Request {
464 id: 1,
465 request: OverlaydRequest::AddPeer {
466 peer: sample_peer(),
467 scope: PeerScope::Global,
468 },
469 });
470 roundtrip(&OverlaydFrame::Request {
471 id: 2,
472 request: OverlaydRequest::AddPeer {
473 peer: sample_peer(),
474 scope: PeerScope::Service {
475 service: "web".into(),
476 },
477 },
478 });
479 // RemovePeer.
480 roundtrip(&OverlaydFrame::Request {
481 id: 3,
482 request: OverlaydRequest::RemovePeer {
483 pubkey: "k".into(),
484 scope: PeerScope::Global,
485 },
486 });
487 roundtrip(&OverlaydFrame::Request {
488 id: 4,
489 request: OverlaydRequest::RemovePeer {
490 pubkey: "k".into(),
491 scope: PeerScope::Service {
492 service: "web".into(),
493 },
494 },
495 });
496 // AddAllowedIp.
497 roundtrip(&OverlaydFrame::Request {
498 id: 5,
499 request: OverlaydRequest::AddAllowedIp {
500 pubkey: "k".into(),
501 cidr: "10.200.1.0/24".into(),
502 scope: PeerScope::Global,
503 },
504 });
505 roundtrip(&OverlaydFrame::Request {
506 id: 6,
507 request: OverlaydRequest::AddAllowedIp {
508 pubkey: "k".into(),
509 cidr: "10.200.1.0/24".into(),
510 scope: PeerScope::Service {
511 service: "web".into(),
512 },
513 },
514 });
515 // RemoveAllowedIp.
516 roundtrip(&OverlaydFrame::Request {
517 id: 7,
518 request: OverlaydRequest::RemoveAllowedIp {
519 pubkey: "k".into(),
520 cidr: "10.200.1.0/24".into(),
521 scope: PeerScope::Global,
522 },
523 });
524 roundtrip(&OverlaydFrame::Request {
525 id: 8,
526 request: OverlaydRequest::RemoveAllowedIp {
527 pubkey: "k".into(),
528 cidr: "10.200.1.0/24".into(),
529 scope: PeerScope::Service {
530 service: "web".into(),
531 },
532 },
533 });
534 }
535
536 #[test]
537 fn add_peer_without_scope_defaults_to_global() {
538 // A pre-Dedicated sender emits no `scope` field. The frame is tagged
539 // `frame: "request"`, the request `op: "add_peer"`, and `PeerSpec` is
540 // flattened so its fields sit at the request level.
541 let json = r#"{
542 "frame": "request",
543 "id": 11,
544 "request": {
545 "op": "add_peer",
546 "public_key": "base64key",
547 "endpoint": "1.2.3.4:51820",
548 "allowed_ips": "10.200.0.2/32",
549 "persistent_keepalive_secs": 25
550 }
551 }"#;
552 let frame: OverlaydFrame = serde_json::from_str(json).expect("deserialize");
553 match frame {
554 OverlaydFrame::Request {
555 request: OverlaydRequest::AddPeer { scope, peer },
556 ..
557 } => {
558 assert_eq!(scope, PeerScope::Global);
559 assert_eq!(peer.public_key, "base64key");
560 }
561 other => panic!("expected AddPeer request, got {other:?}"),
562 }
563 }
564
565 #[test]
566 fn service_overlay_response_round_trips_both_shapes() {
567 // Shared shape: identity fields are None.
568 roundtrip(&OverlaydFrame::Response {
569 id: 20,
570 response: OverlaydResponse::ServiceOverlay(ServiceOverlayInfo {
571 name: "web".into(),
572 mode: crate::overlay::OverlayMode::Shared,
573 wg_public_key: None,
574 wg_port: None,
575 overlay_ip: None,
576 subnet: None,
577 }),
578 });
579 // Dedicated shape: all identity fields populated.
580 roundtrip(&OverlaydFrame::Response {
581 id: 21,
582 response: OverlaydResponse::ServiceOverlay(ServiceOverlayInfo {
583 name: "web".into(),
584 mode: crate::overlay::OverlayMode::Dedicated,
585 wg_public_key: Some("svc-key".into()),
586 wg_port: Some(51821),
587 overlay_ip: Some("10.201.0.1".parse().unwrap()),
588 subnet: Some("10.201.0.0/24".into()),
589 }),
590 });
591 }
592
593 #[test]
594 fn status_snapshot_with_dedicated_service_round_trips() {
595 roundtrip(&OverlaydFrame::Response {
596 id: 22,
597 response: OverlaydResponse::Status(StatusSnapshot {
598 interface: Some("zl-overlay0".into()),
599 node_ip: Some("10.200.0.1".parse().unwrap()),
600 peer_count: 1,
601 service_count: 1,
602 dedicated_services: vec![DedicatedServiceStatus {
603 service: "web".into(),
604 interface: "zl-svc-web0".into(),
605 public_key: "svc-key".into(),
606 listen_port: 51821,
607 overlay_ip: "10.201.0.1".parse().unwrap(),
608 subnet: "10.201.0.0/24".into(),
609 peer_count: 3,
610 }],
611 ..StatusSnapshot::default()
612 }),
613 });
614 }
615}