1use std::fmt;
2
3use crate::auth::AuthType;
4use crate::client_event::ClientEvent;
5use crate::log_level::LogLevel;
6use crate::openvpn_state::OpenVpnState;
7use crate::redacted::Redacted;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum PasswordNotification {
13 NeedAuth {
15 auth_type: AuthType,
17 },
18
19 NeedPassword {
21 auth_type: AuthType,
23 },
24
25 VerificationFailed {
27 auth_type: AuthType,
29 },
30
31 StaticChallenge {
34 echo: bool,
36 response_concat: bool,
40 challenge: String,
42 },
43
44 AuthToken {
52 token: Redacted,
54 },
55
56 DynamicChallenge {
59 flags: String,
61 state_id: String,
63 username_b64: String,
66 challenge: String,
68 },
69}
70
71const SENSITIVE_ENV_KEYS: &[&str] = &["password"];
74
75#[derive(Clone, PartialEq, Eq)]
81pub enum Notification {
82 Client {
86 event: ClientEvent,
88 cid: u64,
90 kid: Option<u64>,
92 env: Vec<(String, String)>,
96 },
97
98 ClientAddress {
100 cid: u64,
102 addr: String,
104 primary: bool,
106 },
107
108 State {
115 timestamp: u64,
117 name: OpenVpnState,
119 description: String,
121 local_ip: String,
123 remote_ip: String,
125 remote_port: String,
127 local_addr: String,
129 local_port: String,
131 local_ipv6: String,
133 },
134
135 ByteCount {
137 bytes_in: u64,
139 bytes_out: u64,
141 },
142
143 ByteCountCli {
145 cid: u64,
147 bytes_in: u64,
149 bytes_out: u64,
151 },
152
153 Log {
155 timestamp: u64,
157 level: LogLevel,
159 message: String,
161 },
162
163 Echo {
165 timestamp: u64,
167 param: String,
169 },
170
171 Hold {
173 text: String,
175 },
176
177 Fatal {
179 message: String,
181 },
182
183 Pkcs11IdCount {
185 count: u32,
187 },
188
189 NeedOk {
191 name: String,
193 message: String,
195 },
196
197 NeedStr {
199 name: String,
201 message: String,
203 },
204
205 RsaSign {
207 data: String,
209 },
210
211 Remote {
213 host: String,
215 port: u16,
217 protocol: crate::transport_protocol::TransportProtocol,
219 },
220
221 Proxy {
227 index: u32,
229 proxy_type: String,
231 host: String,
233 },
234
235 Password(PasswordNotification),
237
238 Simple {
241 kind: String,
243 payload: String,
245 },
246}
247
248struct RedactedEnv<'a>(&'a [(String, String)]);
250
251impl fmt::Debug for RedactedEnv<'_> {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 f.debug_list()
254 .entries(self.0.iter().map(|(k, v)| {
255 if SENSITIVE_ENV_KEYS.contains(&k.as_str()) {
256 (k.as_str(), "<redacted>")
257 } else {
258 (k.as_str(), v.as_str())
259 }
260 }))
261 .finish()
262 }
263}
264
265impl fmt::Debug for Notification {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 match self {
268 Self::Client {
269 event,
270 cid,
271 kid,
272 env,
273 } => f
274 .debug_struct("Client")
275 .field("event", event)
276 .field("cid", cid)
277 .field("kid", kid)
278 .field("env", &RedactedEnv(env))
279 .finish(),
280 Self::ClientAddress { cid, addr, primary } => f
281 .debug_struct("ClientAddress")
282 .field("cid", cid)
283 .field("addr", addr)
284 .field("primary", primary)
285 .finish(),
286 Self::State {
287 timestamp,
288 name,
289 description,
290 local_ip,
291 remote_ip,
292 remote_port,
293 local_addr,
294 local_port,
295 local_ipv6,
296 } => f
297 .debug_struct("State")
298 .field("timestamp", timestamp)
299 .field("name", name)
300 .field("description", description)
301 .field("local_ip", local_ip)
302 .field("remote_ip", remote_ip)
303 .field("remote_port", remote_port)
304 .field("local_addr", local_addr)
305 .field("local_port", local_port)
306 .field("local_ipv6", local_ipv6)
307 .finish(),
308 Self::ByteCount {
309 bytes_in,
310 bytes_out,
311 } => f
312 .debug_struct("ByteCount")
313 .field("bytes_in", bytes_in)
314 .field("bytes_out", bytes_out)
315 .finish(),
316 Self::ByteCountCli {
317 cid,
318 bytes_in,
319 bytes_out,
320 } => f
321 .debug_struct("ByteCountCli")
322 .field("cid", cid)
323 .field("bytes_in", bytes_in)
324 .field("bytes_out", bytes_out)
325 .finish(),
326 Self::Log {
327 timestamp,
328 level,
329 message,
330 } => f
331 .debug_struct("Log")
332 .field("timestamp", timestamp)
333 .field("level", level)
334 .field("message", message)
335 .finish(),
336 Self::Echo { timestamp, param } => f
337 .debug_struct("Echo")
338 .field("timestamp", timestamp)
339 .field("param", param)
340 .finish(),
341 Self::Hold { text } => f.debug_struct("Hold").field("text", text).finish(),
342 Self::Fatal { message } => f.debug_struct("Fatal").field("message", message).finish(),
343 Self::Pkcs11IdCount { count } => f
344 .debug_struct("Pkcs11IdCount")
345 .field("count", count)
346 .finish(),
347 Self::NeedOk { name, message } => f
348 .debug_struct("NeedOk")
349 .field("name", name)
350 .field("message", message)
351 .finish(),
352 Self::NeedStr { name, message } => f
353 .debug_struct("NeedStr")
354 .field("name", name)
355 .field("message", message)
356 .finish(),
357 Self::RsaSign { data } => f.debug_struct("RsaSign").field("data", data).finish(),
358 Self::Remote {
359 host,
360 port,
361 protocol,
362 } => f
363 .debug_struct("Remote")
364 .field("host", host)
365 .field("port", port)
366 .field("protocol", protocol)
367 .finish(),
368 Self::Proxy {
369 index,
370 proxy_type,
371 host,
372 } => f
373 .debug_struct("Proxy")
374 .field("index", index)
375 .field("proxy_type", proxy_type)
376 .field("host", host)
377 .finish(),
378 Self::Password(p) => f.debug_tuple("Password").field(p).finish(),
379 Self::Simple { kind, payload } => f
380 .debug_struct("Simple")
381 .field("kind", kind)
382 .field("payload", payload)
383 .finish(),
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::transport_protocol::TransportProtocol;
392
393 #[test]
396 fn debug_redacts_password_env_key() {
397 let notif = Notification::Client {
398 event: ClientEvent::Connect,
399 cid: 1,
400 kid: Some(0),
401 env: vec![
402 ("common_name".to_string(), "alice".to_string()),
403 ("password".to_string(), "s3cret".to_string()),
404 ],
405 };
406 let dbg = format!("{notif:?}");
407 assert!(dbg.contains("alice"), "non-sensitive values should appear");
408 assert!(
409 !dbg.contains("s3cret"),
410 "password value must not appear in Debug output"
411 );
412 assert!(
413 dbg.contains("<redacted>"),
414 "password value should be replaced with <redacted>"
415 );
416 }
417
418 #[test]
419 fn debug_does_not_redact_non_sensitive_keys() {
420 let notif = Notification::Client {
421 event: ClientEvent::Disconnect,
422 cid: 5,
423 kid: None,
424 env: vec![("untrusted_ip".to_string(), "10.0.0.1".to_string())],
425 };
426 let dbg = format!("{notif:?}");
427 assert!(dbg.contains("10.0.0.1"));
428 }
429
430 #[test]
433 fn password_notification_debug_redacts_token() {
434 let notif = PasswordNotification::AuthToken {
435 token: Redacted::new("super-secret-token".to_string()),
436 };
437 let dbg = format!("{notif:?}");
438 assert!(
439 !dbg.contains("super-secret-token"),
440 "auth token must not appear in Debug output"
441 );
442 }
443
444 #[test]
445 fn password_notification_eq() {
446 let a = PasswordNotification::NeedAuth {
447 auth_type: AuthType::Auth,
448 };
449 let b = PasswordNotification::NeedAuth {
450 auth_type: AuthType::Auth,
451 };
452 assert_eq!(a, b);
453
454 let c = PasswordNotification::NeedPassword {
455 auth_type: AuthType::PrivateKey,
456 };
457 assert_ne!(a, c);
458 }
459
460 #[test]
461 fn password_notification_static_challenge_fields() {
462 let sc = PasswordNotification::StaticChallenge {
463 echo: true,
464 response_concat: false,
465 challenge: "Enter PIN".to_string(),
466 };
467 if let PasswordNotification::StaticChallenge {
468 echo,
469 response_concat,
470 challenge,
471 } = sc
472 {
473 assert!(echo);
474 assert!(!response_concat);
475 assert_eq!(challenge, "Enter PIN");
476 } else {
477 panic!("wrong variant");
478 }
479 }
480
481 #[test]
482 fn password_notification_dynamic_challenge_fields() {
483 let dc = PasswordNotification::DynamicChallenge {
484 flags: "R,E".to_string(),
485 state_id: "abc123".to_string(),
486 username_b64: "dXNlcg==".to_string(),
487 challenge: "Enter OTP".to_string(),
488 };
489 if let PasswordNotification::DynamicChallenge {
490 flags,
491 state_id,
492 challenge,
493 ..
494 } = dc
495 {
496 assert_eq!(flags, "R,E");
497 assert_eq!(state_id, "abc123");
498 assert_eq!(challenge, "Enter OTP");
499 } else {
500 panic!("wrong variant");
501 }
502 }
503
504 #[test]
507 fn debug_state_notification() {
508 let notif = Notification::State {
509 timestamp: 1700000000,
510 name: OpenVpnState::Connected,
511 description: "SUCCESS".to_string(),
512 local_ip: "10.0.0.2".to_string(),
513 remote_ip: "1.2.3.4".to_string(),
514 remote_port: "1194".to_string(),
515 local_addr: "192.168.1.5".to_string(),
516 local_port: "51234".to_string(),
517 local_ipv6: String::new(),
518 };
519 let dbg = format!("{notif:?}");
520 assert!(dbg.contains("State"));
521 assert!(dbg.contains("Connected"));
522 assert!(dbg.contains("10.0.0.2"));
523 }
524
525 #[test]
526 fn debug_bytecount() {
527 let notif = Notification::ByteCount {
528 bytes_in: 1024,
529 bytes_out: 2048,
530 };
531 let dbg = format!("{notif:?}");
532 assert!(dbg.contains("1024"));
533 assert!(dbg.contains("2048"));
534 }
535
536 #[test]
537 fn debug_bytecount_cli() {
538 let notif = Notification::ByteCountCli {
539 cid: 7,
540 bytes_in: 100,
541 bytes_out: 200,
542 };
543 let dbg = format!("{notif:?}");
544 assert!(dbg.contains("ByteCountCli"));
545 assert!(dbg.contains("7"));
546 }
547
548 #[test]
549 fn debug_log() {
550 let notif = Notification::Log {
551 timestamp: 1700000000,
552 level: LogLevel::Warning,
553 message: "something happened".to_string(),
554 };
555 let dbg = format!("{notif:?}");
556 assert!(dbg.contains("Log"));
557 assert!(dbg.contains("something happened"));
558 }
559
560 #[test]
561 fn debug_echo() {
562 let notif = Notification::Echo {
563 timestamp: 123,
564 param: "push-update".to_string(),
565 };
566 let dbg = format!("{notif:?}");
567 assert!(dbg.contains("Echo"));
568 assert!(dbg.contains("push-update"));
569 }
570
571 #[test]
572 fn debug_hold() {
573 let notif = Notification::Hold {
574 text: "Waiting for hold release".to_string(),
575 };
576 let dbg = format!("{notif:?}");
577 assert!(dbg.contains("Hold"));
578 }
579
580 #[test]
581 fn debug_fatal() {
582 let notif = Notification::Fatal {
583 message: "cannot allocate TUN/TAP".to_string(),
584 };
585 let dbg = format!("{notif:?}");
586 assert!(dbg.contains("Fatal"));
587 assert!(dbg.contains("cannot allocate TUN/TAP"));
588 }
589
590 #[test]
591 fn debug_remote() {
592 let notif = Notification::Remote {
593 host: "vpn.example.com".to_string(),
594 port: 1194,
595 protocol: TransportProtocol::Udp,
596 };
597 let dbg = format!("{notif:?}");
598 assert!(dbg.contains("Remote"));
599 assert!(dbg.contains("vpn.example.com"));
600 }
601
602 #[test]
603 fn debug_proxy() {
604 let notif = Notification::Proxy {
605 index: 1,
606 proxy_type: "TCP".to_string(),
607 host: "proxy.local".to_string(),
608 };
609 let dbg = format!("{notif:?}");
610 assert!(dbg.contains("Proxy"));
611 assert!(dbg.contains("proxy.local"));
612 }
613
614 #[test]
615 fn debug_simple_fallback() {
616 let notif = Notification::Simple {
617 kind: "FUTURE_TYPE".to_string(),
618 payload: "some data".to_string(),
619 };
620 let dbg = format!("{notif:?}");
621 assert!(dbg.contains("FUTURE_TYPE"));
622 assert!(dbg.contains("some data"));
623 }
624
625 #[test]
626 fn debug_client_address() {
627 let notif = Notification::ClientAddress {
628 cid: 42,
629 addr: "10.8.0.6".to_string(),
630 primary: true,
631 };
632 let dbg = format!("{notif:?}");
633 assert!(dbg.contains("ClientAddress"));
634 assert!(dbg.contains("10.8.0.6"));
635 assert!(dbg.contains("true"));
636 }
637
638 #[test]
641 fn ovpn_message_eq() {
642 assert_eq!(
643 OvpnMessage::Success("pid=42".to_string()),
644 OvpnMessage::Success("pid=42".to_string()),
645 );
646 assert_ne!(
647 OvpnMessage::Success("a".to_string()),
648 OvpnMessage::Error("a".to_string()),
649 );
650 }
651
652 #[test]
653 fn ovpn_message_pkcs11_entry() {
654 let msg = OvpnMessage::Pkcs11IdEntry {
655 index: "0".to_string(),
656 id: "slot_0".to_string(),
657 blob: "AQID".to_string(),
658 };
659 let dbg = format!("{msg:?}");
660 assert!(dbg.contains("Pkcs11IdEntry"));
661 assert!(dbg.contains("slot_0"));
662 }
663
664 #[test]
665 fn ovpn_message_password_prompt() {
666 assert_eq!(OvpnMessage::PasswordPrompt, OvpnMessage::PasswordPrompt);
667 }
668
669 #[test]
670 fn ovpn_message_unrecognized() {
671 let msg = OvpnMessage::Unrecognized {
672 line: "garbage".to_string(),
673 kind: crate::unrecognized::UnrecognizedKind::UnexpectedLine,
674 };
675 let dbg = format!("{msg:?}");
676 assert!(dbg.contains("garbage"));
677 }
678}
679
680#[derive(Debug, Clone, PartialEq, Eq)]
682pub enum OvpnMessage {
683 Success(String),
685
686 Error(String),
688
689 MultiLine(Vec<String>),
692
693 Pkcs11IdEntry {
696 index: String,
698 id: String,
700 blob: String,
702 },
703
704 Notification(Notification),
706
707 Info(String),
711
712 PasswordPrompt,
717
718 Unrecognized {
721 line: String,
723 kind: crate::unrecognized::UnrecognizedKind,
725 },
726}