1use serde::{Deserialize, Serialize};
12
13pub const VERB_PING: &str = "ping";
14pub const VERB_STATS: &str = "stats";
15pub const VERB_SHUTDOWN: &str = "shutdown";
16pub const VERB_GET_CONFIG: &str = "get_config";
17pub const VERB_RELOAD: &str = "reload";
18pub const VERB_COMPILE_DRY_RUN: &str = "compile_dry_run";
19pub const VERB_GET_CONNECTIONS: &str = "get_connections";
20pub const VERB_TAIL_FLOW: &str = "tail_flow";
21pub const VERB_TAIL_LOG: &str = "tail_log";
22pub const VERB_GET_METRICS: &str = "get_metrics";
23pub const VERB_GET_POOLS: &str = "get_pools";
24pub const VERB_GET_UPSTREAMS: &str = "get_upstreams";
25pub const VERB_RELOAD_NATIVE_ROOTS: &str = "reload_native_roots";
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct NoArgs {}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct PingResult {
37 pub pong: bool,
38 pub version: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43pub struct StatsResult {
44 pub uptime_ms: u64,
45 pub graph_version_hash: String,
47 pub listeners: Vec<ListenerStatus>,
48 #[serde(default)]
52 pub flow_log_subscribers: usize,
53 #[serde(default)]
55 pub tracing_log_subscribers: usize,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59pub struct ListenerStatus {
60 pub addr: String,
61 pub bound: bool,
62 pub in_flight_count: usize,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub struct ShutdownResult {
67 pub draining: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GetConfigResult {
75 pub graph: serde_json::Value,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(tag = "kind", rename_all = "snake_case")]
83pub enum ReloadResult {
84 Swapped { hash: String },
86 Unchanged { hash: String },
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub struct ReloadNativeRootsResult {
95 pub anchors: usize,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CompileDryRunArgs {
100 pub config_dir: String,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct CompileDryRunResult {
106 pub graph: serde_json::Value,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct ConnectionInfo {
115 pub conn_id: String,
116 pub listener_addr: String,
117 pub remote: String,
118 pub age_ms: u64,
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct GetMetricsArgs {
129 #[serde(default)]
132 pub format: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(tag = "format", rename_all = "snake_case")]
139pub enum GetMetricsResult {
140 Prometheus { body: String },
141 Json { metrics: serde_json::Value },
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub struct GetConnectionsResult {
147 pub listeners: Vec<ListenerStatus>,
148 #[serde(default)]
149 pub connections: Vec<ConnectionInfo>,
150}
151
152#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
161pub struct GetPoolsResult {
162 #[serde(default)]
163 pub wasm: Vec<WasmPoolEntry>,
164 #[serde(default)]
168 pub cgi: Option<CgiPoolEntry>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172pub struct WasmPoolEntry {
173 pub kind: String,
175 pub key: String,
177 pub export: String,
179 pub capacity: usize,
180 pub available: usize,
181 pub in_use: usize,
182 #[serde(default)]
185 pub total_allocations: u64,
186 #[serde(default)]
188 pub failures: u64,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct CgiPoolEntry {
193 pub cap: usize,
194 pub available: usize,
195 pub in_use: usize,
196 #[serde(default)]
199 pub total_allocations: u64,
200 #[serde(default)]
203 pub failures: u64,
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
209pub struct GetUpstreamsResult {
210 #[serde(default)]
211 pub tcp: Vec<TcpUpstreamEntry>,
212 #[serde(default)]
214 pub quic: Vec<QuicUpstreamEntry>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218pub struct TcpUpstreamEntry {
219 pub version: String,
221 pub scheme: String,
223 pub root_ca: String,
226 pub verify_mode: String,
228 pub alpn: Vec<String>,
229 pub dns: String,
232 #[serde(default)]
236 pub fingerprint_id: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240pub struct QuicUpstreamEntry {
241 pub remote_addr: String,
242 pub sni: String,
243 pub alpn: Vec<String>,
244 #[serde(default)]
246 pub fingerprint_id: String,
247}
248
249pub const VERB_POOL_DRAIN: &str = "pool_drain";
255
256#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
257pub struct PoolDrainArgs {
258 pub fingerprint_id: String,
259}
260
261#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
262pub struct PoolDrainResult {
263 pub tcp_drained: usize,
266 pub quic_drained: usize,
269}
270
271pub const VERB_FORCE_RENEW: &str = "force_renew";
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
280pub struct ForceRenewArgs {
281 pub sni: String,
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
285pub struct ForceRenewResult {
286 pub queued: bool,
291 pub current_status: String,
296}
297
298pub const VERB_GET_CERTS: &str = "get_certs";
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
304pub struct GetCertsResult {
305 pub certs: Vec<CertSummary>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
319pub struct CertSummary {
320 pub sni: String,
321 pub source: String,
324 #[serde(default)]
325 pub san: Vec<String>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub not_after: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub issued_at: Option<String>,
332 #[serde(default)]
335 pub status: String,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub last_attempt_at: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub last_error: Option<String>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub next_attempt_at: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub ari_window: Option<AriWindowWire>,
344 #[serde(default)]
355 pub ocsp_status: String,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub ocsp_next_update: Option<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub ocsp_aia_url: Option<String>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
368pub struct AriWindowWire {
369 pub start: String,
371 pub end: String,
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn round_trip<T>(value: &T) -> T
380 where
381 T: serde::Serialize + for<'de> serde::Deserialize<'de>,
382 {
383 let s = serde_json::to_string(value).expect("serialize");
384 serde_json::from_str(&s).expect("deserialize")
385 }
386
387 #[test]
388 fn no_args_round_trips() {
389 let s = serde_json::to_string(&NoArgs {}).expect("serialize");
390 assert_eq!(s, "{}");
391 let _back: NoArgs = serde_json::from_str(&s).expect("deserialize");
392 }
393
394 #[test]
395 fn ping_result_round_trips() {
396 let r = PingResult { pong: true, version: env!("CARGO_PKG_VERSION").to_string() };
397 assert_eq!(round_trip(&r), r);
398 }
399
400 #[test]
401 fn stats_result_round_trips() {
402 let r = StatsResult {
403 uptime_ms: 12_345,
404 graph_version_hash: "abcd".to_string(),
405 listeners: vec![ListenerStatus {
406 addr: "127.0.0.1:8080".to_string(),
407 bound: true,
408 in_flight_count: 3,
409 }],
410 flow_log_subscribers: 2,
411 tracing_log_subscribers: 1,
412 };
413 assert_eq!(round_trip(&r), r);
414 }
415
416 #[test]
417 fn stats_result_decodes_payload_without_subscriber_counts() {
418 let raw = r#"{"uptime_ms":1,"graph_version_hash":"00","listeners":[]}"#;
422 let r: StatsResult = serde_json::from_str(raw).expect("decode");
423 assert_eq!(r.flow_log_subscribers, 0);
424 assert_eq!(r.tracing_log_subscribers, 0);
425 }
426
427 #[test]
428 fn reload_result_swapped_round_trips() {
429 let r = ReloadResult::Swapped { hash: "ff".to_string() };
430 assert_eq!(round_trip(&r), r);
431 let value = serde_json::to_value(&r).expect("to_value");
433 assert_eq!(value["kind"], "swapped");
434 assert_eq!(value["hash"], "ff");
435 }
436
437 #[test]
438 fn reload_result_unchanged_round_trips() {
439 let r = ReloadResult::Unchanged { hash: "00".to_string() };
440 assert_eq!(round_trip(&r), r);
441 let value = serde_json::to_value(&r).expect("to_value");
442 assert_eq!(value["kind"], "unchanged");
443 }
444
445 #[test]
446 fn shutdown_result_round_trips() {
447 let r = ShutdownResult { draining: true };
448 assert_eq!(round_trip(&r), r);
449 }
450
451 #[test]
452 fn get_connections_result_round_trips() {
453 let r = GetConnectionsResult {
454 listeners: vec![
455 ListenerStatus { addr: "127.0.0.1:1".to_string(), bound: true, in_flight_count: 0 },
456 ListenerStatus { addr: "127.0.0.1:2".to_string(), bound: false, in_flight_count: 9 },
457 ],
458 connections: vec![ConnectionInfo {
459 conn_id: "00000000deadbeef".to_string(),
460 listener_addr: "127.0.0.1:1".to_string(),
461 remote: "203.0.113.7:54321".to_string(),
462 age_ms: 1234,
463 }],
464 };
465 assert_eq!(round_trip(&r), r);
466 }
467
468 #[test]
469 fn get_connections_result_deserialises_payload_without_connections() {
470 let raw = r#"{"listeners":[{"addr":"127.0.0.1:1","bound":true,"in_flight_count":0}]}"#;
474 let r: GetConnectionsResult = serde_json::from_str(raw).expect("decode");
475 assert_eq!(r.listeners.len(), 1);
476 assert!(r.connections.is_empty());
477 }
478
479 #[test]
480 fn compile_dry_run_args_round_trips() {
481 let a = CompileDryRunArgs { config_dir: "/etc/vaned-b".to_string() };
482 let s = serde_json::to_string(&a).expect("serialize");
483 let back: CompileDryRunArgs = serde_json::from_str(&s).expect("deserialize");
484 assert_eq!(back.config_dir, "/etc/vaned-b");
485 }
486
487 #[test]
488 fn get_pools_result_round_trips() {
489 let r = GetPoolsResult {
490 wasm: vec![WasmPoolEntry {
491 kind: "stateful".to_string(),
492 key: "/etc/vaned/plugins/edge.wasm".to_string(),
493 export: "l4-peek".to_string(),
494 capacity: 8,
495 available: 5,
496 in_use: 3,
497 total_allocations: 0,
498 failures: 0,
499 }],
500 cgi: Some(CgiPoolEntry {
501 cap: 100,
502 available: 99,
503 in_use: 1,
504 total_allocations: 0,
505 failures: 0,
506 }),
507 };
508 assert_eq!(round_trip(&r), r);
509 }
510
511 #[test]
512 fn get_pools_result_decodes_minimal_payload() {
513 let raw = r#"{"cgi": null}"#;
516 let r: GetPoolsResult = serde_json::from_str(raw).expect("decode");
517 assert!(r.wasm.is_empty());
518 assert!(r.cgi.is_none());
519 }
520
521 #[test]
522 fn get_upstreams_result_round_trips() {
523 let r = GetUpstreamsResult {
524 tcp: vec![TcpUpstreamEntry {
525 version: "auto".to_string(),
526 scheme: "https".to_string(),
527 root_ca: "system".to_string(),
528 verify_mode: "full".to_string(),
529 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
530 dns: "system".to_string(),
531 fingerprint_id: "abcdef0123456789".to_string(),
532 }],
533 quic: vec![QuicUpstreamEntry {
534 remote_addr: "127.0.0.1:443".to_string(),
535 sni: "example.com".to_string(),
536 alpn: vec!["h3".to_string()],
537 fingerprint_id: "fedcba9876543210".to_string(),
538 }],
539 };
540 assert_eq!(round_trip(&r), r);
541 }
542
543 #[test]
544 fn get_upstreams_result_decodes_payload_without_quic() {
545 let raw = r#"{"tcp": []}"#;
548 let r: GetUpstreamsResult = serde_json::from_str(raw).expect("decode");
549 assert!(r.tcp.is_empty());
550 assert!(r.quic.is_empty());
551 }
552}