1use crate::core_error::CoreError;
2use crate::model::{
3 Admin, Alarm, EntityId, HealthSummary, IpsecSa, MagicSiteToSiteVpnConfig,
4 RemoteAccessVpnServer, SiteToSiteVpn, SysInfo, SystemInfo, VpnClientConnection,
5 VpnClientProfile, VpnSetting, WireGuardPeer,
6};
7use crate::session::models::{ChannelAvailability, RogueAp};
8
9use super::Controller;
10use super::support::{convert_health_summaries, require_session};
11
12const VPN_SETTING_KEYS: &[&str] = &[
13 "teleport",
14 "magic_site_to_site_vpn",
15 "openvpn",
16 "peer_to_peer",
17];
18
19impl Controller {
20 pub async fn list_site_to_site_vpns(&self) -> Result<Vec<SiteToSiteVpn>, CoreError> {
21 let guard = self.inner.session_client.lock().await;
22 let session = require_session(guard.as_ref())?;
23 let raw = session.list_network_conf().await?;
24 Ok(raw
25 .iter()
26 .filter_map(site_to_site_vpn_from_raw)
27 .collect::<Vec<_>>())
28 }
29
30 pub async fn get_site_to_site_vpn(&self, id: &str) -> Result<SiteToSiteVpn, CoreError> {
31 self.list_site_to_site_vpns()
32 .await?
33 .into_iter()
34 .find(|vpn| matches!(&vpn.id, EntityId::Legacy(value) if value == id))
35 .ok_or_else(|| CoreError::NotFound {
36 entity_type: "site-to-site VPN".into(),
37 identifier: id.into(),
38 })
39 }
40
41 pub async fn list_remote_access_vpn_servers(
42 &self,
43 ) -> Result<Vec<RemoteAccessVpnServer>, CoreError> {
44 let guard = self.inner.session_client.lock().await;
45 let session = require_session(guard.as_ref())?;
46 let raw = session.list_network_conf().await?;
47 Ok(raw
48 .iter()
49 .filter_map(remote_access_vpn_server_from_raw)
50 .collect::<Vec<_>>())
51 }
52
53 pub async fn get_remote_access_vpn_server(
54 &self,
55 id: &str,
56 ) -> Result<RemoteAccessVpnServer, CoreError> {
57 self.list_remote_access_vpn_servers()
58 .await?
59 .into_iter()
60 .find(|server| matches!(&server.id, EntityId::Legacy(value) if value == id))
61 .ok_or_else(|| CoreError::NotFound {
62 entity_type: "remote-access VPN server".into(),
63 identifier: id.into(),
64 })
65 }
66
67 pub async fn list_wireguard_peers(
68 &self,
69 server_id: Option<&str>,
70 ) -> Result<Vec<WireGuardPeer>, CoreError> {
71 let guard = self.inner.session_client.lock().await;
72 let session = require_session(guard.as_ref())?;
73 let raw = match server_id {
74 Some(server_id) => session.list_wireguard_peers(server_id).await?,
75 None => session.list_all_wireguard_peers().await?,
76 };
77 let mut peers = raw
78 .iter()
79 .filter_map(|value| wireguard_peer_from_raw(value, server_id))
80 .collect::<Vec<_>>();
81 peers.sort_by(|left, right| {
82 left.name
83 .cmp(&right.name)
84 .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
85 });
86 Ok(peers)
87 }
88
89 pub async fn get_wireguard_peer(
90 &self,
91 server_id: &str,
92 id: &str,
93 ) -> Result<WireGuardPeer, CoreError> {
94 self.list_wireguard_peers(Some(server_id))
95 .await?
96 .into_iter()
97 .find(|peer| matches!(&peer.id, EntityId::Legacy(value) if value == id))
98 .ok_or_else(|| CoreError::NotFound {
99 entity_type: "WireGuard peer".into(),
100 identifier: id.into(),
101 })
102 }
103
104 pub async fn list_wireguard_peer_existing_subnets(&self) -> Result<Vec<String>, CoreError> {
105 let guard = self.inner.session_client.lock().await;
106 let session = require_session(guard.as_ref())?;
107 let raw = session.get_wireguard_peer_existing_subnets().await?;
108 Ok(raw
109 .get("subnets")
110 .and_then(serde_json::Value::as_array)
111 .map(|values| {
112 values
113 .iter()
114 .filter_map(serde_json::Value::as_str)
115 .map(str::to_owned)
116 .collect::<Vec<_>>()
117 })
118 .unwrap_or_default())
119 }
120
121 pub async fn list_openvpn_port_suggestions(&self) -> Result<Vec<u16>, CoreError> {
122 let guard = self.inner.session_client.lock().await;
123 let session = require_session(guard.as_ref())?;
124 let raw = session.get_openvpn_port_suggestions().await?;
125 Ok(raw
126 .get("available_ports")
127 .and_then(serde_json::Value::as_array)
128 .map(|values| {
129 values
130 .iter()
131 .filter_map(serde_json::Value::as_u64)
132 .filter_map(|value| u16::try_from(value).ok())
133 .collect::<Vec<_>>()
134 })
135 .unwrap_or_default())
136 }
137
138 pub async fn download_openvpn_configuration(&self, id: &str) -> Result<Vec<u8>, CoreError> {
139 let guard = self.inner.session_client.lock().await;
140 let session = require_session(guard.as_ref())?;
141 Ok(session.download_openvpn_configuration(id).await?)
142 }
143
144 pub async fn list_vpn_client_profiles(&self) -> Result<Vec<VpnClientProfile>, CoreError> {
145 let guard = self.inner.session_client.lock().await;
146 let session = require_session(guard.as_ref())?;
147 let mut clients = session
148 .list_network_conf()
149 .await?
150 .iter()
151 .filter_map(vpn_client_profile_from_raw)
152 .collect::<Vec<_>>();
153 clients.sort_by(|left, right| {
154 left.name
155 .cmp(&right.name)
156 .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
157 });
158 Ok(clients)
159 }
160
161 pub async fn get_vpn_client_profile(&self, id: &str) -> Result<VpnClientProfile, CoreError> {
162 self.list_vpn_client_profiles()
163 .await?
164 .into_iter()
165 .find(|client| matches!(&client.id, EntityId::Legacy(value) if value == id))
166 .ok_or_else(|| CoreError::NotFound {
167 entity_type: "VPN client profile".into(),
168 identifier: id.into(),
169 })
170 }
171
172 pub async fn list_vpn_client_connections(&self) -> Result<Vec<VpnClientConnection>, CoreError> {
173 let guard = self.inner.session_client.lock().await;
174 let session = require_session(guard.as_ref())?;
175 let mut connections = session
176 .list_vpn_client_connections()
177 .await?
178 .iter()
179 .filter_map(vpn_client_connection_from_raw)
180 .collect::<Vec<_>>();
181 connections.sort_by(|left, right| {
182 left.name
183 .cmp(&right.name)
184 .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
185 });
186 Ok(connections)
187 }
188
189 pub async fn get_vpn_client_connection(
190 &self,
191 id: &str,
192 ) -> Result<VpnClientConnection, CoreError> {
193 self.list_vpn_client_connections()
194 .await?
195 .into_iter()
196 .find(|connection| matches!(&connection.id, EntityId::Legacy(value) if value == id))
197 .ok_or_else(|| CoreError::NotFound {
198 entity_type: "VPN client connection".into(),
199 identifier: id.into(),
200 })
201 }
202
203 pub async fn list_magic_site_to_site_vpn_configs(
204 &self,
205 ) -> Result<Vec<MagicSiteToSiteVpnConfig>, CoreError> {
206 let guard = self.inner.session_client.lock().await;
207 let session = require_session(guard.as_ref())?;
208 let mut configs = session
209 .list_magic_site_to_site_vpn_configs()
210 .await?
211 .iter()
212 .filter_map(magic_site_to_site_vpn_config_from_raw)
213 .collect::<Vec<_>>();
214 configs.sort_by(|left, right| {
215 left.name
216 .cmp(&right.name)
217 .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
218 });
219 Ok(configs)
220 }
221
222 pub async fn get_magic_site_to_site_vpn_config(
223 &self,
224 id: &str,
225 ) -> Result<MagicSiteToSiteVpnConfig, CoreError> {
226 self.list_magic_site_to_site_vpn_configs()
227 .await?
228 .into_iter()
229 .find(|config| matches!(&config.id, EntityId::Legacy(value) if value == id))
230 .ok_or_else(|| CoreError::NotFound {
231 entity_type: "magic site-to-site VPN config".into(),
232 identifier: id.into(),
233 })
234 }
235
236 pub async fn list_backups(&self) -> Result<Vec<serde_json::Value>, CoreError> {
237 let guard = self.inner.session_client.lock().await;
238 let session = require_session(guard.as_ref())?;
239 Ok(session.list_backups().await?)
240 }
241
242 pub async fn download_backup(&self, filename: &str) -> Result<Vec<u8>, CoreError> {
243 let guard = self.inner.session_client.lock().await;
244 let session = require_session(guard.as_ref())?;
245 Ok(session.download_backup(filename).await?)
246 }
247
248 pub async fn get_site_stats(
249 &self,
250 interval: &str,
251 start: Option<i64>,
252 end: Option<i64>,
253 attrs: Option<&[String]>,
254 ) -> Result<Vec<serde_json::Value>, CoreError> {
255 let guard = self.inner.session_client.lock().await;
256 let session = require_session(guard.as_ref())?;
257 Ok(session.get_site_stats(interval, start, end, attrs).await?)
258 }
259
260 pub async fn get_device_stats(
261 &self,
262 interval: &str,
263 macs: Option<&[String]>,
264 attrs: Option<&[String]>,
265 ) -> Result<Vec<serde_json::Value>, CoreError> {
266 let guard = self.inner.session_client.lock().await;
267 let session = require_session(guard.as_ref())?;
268 Ok(session.get_device_stats(interval, macs, attrs).await?)
269 }
270
271 pub async fn get_client_stats(
272 &self,
273 interval: &str,
274 macs: Option<&[String]>,
275 attrs: Option<&[String]>,
276 ) -> Result<Vec<serde_json::Value>, CoreError> {
277 let guard = self.inner.session_client.lock().await;
278 let session = require_session(guard.as_ref())?;
279 Ok(session.get_client_stats(interval, macs, attrs).await?)
280 }
281
282 pub async fn get_gateway_stats(
283 &self,
284 interval: &str,
285 start: Option<i64>,
286 end: Option<i64>,
287 attrs: Option<&[String]>,
288 ) -> Result<Vec<serde_json::Value>, CoreError> {
289 let guard = self.inner.session_client.lock().await;
290 let session = require_session(guard.as_ref())?;
291 Ok(session
292 .get_gateway_stats(interval, start, end, attrs)
293 .await?)
294 }
295
296 pub async fn get_dpi_stats(
297 &self,
298 group_by: &str,
299 macs: Option<&[String]>,
300 ) -> Result<Vec<serde_json::Value>, CoreError> {
301 let guard = self.inner.session_client.lock().await;
302 let session = require_session(guard.as_ref())?;
303 Ok(session.get_dpi_stats(group_by, macs).await?)
304 }
305
306 pub async fn list_admins(&self) -> Result<Vec<Admin>, CoreError> {
307 let guard = self.inner.session_client.lock().await;
308 let session = require_session(guard.as_ref())?;
309 let raw = session.list_admins().await?;
310 Ok(raw
311 .into_iter()
312 .map(|value| Admin {
313 id: value
314 .get("_id")
315 .and_then(|value| value.as_str())
316 .map_or_else(
317 || EntityId::Legacy("unknown".into()),
318 |value| EntityId::Legacy(value.into()),
319 ),
320 name: value
321 .get("name")
322 .and_then(|value| value.as_str())
323 .unwrap_or("")
324 .to_owned(),
325 email: value
326 .get("email")
327 .and_then(|value| value.as_str())
328 .map(String::from),
329 role: value
330 .get("role")
331 .and_then(|value| value.as_str())
332 .unwrap_or("unknown")
333 .to_owned(),
334 is_super: value
335 .get("is_super")
336 .and_then(serde_json::Value::as_bool)
337 .unwrap_or(false),
338 last_login: None,
339 })
340 .collect())
341 }
342
343 pub async fn list_users(
344 &self,
345 ) -> Result<Vec<crate::session::models::SessionUserEntry>, CoreError> {
346 let guard = self.inner.session_client.lock().await;
347 let session = require_session(guard.as_ref())?;
348 Ok(session.list_users().await?)
349 }
350
351 pub async fn list_rogue_aps(
352 &self,
353 within_secs: Option<i64>,
354 ) -> Result<Vec<RogueAp>, CoreError> {
355 let guard = self.inner.session_client.lock().await;
356 let session = require_session(guard.as_ref())?;
357 Ok(session.list_rogue_aps(within_secs).await?)
358 }
359
360 pub async fn list_channels(&self) -> Result<Vec<ChannelAvailability>, CoreError> {
361 let guard = self.inner.session_client.lock().await;
362 let session = require_session(guard.as_ref())?;
363 Ok(session.list_channels().await?)
364 }
365
366 pub async fn get_client_roams(
367 &self,
368 mac: &str,
369 limit: Option<u32>,
370 ) -> Result<Vec<serde_json::Value>, CoreError> {
371 let guard = self.inner.session_client.lock().await;
372 let session = require_session(guard.as_ref())?;
373 Ok(session.get_client_roams(mac, limit).await?)
374 }
375
376 pub async fn get_client_wifi_experience(
377 &self,
378 client_ip: &str,
379 ) -> Result<serde_json::Value, CoreError> {
380 let guard = self.inner.session_client.lock().await;
381 let session = require_session(guard.as_ref())?;
382 Ok(session.get_client_wifi_experience(client_ip).await?)
383 }
384
385 pub async fn is_dpi_enabled(&self) -> Result<bool, CoreError> {
386 let guard = self.inner.session_client.lock().await;
387 let session = require_session(guard.as_ref())?;
388 let settings = session.get_site_settings().await?;
389 let enabled = settings
390 .iter()
391 .find(|s| s.get("key").and_then(|v| v.as_str()) == Some("dpi"))
392 .and_then(|s| s.get("enabled"))
393 .and_then(serde_json::Value::as_bool)
394 .unwrap_or(false);
395 Ok(enabled)
396 }
397
398 pub async fn list_alarms(&self) -> Result<Vec<Alarm>, CoreError> {
399 let guard = self.inner.session_client.lock().await;
400 let session = require_session(guard.as_ref())?;
401 let raw = session.list_alarms().await?;
402 Ok(raw.into_iter().map(Alarm::from).collect())
403 }
404
405 pub async fn get_system_info(&self) -> Result<SystemInfo, CoreError> {
406 {
407 let guard = self.inner.integration_client.lock().await;
408 if let Some(ic) = guard.as_ref() {
409 let info = ic.get_info().await?;
410 let fields = &info.fields;
411 return Ok(SystemInfo {
412 controller_name: fields
413 .get("applicationName")
414 .or_else(|| fields.get("name"))
415 .and_then(|value| value.as_str())
416 .map(String::from),
417 version: fields
418 .get("applicationVersion")
419 .or_else(|| fields.get("version"))
420 .and_then(|value| value.as_str())
421 .unwrap_or("unknown")
422 .to_owned(),
423 build: fields
424 .get("build")
425 .and_then(|value| value.as_str())
426 .map(String::from),
427 hostname: fields
428 .get("hostname")
429 .and_then(|value| value.as_str())
430 .map(String::from),
431 ip: None,
432 uptime_secs: fields.get("uptime").and_then(serde_json::Value::as_u64),
433 update_available: fields
434 .get("isUpdateAvailable")
435 .or_else(|| fields.get("update_available"))
436 .and_then(serde_json::Value::as_bool),
437 });
438 }
439 }
440
441 let guard = self.inner.session_client.lock().await;
442 let session = require_session(guard.as_ref())?;
443 let raw = session.get_sysinfo().await?;
444 Ok(SystemInfo {
445 controller_name: raw
446 .get("controller_name")
447 .or_else(|| raw.get("name"))
448 .and_then(|value| value.as_str())
449 .map(String::from),
450 version: raw
451 .get("version")
452 .and_then(|value| value.as_str())
453 .unwrap_or("unknown")
454 .to_owned(),
455 build: raw
456 .get("build")
457 .and_then(|value| value.as_str())
458 .map(String::from),
459 hostname: raw
460 .get("hostname")
461 .and_then(|value| value.as_str())
462 .map(String::from),
463 ip: raw
464 .get("ip_addrs")
465 .and_then(|value| value.as_array())
466 .and_then(|values| values.first())
467 .and_then(|value| value.as_str())
468 .and_then(|value| value.parse().ok()),
469 uptime_secs: raw.get("uptime").and_then(serde_json::Value::as_u64),
470 update_available: raw
471 .get("update_available")
472 .and_then(serde_json::Value::as_bool),
473 })
474 }
475
476 pub async fn get_site_health(&self) -> Result<Vec<HealthSummary>, CoreError> {
477 let guard = self.inner.session_client.lock().await;
478 let session = require_session(guard.as_ref())?;
479 let raw = session.get_health().await?;
480 Ok(convert_health_summaries(raw))
481 }
482
483 pub async fn list_ipsec_sa(&self) -> Result<Vec<IpsecSa>, CoreError> {
484 let guard = self.inner.session_client.lock().await;
485 let session = require_session(guard.as_ref())?;
486 Ok(session.list_ipsec_sa().await?)
487 }
488
489 pub fn get_vpn_health(&self) -> Option<HealthSummary> {
490 self.inner
491 .store
492 .site_health_snapshot()
493 .iter()
494 .find(|summary| summary.subsystem.eq_ignore_ascii_case("vpn"))
495 .cloned()
496 }
497
498 pub async fn get_sysinfo(&self) -> Result<SysInfo, CoreError> {
499 let guard = self.inner.session_client.lock().await;
500 let session = require_session(guard.as_ref())?;
501 let raw = session.get_sysinfo().await?;
502 Ok(SysInfo {
503 timezone: raw
504 .get("timezone")
505 .and_then(|value| value.as_str())
506 .map(String::from),
507 autobackup: raw.get("autobackup").and_then(serde_json::Value::as_bool),
508 hostname: raw
509 .get("hostname")
510 .and_then(|value| value.as_str())
511 .map(String::from),
512 ip_addrs: raw
513 .get("ip_addrs")
514 .and_then(|value| value.as_array())
515 .map(|values| {
516 values
517 .iter()
518 .filter_map(|value| value.as_str().map(String::from))
519 .collect()
520 })
521 .unwrap_or_default(),
522 live_chat: raw
523 .get("live_chat")
524 .and_then(|value| value.as_str())
525 .map(String::from),
526 #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
527 data_retention_days: raw
528 .get("data_retention_days")
529 .and_then(serde_json::Value::as_u64)
530 .map(|value| value as u32),
531 extra: raw,
532 })
533 }
534
535 pub async fn list_vpn_settings(&self) -> Result<Vec<VpnSetting>, CoreError> {
536 let guard = self.inner.session_client.lock().await;
537 let session = require_session(guard.as_ref())?;
538 let raw = session.get_site_settings().await?;
539 let mut settings = raw
540 .iter()
541 .filter_map(vpn_setting_from_raw)
542 .collect::<Vec<_>>();
543 settings.sort_by(|left, right| left.key.cmp(&right.key));
544 Ok(settings)
545 }
546
547 pub async fn get_vpn_setting(&self, key: &str) -> Result<VpnSetting, CoreError> {
548 self.list_vpn_settings()
549 .await?
550 .into_iter()
551 .find(|setting| setting.key == key)
552 .ok_or_else(|| CoreError::NotFound {
553 entity_type: "vpn setting".into(),
554 identifier: key.into(),
555 })
556 }
557
558 pub async fn update_vpn_setting(
559 &self,
560 key: &str,
561 body: &serde_json::Value,
562 ) -> Result<VpnSetting, CoreError> {
563 if !VPN_SETTING_KEYS.contains(&key) {
564 return Err(CoreError::NotFound {
565 entity_type: "vpn setting".into(),
566 identifier: key.into(),
567 });
568 }
569
570 let guard = self.inner.session_client.lock().await;
571 let session = require_session(guard.as_ref())?;
572 session.set_site_setting(key, body).await?;
573 drop(guard);
574
575 self.get_vpn_setting(key).await
576 }
577
578 pub async fn get_all_site_settings(&self) -> Result<Vec<serde_json::Value>, CoreError> {
579 let guard = self.inner.session_client.lock().await;
580 let session = require_session(guard.as_ref())?;
581 Ok(session.get_site_settings().await?)
582 }
583
584 pub async fn get_site_setting(&self, key: &str) -> Result<serde_json::Value, CoreError> {
585 self.get_all_site_settings()
586 .await?
587 .into_iter()
588 .find(|s| s.get("key").and_then(|v| v.as_str()) == Some(key))
589 .ok_or_else(|| CoreError::NotFound {
590 entity_type: "setting".into(),
591 identifier: key.into(),
592 })
593 }
594
595 pub async fn update_site_setting(
596 &self,
597 key: &str,
598 body: &serde_json::Value,
599 ) -> Result<(), CoreError> {
600 let guard = self.inner.session_client.lock().await;
601 let session = require_session(guard.as_ref())?;
602 session.set_site_setting(key, body).await?;
603 Ok(())
604 }
605
606 pub async fn raw_get(&self, path: &str) -> Result<serde_json::Value, CoreError> {
612 let guard = self.inner.session_client.lock().await;
613 let session = require_session(guard.as_ref())?;
614 Ok(session.raw_get(path).await?)
615 }
616
617 pub async fn raw_post(
619 &self,
620 path: &str,
621 body: &serde_json::Value,
622 ) -> Result<serde_json::Value, CoreError> {
623 let guard = self.inner.session_client.lock().await;
624 let session = require_session(guard.as_ref())?;
625 Ok(session.raw_post(path, body).await?)
626 }
627
628 pub async fn raw_put(
630 &self,
631 path: &str,
632 body: &serde_json::Value,
633 ) -> Result<serde_json::Value, CoreError> {
634 let guard = self.inner.session_client.lock().await;
635 let session = require_session(guard.as_ref())?;
636 Ok(session.raw_put(path, body).await?)
637 }
638
639 pub async fn raw_patch(
641 &self,
642 path: &str,
643 body: &serde_json::Value,
644 ) -> Result<serde_json::Value, CoreError> {
645 let guard = self.inner.session_client.lock().await;
646 let session = require_session(guard.as_ref())?;
647 Ok(session.raw_patch(path, body).await?)
648 }
649
650 pub async fn raw_delete(&self, path: &str) -> Result<(), CoreError> {
652 let guard = self.inner.session_client.lock().await;
653 let session = require_session(guard.as_ref())?;
654 session.raw_delete(path).await?;
655 Ok(())
656 }
657}
658
659fn site_to_site_vpn_from_raw(raw: &serde_json::Value) -> Option<SiteToSiteVpn> {
660 let fields = raw.as_object()?;
661 if fields.get("purpose")?.as_str()? != "site-vpn" {
662 return None;
663 }
664
665 let id = fields.get("_id")?.as_str()?.to_owned();
666 let name = fields
667 .get("name")
668 .and_then(serde_json::Value::as_str)
669 .unwrap_or_default()
670 .to_owned();
671
672 let vpn_type = fields
673 .get("vpn_type")
674 .and_then(serde_json::Value::as_str)
675 .unwrap_or("unknown")
676 .to_owned();
677
678 let remote_host = fields
679 .get("ipsec_peer_ip")
680 .or_else(|| fields.get("openvpn_remote_host"))
681 .and_then(serde_json::Value::as_str)
682 .map(str::to_owned);
683
684 let local_ip = fields
685 .get("ipsec_local_ip")
686 .or_else(|| fields.get("openvpn_local_address"))
687 .and_then(serde_json::Value::as_str)
688 .map(str::to_owned);
689
690 let interface = fields
691 .get("ipsec_interface")
692 .and_then(serde_json::Value::as_str)
693 .map(str::to_owned);
694
695 let remote_site_id = fields
696 .get("remote_site_id")
697 .and_then(serde_json::Value::as_str)
698 .filter(|value| !value.is_empty())
699 .map(str::to_owned);
700
701 let remote_vpn_subnets = fields
702 .get("remote_vpn_subnets")
703 .and_then(serde_json::Value::as_array)
704 .map(|values| {
705 values
706 .iter()
707 .filter_map(serde_json::Value::as_str)
708 .map(str::to_owned)
709 .collect::<Vec<_>>()
710 })
711 .unwrap_or_default();
712
713 Some(SiteToSiteVpn {
714 id: EntityId::Legacy(id),
715 name,
716 enabled: fields
717 .get("enabled")
718 .and_then(serde_json::Value::as_bool)
719 .unwrap_or(true),
720 vpn_type,
721 remote_site_id,
722 local_ip,
723 interface,
724 remote_host,
725 remote_vpn_subnets,
726 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
727 .as_object()
728 .cloned()
729 .unwrap_or_default(),
730 })
731}
732
733fn remote_access_vpn_server_from_raw(raw: &serde_json::Value) -> Option<RemoteAccessVpnServer> {
734 let fields = raw.as_object()?;
735 if fields.get("purpose")?.as_str()? != "remote-user-vpn" {
736 return None;
737 }
738
739 let id = fields.get("_id")?.as_str()?.to_owned();
740 let vpn_type = fields
741 .get("vpn_type")
742 .and_then(serde_json::Value::as_str)
743 .unwrap_or("unknown")
744 .to_owned();
745
746 Some(RemoteAccessVpnServer {
747 id: EntityId::Legacy(id),
748 name: fields
749 .get("name")
750 .and_then(serde_json::Value::as_str)
751 .unwrap_or_default()
752 .to_owned(),
753 enabled: fields
754 .get("enabled")
755 .and_then(serde_json::Value::as_bool)
756 .unwrap_or(true),
757 vpn_type,
758 local_port: fields
759 .get("local_port")
760 .and_then(serde_json::Value::as_u64)
761 .and_then(|value| u16::try_from(value).ok()),
762 local_wan_ip: first_non_empty_string(
763 fields,
764 &[
765 "wireguard_local_wan_ip",
766 "openvpn_local_wan_ip",
767 "l2tp_local_wan_ip",
768 ],
769 ),
770 interface: first_non_empty_string(
771 fields,
772 &["wireguard_interface", "openvpn_interface", "l2tp_interface"],
773 ),
774 gateway_subnet: first_non_empty_string(fields, &["ip_subnet", "ipv6_subnet"]),
775 radius_profile_id: first_non_empty_string(fields, &["radiusprofile_id"]),
776 exposed_to_site_vpn: fields
777 .get("exposed_to_site_vpn")
778 .and_then(serde_json::Value::as_bool),
779 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
780 .as_object()
781 .cloned()
782 .unwrap_or_default(),
783 })
784}
785
786fn wireguard_peer_from_raw(
787 raw: &serde_json::Value,
788 default_server_id: Option<&str>,
789) -> Option<WireGuardPeer> {
790 let fields = raw.as_object()?;
791 let id = fields.get("_id")?.as_str()?.to_owned();
792 let server_id = first_non_empty_string(fields, &["networkId", "network_id"])
793 .or_else(|| default_server_id.map(str::to_owned))
794 .map(EntityId::Legacy);
795 let allowed_ips = fields
796 .get("allowed_ips")
797 .and_then(serde_json::Value::as_array)
798 .map(|values| {
799 values
800 .iter()
801 .filter_map(serde_json::Value::as_str)
802 .map(str::to_owned)
803 .collect::<Vec<_>>()
804 })
805 .unwrap_or_default();
806
807 Some(WireGuardPeer {
808 id: EntityId::Legacy(id),
809 server_id,
810 name: fields
811 .get("name")
812 .and_then(serde_json::Value::as_str)
813 .unwrap_or_default()
814 .to_owned(),
815 interface_ip: first_non_empty_string(fields, &["interface_ip"]),
816 interface_ipv6: first_non_empty_string(fields, &["interface_ipv6"]),
817 public_key: first_non_empty_string(fields, &["public_key"]),
818 allowed_ips,
819 has_private_key: fields
820 .get("private_key")
821 .and_then(serde_json::Value::as_str)
822 .is_some_and(|value| !value.is_empty()),
823 has_preshared_key: fields
824 .get("preshared_key")
825 .and_then(serde_json::Value::as_str)
826 .is_some_and(|value| !value.is_empty()),
827 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
828 .as_object()
829 .cloned()
830 .unwrap_or_default(),
831 })
832}
833
834fn vpn_client_connection_from_raw(raw: &serde_json::Value) -> Option<VpnClientConnection> {
835 let fields = raw.as_object()?;
836 let id = first_non_empty_string(fields, &["network_id", "networkId", "id", "_id"])?;
837
838 Some(VpnClientConnection {
839 id: EntityId::Legacy(id),
840 name: first_non_empty_string(
841 fields,
842 &[
843 "name",
844 "display_name",
845 "network_name",
846 "server_name",
847 "remote_name",
848 ],
849 ),
850 connection_type: first_non_empty_string(fields, &["type", "vpn_type", "connection_type"]),
851 status: first_non_empty_string(fields, &["status", "state"]),
852 local_address: first_non_empty_string(
853 fields,
854 &["local_ip", "local_address", "localAddress"],
855 ),
856 remote_address: first_non_empty_string(
857 fields,
858 &[
859 "remote_ip",
860 "remote_address",
861 "remoteAddress",
862 "server_ip",
863 "serverAddress",
864 "remote_host",
865 ],
866 ),
867 username: first_non_empty_string(fields, &["username", "user"]),
868 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
869 .as_object()
870 .cloned()
871 .unwrap_or_default(),
872 })
873}
874
875fn vpn_client_profile_from_raw(raw: &serde_json::Value) -> Option<VpnClientProfile> {
876 let fields = raw.as_object()?;
877 let vpn_type = first_non_empty_string(fields, &["vpn_type", "type"]).unwrap_or_default();
878
879 match fields.get("purpose").and_then(serde_json::Value::as_str) {
880 Some("vpn-client") => {}
881 Some(_) => return None,
882 None if !vpn_type.ends_with("-client") => return None,
883 None => {}
884 }
885
886 let id = first_non_empty_string(fields, &["_id", "id"])?;
887
888 Some(VpnClientProfile {
889 id: EntityId::Legacy(id),
890 name: fields
891 .get("name")
892 .and_then(serde_json::Value::as_str)
893 .unwrap_or_default()
894 .to_owned(),
895 enabled: fields
896 .get("enabled")
897 .and_then(serde_json::Value::as_bool)
898 .unwrap_or(true),
899 vpn_type,
900 server_address: first_non_empty_string(
901 fields,
902 &[
903 "remote_host",
904 "remote_address",
905 "server_address",
906 "server_ip",
907 "server",
908 "host",
909 "peer_ip",
910 ],
911 ),
912 server_port: ["remote_port", "server_port", "port"]
913 .iter()
914 .find_map(|key| {
915 fields
916 .get(*key)
917 .and_then(serde_json::Value::as_u64)
918 .and_then(|value| u16::try_from(value).ok())
919 }),
920 local_address: first_non_empty_string(
921 fields,
922 &[
923 "local_address",
924 "local_ip",
925 "interface_ip",
926 "vpn_ip",
927 "openvpn_local_address",
928 "wireguard_local_address",
929 ],
930 ),
931 username: first_non_empty_string(fields, &["username", "user"]),
932 interface: first_non_empty_string(
933 fields,
934 &[
935 "interface",
936 "wan_interface",
937 "wan_network",
938 "bind_interface",
939 "openvpn_interface",
940 "wireguard_interface",
941 ],
942 ),
943 route_distance: fields
944 .get("route_distance")
945 .and_then(serde_json::Value::as_u64)
946 .and_then(|value| u32::try_from(value).ok()),
947 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
948 .as_object()
949 .cloned()
950 .unwrap_or_default(),
951 })
952}
953
954fn magic_site_to_site_vpn_config_from_raw(
955 raw: &serde_json::Value,
956) -> Option<MagicSiteToSiteVpnConfig> {
957 let fields = raw.as_object()?;
958 let id = first_non_empty_string(fields, &["id", "_id"])?;
959
960 Some(MagicSiteToSiteVpnConfig {
961 id: EntityId::Legacy(id),
962 name: first_non_empty_string(fields, &["name", "display_name"]),
963 status: first_non_empty_string(fields, &["status", "state"]),
964 enabled: fields.get("enabled").and_then(serde_json::Value::as_bool),
965 local_site_name: first_non_empty_string(
966 fields,
967 &["localSiteName", "local_site_name", "local_site"],
968 ),
969 remote_site_name: first_non_empty_string(
970 fields,
971 &["remoteSiteName", "remote_site_name", "remote_site"],
972 ),
973 fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
974 .as_object()
975 .cloned()
976 .unwrap_or_default(),
977 })
978}
979
980fn first_non_empty_string(
981 fields: &serde_json::Map<String, serde_json::Value>,
982 keys: &[&str],
983) -> Option<String> {
984 keys.iter().find_map(|key| {
985 fields
986 .get(*key)
987 .and_then(serde_json::Value::as_str)
988 .filter(|value| !value.is_empty())
989 .map(str::to_owned)
990 })
991}
992
993fn vpn_setting_from_raw(raw: &serde_json::Value) -> Option<VpnSetting> {
994 let object = raw.as_object()?;
995 let key = object.get("key")?.as_str()?;
996 if !VPN_SETTING_KEYS.contains(&key) {
997 return None;
998 }
999
1000 let mut fields = object.clone();
1001 fields.remove("_id");
1002 fields.remove("key");
1003 fields.remove("site_id");
1004 let fields = redact_sensitive_value(&serde_json::Value::Object(fields))
1005 .as_object()
1006 .cloned()
1007 .unwrap_or_default();
1008
1009 Some(VpnSetting {
1010 key: key.to_owned(),
1011 enabled: fields.get("enabled").and_then(serde_json::Value::as_bool),
1012 fields,
1013 })
1014}
1015
1016fn redact_sensitive_value(value: &serde_json::Value) -> serde_json::Value {
1017 match value {
1018 serde_json::Value::Object(map) => serde_json::Value::Object(
1019 map.iter()
1020 .map(|(key, value)| {
1021 (
1022 key.clone(),
1023 if should_redact_field(key) {
1024 serde_json::Value::String("***REDACTED***".into())
1025 } else {
1026 redact_sensitive_value(value)
1027 },
1028 )
1029 })
1030 .collect(),
1031 ),
1032 serde_json::Value::Array(values) => {
1033 serde_json::Value::Array(values.iter().map(redact_sensitive_value).collect())
1034 }
1035 _ => value.clone(),
1036 }
1037}
1038
1039fn should_redact_field(key: &str) -> bool {
1040 let key = key.to_ascii_lowercase();
1041 [
1042 "private",
1043 "password",
1044 "secret",
1045 "token",
1046 "psk",
1047 "shared_key",
1048 "certificate",
1049 "dh_key",
1050 ]
1051 .into_iter()
1052 .any(|needle| key.contains(needle))
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use std::sync::Arc;
1058
1059 use super::{
1060 Controller, magic_site_to_site_vpn_config_from_raw, redact_sensitive_value,
1061 remote_access_vpn_server_from_raw, site_to_site_vpn_from_raw,
1062 vpn_client_connection_from_raw, vpn_client_profile_from_raw, vpn_setting_from_raw,
1063 wireguard_peer_from_raw,
1064 };
1065 use crate::config::ControllerConfig;
1066 use crate::model::{EntityId, HealthSummary};
1067
1068 #[test]
1069 fn get_vpn_health_reads_cached_store_snapshot() {
1070 let controller = Controller::new(ControllerConfig::default());
1071 let summaries = Arc::new(vec![
1072 HealthSummary {
1073 subsystem: "wan".into(),
1074 status: "ok".into(),
1075 num_adopted: Some(1),
1076 num_sta: Some(5),
1077 tx_bytes_r: Some(100),
1078 rx_bytes_r: Some(200),
1079 latency: Some(1.0),
1080 wan_ip: Some("198.51.100.1".into()),
1081 gateways: Some(vec!["gw".into()]),
1082 extra: serde_json::Value::Null,
1083 },
1084 HealthSummary {
1085 subsystem: "vpn".into(),
1086 status: "warn".into(),
1087 num_adopted: None,
1088 num_sta: Some(2),
1089 tx_bytes_r: Some(300),
1090 rx_bytes_r: Some(400),
1091 latency: Some(2.5),
1092 wan_ip: None,
1093 gateways: None,
1094 extra: serde_json::json!({ "source": "cache" }),
1095 },
1096 ]);
1097 let _previous = controller.inner.store.site_health.send_replace(summaries);
1098
1099 let vpn_health = controller.get_vpn_health().expect("vpn summary");
1100
1101 assert_eq!(vpn_health.subsystem, "vpn");
1102 assert_eq!(vpn_health.status, "warn");
1103 assert_eq!(vpn_health.num_sta, Some(2));
1104 assert_eq!(vpn_health.extra["source"], "cache");
1105 }
1106
1107 #[test]
1108 fn site_to_site_vpn_from_raw_maps_legacy_networkconf_record() {
1109 let raw = serde_json::json!({
1110 "_id": "vpn123",
1111 "name": "Branch Tunnel",
1112 "purpose": "site-vpn",
1113 "enabled": true,
1114 "vpn_type": "ipsec-vpn",
1115 "ipsec_local_ip": "10.0.0.1",
1116 "ipsec_interface": "WAN1",
1117 "ipsec_peer_ip": "198.51.100.10",
1118 "remote_vpn_subnets": ["10.10.0.0/24"]
1119 });
1120
1121 let vpn = site_to_site_vpn_from_raw(&raw).expect("site-vpn should map");
1122
1123 assert_eq!(vpn.id, EntityId::Legacy("vpn123".into()));
1124 assert_eq!(vpn.name, "Branch Tunnel");
1125 assert_eq!(vpn.vpn_type, "ipsec-vpn");
1126 assert_eq!(vpn.local_ip.as_deref(), Some("10.0.0.1"));
1127 assert_eq!(vpn.interface.as_deref(), Some("WAN1"));
1128 assert_eq!(vpn.remote_host.as_deref(), Some("198.51.100.10"));
1129 assert_eq!(vpn.remote_vpn_subnets, vec!["10.10.0.0/24"]);
1130 }
1131
1132 #[test]
1133 fn vpn_setting_from_raw_filters_to_known_keys() {
1134 let raw = serde_json::json!({
1135 "key": "teleport",
1136 "enabled": true,
1137 "_id": "abc123",
1138 "site_id": "default",
1139 });
1140 let setting = vpn_setting_from_raw(&raw).expect("teleport should be recognized");
1141
1142 assert_eq!(setting.key, "teleport");
1143 assert_eq!(setting.enabled, Some(true));
1144 assert!(!setting.fields.contains_key("_id"));
1145 assert!(!setting.fields.contains_key("site_id"));
1146 }
1147
1148 #[test]
1149 fn remote_access_vpn_server_from_raw_maps_legacy_networkconf_record() {
1150 let raw = serde_json::json!({
1151 "_id": "vpn456",
1152 "name": "WireGuard Remote Access",
1153 "purpose": "remote-user-vpn",
1154 "enabled": true,
1155 "vpn_type": "wireguard",
1156 "local_port": 51820,
1157 "wireguard_local_wan_ip": "203.0.113.10",
1158 "wireguard_interface": "WAN1",
1159 "ip_subnet": "192.168.42.1/24",
1160 "radiusprofile_id": "radius123",
1161 "exposed_to_site_vpn": true,
1162 "x_wireguard_private_key": "secret"
1163 });
1164
1165 let server = remote_access_vpn_server_from_raw(&raw).expect("remote-user-vpn should map");
1166
1167 assert_eq!(server.id, EntityId::Legacy("vpn456".into()));
1168 assert_eq!(server.name, "WireGuard Remote Access");
1169 assert_eq!(server.vpn_type, "wireguard");
1170 assert_eq!(server.local_port, Some(51820));
1171 assert_eq!(server.local_wan_ip.as_deref(), Some("203.0.113.10"));
1172 assert_eq!(server.interface.as_deref(), Some("WAN1"));
1173 assert_eq!(server.gateway_subnet.as_deref(), Some("192.168.42.1/24"));
1174 assert_eq!(server.radius_profile_id.as_deref(), Some("radius123"));
1175 assert_eq!(server.exposed_to_site_vpn, Some(true));
1176 assert_eq!(
1177 server.fields["x_wireguard_private_key"].as_str(),
1178 Some("***REDACTED***")
1179 );
1180 }
1181
1182 #[test]
1183 fn wireguard_peer_from_raw_maps_legacy_v2_record() {
1184 let raw = serde_json::json!({
1185 "_id": "peer789",
1186 "networkId": "server123",
1187 "name": "Laptop",
1188 "interface_ip": "192.168.42.2",
1189 "interface_ipv6": "fd00::2",
1190 "public_key": "pubkey",
1191 "private_key": "secret",
1192 "preshared_key": "psk",
1193 "allowed_ips": ["10.0.0.0/24"]
1194 });
1195
1196 let peer = wireguard_peer_from_raw(&raw, None).expect("WireGuard peer should map");
1197
1198 assert_eq!(peer.id, EntityId::Legacy("peer789".into()));
1199 assert_eq!(peer.server_id, Some(EntityId::Legacy("server123".into())));
1200 assert_eq!(peer.name, "Laptop");
1201 assert_eq!(peer.interface_ip.as_deref(), Some("192.168.42.2"));
1202 assert_eq!(peer.interface_ipv6.as_deref(), Some("fd00::2"));
1203 assert_eq!(peer.public_key.as_deref(), Some("pubkey"));
1204 assert_eq!(peer.allowed_ips, vec!["10.0.0.0/24"]);
1205 assert!(peer.has_private_key);
1206 assert!(peer.has_preshared_key);
1207 assert_eq!(peer.fields["private_key"].as_str(), Some("***REDACTED***"));
1208 assert_eq!(
1209 peer.fields["preshared_key"].as_str(),
1210 Some("***REDACTED***")
1211 );
1212 }
1213
1214 #[test]
1215 fn vpn_client_connection_from_raw_maps_v2_record() {
1216 let raw = serde_json::json!({
1217 "network_id": "vpn-client-1",
1218 "name": "Branch Client",
1219 "type": "openvpn-client",
1220 "status": "CONNECTED",
1221 "local_ip": "10.0.0.2",
1222 "remote_ip": "198.51.100.10",
1223 "username": "branch-user",
1224 "password": "secret"
1225 });
1226
1227 let connection = vpn_client_connection_from_raw(&raw).expect("VPN connection should map");
1228
1229 assert_eq!(connection.id, EntityId::Legacy("vpn-client-1".into()));
1230 assert_eq!(connection.name.as_deref(), Some("Branch Client"));
1231 assert_eq!(
1232 connection.connection_type.as_deref(),
1233 Some("openvpn-client")
1234 );
1235 assert_eq!(connection.status.as_deref(), Some("CONNECTED"));
1236 assert_eq!(connection.local_address.as_deref(), Some("10.0.0.2"));
1237 assert_eq!(connection.remote_address.as_deref(), Some("198.51.100.10"));
1238 assert_eq!(connection.username.as_deref(), Some("branch-user"));
1239 assert_eq!(
1240 connection.fields["password"].as_str(),
1241 Some("***REDACTED***")
1242 );
1243 }
1244
1245 #[test]
1246 fn vpn_client_profile_from_raw_maps_legacy_networkconf_record() {
1247 let raw = serde_json::json!({
1248 "_id": "vpn-client-1",
1249 "name": "Branch Client",
1250 "purpose": "vpn-client",
1251 "enabled": false,
1252 "vpn_type": "openvpn-client",
1253 "remote_host": "198.51.100.10",
1254 "remote_port": 1194,
1255 "local_address": "10.0.0.2",
1256 "username": "branch-user",
1257 "interface": "WAN1",
1258 "route_distance": 15,
1259 "password": "secret"
1260 });
1261
1262 let client = vpn_client_profile_from_raw(&raw).expect("vpn-client should map");
1263
1264 assert_eq!(client.id, EntityId::Legacy("vpn-client-1".into()));
1265 assert_eq!(client.name, "Branch Client");
1266 assert!(!client.enabled);
1267 assert_eq!(client.vpn_type, "openvpn-client");
1268 assert_eq!(client.server_address.as_deref(), Some("198.51.100.10"));
1269 assert_eq!(client.server_port, Some(1194));
1270 assert_eq!(client.local_address.as_deref(), Some("10.0.0.2"));
1271 assert_eq!(client.username.as_deref(), Some("branch-user"));
1272 assert_eq!(client.interface.as_deref(), Some("WAN1"));
1273 assert_eq!(client.route_distance, Some(15));
1274 assert_eq!(client.fields["password"].as_str(), Some("***REDACTED***"));
1275 }
1276
1277 #[test]
1278 fn magic_site_to_site_vpn_config_from_raw_maps_v2_record() {
1279 let raw = serde_json::json!({
1280 "id": "magic-1",
1281 "name": "HQ <-> Branch",
1282 "status": "CONNECTED",
1283 "enabled": true,
1284 "localSiteName": "HQ",
1285 "remoteSiteName": "Branch",
1286 "token": "secret"
1287 });
1288
1289 let config =
1290 magic_site_to_site_vpn_config_from_raw(&raw).expect("magic site-to-site should map");
1291
1292 assert_eq!(config.id, EntityId::Legacy("magic-1".into()));
1293 assert_eq!(config.name.as_deref(), Some("HQ <-> Branch"));
1294 assert_eq!(config.status.as_deref(), Some("CONNECTED"));
1295 assert_eq!(config.enabled, Some(true));
1296 assert_eq!(config.local_site_name.as_deref(), Some("HQ"));
1297 assert_eq!(config.remote_site_name.as_deref(), Some("Branch"));
1298 assert_eq!(config.fields["token"].as_str(), Some("***REDACTED***"));
1299 }
1300
1301 #[test]
1302 fn redact_sensitive_value_masks_nested_vpn_secrets() {
1303 let redacted = redact_sensitive_value(&serde_json::json!({
1304 "enabled": true,
1305 "public_key": "keep-me",
1306 "x_private_key": "secret",
1307 "nested": {
1308 "psk": "hide-me",
1309 "certificatePem": "hide-me-too"
1310 }
1311 }));
1312
1313 assert_eq!(
1314 redacted.get("enabled").and_then(serde_json::Value::as_bool),
1315 Some(true)
1316 );
1317 assert_eq!(
1318 redacted
1319 .get("public_key")
1320 .and_then(serde_json::Value::as_str),
1321 Some("keep-me")
1322 );
1323 assert_eq!(
1324 redacted
1325 .get("x_private_key")
1326 .and_then(serde_json::Value::as_str),
1327 Some("***REDACTED***")
1328 );
1329 assert_eq!(redacted["nested"]["psk"].as_str(), Some("***REDACTED***"));
1330 assert_eq!(
1331 redacted["nested"]["certificatePem"].as_str(),
1332 Some("***REDACTED***")
1333 );
1334 }
1335}