1use crate::{wg::PeerInfoExt, DEFAULT_HOSTS_PATH};
2use anyhow::{anyhow, Error};
3use clap::{
4 builder::{PossibleValuesParser, TypedValueParser},
5 Args,
6};
7use ipnet::{IpNet, PrefixLenError};
8use once_cell::sync::Lazy;
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use std::{
12 fmt::{self, Display, Formatter},
13 io,
14 net::{IpAddr, SocketAddr, ToSocketAddrs},
15 ops::{Deref, DerefMut},
16 path::{Path, PathBuf},
17 str::FromStr,
18 time::{Duration, SystemTime},
19 vec,
20};
21use url::Host;
22use wireguard_control::{
23 AllowedIp, Backend, InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder,
24 PeerInfo,
25};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Interface {
29 name: InterfaceName,
30}
31
32impl FromStr for Interface {
33 type Err = InvalidInterfaceName;
34
35 fn from_str(name: &str) -> Result<Self, Self::Err> {
36 if !Hostname::is_valid(name) {
37 Err(InvalidInterfaceName::InvalidChars)
38 } else {
39 Ok(Self {
40 name: name.parse()?,
41 })
42 }
43 }
44}
45
46impl Deref for Interface {
47 type Target = InterfaceName;
48
49 fn deref(&self) -> &Self::Target {
50 &self.name
51 }
52}
53
54impl Display for Interface {
55 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
56 f.write_str(&self.name.to_string())
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Endpoint {
63 host: Host,
64 port: u16,
65}
66
67impl From<SocketAddr> for Endpoint {
68 fn from(addr: SocketAddr) -> Self {
69 match addr {
70 SocketAddr::V4(v4addr) => Self {
71 host: Host::Ipv4(*v4addr.ip()),
72 port: v4addr.port(),
73 },
74 SocketAddr::V6(v6addr) => Self {
75 host: Host::Ipv6(*v6addr.ip()),
76 port: v6addr.port(),
77 },
78 }
79 }
80}
81
82impl FromStr for Endpoint {
83 type Err = &'static str;
84
85 fn from_str(s: &str) -> Result<Self, Self::Err> {
86 match s.rsplitn(2, ':').collect::<Vec<&str>>().as_slice() {
87 [port, host] => {
88 let port = port.parse().map_err(|_| "couldn't parse port")?;
89 let host = Host::parse(host).map_err(|_| "couldn't parse host")?;
90 Ok(Endpoint { host, port })
91 },
92 _ => Err("couldn't parse in form of 'host:port'"),
93 }
94 }
95}
96
97impl Serialize for Endpoint {
98 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
99 where
100 S: serde::Serializer,
101 {
102 serializer.serialize_str(&self.to_string())
103 }
104}
105
106impl<'de> Deserialize<'de> for Endpoint {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: serde::Deserializer<'de>,
110 {
111 struct EndpointVisitor;
112 impl serde::de::Visitor<'_> for EndpointVisitor {
113 type Value = Endpoint;
114
115 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
116 formatter.write_str("a valid host:port endpoint")
117 }
118
119 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
120 where
121 E: serde::de::Error,
122 {
123 s.parse().map_err(serde::de::Error::custom)
124 }
125 }
126 deserializer.deserialize_str(EndpointVisitor)
127 }
128}
129
130impl Display for Endpoint {
131 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
132 self.host.fmt(f)?;
133 f.write_str(":")?;
134 self.port.fmt(f)
135 }
136}
137
138impl Endpoint {
139 pub fn resolve(&self) -> Result<SocketAddr, io::Error> {
140 let mut addrs = self.to_string().to_socket_addrs()?;
141 addrs.next().ok_or_else(|| {
142 io::Error::new(
143 io::ErrorKind::AddrNotAvailable,
144 "failed to resolve address".to_string(),
145 )
146 })
147 }
148
149 pub fn is_host_unspecified(&self) -> bool {
151 match self.host {
152 Host::Ipv4(ip) => ip.is_unspecified(),
153 Host::Ipv6(ip) => ip.is_unspecified(),
154 Host::Domain(_) => false,
155 }
156 }
157
158 pub fn port(&self) -> u16 {
159 self.port
160 }
161}
162
163#[derive(Deserialize, Serialize, Debug)]
164#[serde(tag = "option", content = "content")]
165pub enum EndpointContents {
166 Set(Endpoint),
167 Unset,
168}
169
170impl From<EndpointContents> for Option<Endpoint> {
171 fn from(endpoint: EndpointContents) -> Self {
172 match endpoint {
173 EndpointContents::Set(addr) => Some(addr),
174 EndpointContents::Unset => None,
175 }
176 }
177}
178
179impl From<Option<Endpoint>> for EndpointContents {
180 fn from(option: Option<Endpoint>) -> Self {
181 match option {
182 Some(addr) => Self::Set(addr),
183 None => Self::Unset,
184 }
185 }
186}
187
188#[derive(Deserialize, Serialize, Debug)]
189pub struct AssociationContents {
190 pub cidr_id_1: i64,
191 pub cidr_id_2: i64,
192}
193
194#[derive(Deserialize, Serialize, Debug)]
195pub struct Association {
196 pub id: i64,
197
198 #[serde(flatten)]
199 pub contents: AssociationContents,
200}
201
202impl Deref for Association {
203 type Target = AssociationContents;
204
205 fn deref(&self) -> &Self::Target {
206 &self.contents
207 }
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Eq, Ord)]
211pub struct CidrContents {
212 pub name: String,
213 pub cidr: IpNet,
214 pub parent: Option<i64>,
215}
216
217impl Deref for CidrContents {
218 type Target = IpNet;
219
220 fn deref(&self) -> &Self::Target {
221 &self.cidr
222 }
223}
224
225#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Eq, Ord)]
226pub struct Cidr {
227 pub id: i64,
228
229 #[serde(flatten)]
230 pub contents: CidrContents,
231}
232
233impl Display for Cidr {
234 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
235 write!(f, "{} ({})", self.name, self.cidr)
236 }
237}
238
239impl Deref for Cidr {
240 type Target = CidrContents;
241
242 fn deref(&self) -> &Self::Target {
243 &self.contents
244 }
245}
246
247#[derive(Clone, PartialEq, PartialOrd, Eq, Ord)]
248pub struct CidrTree<'a> {
249 cidrs: &'a [Cidr],
250 contents: &'a Cidr,
251}
252
253impl std::ops::Deref for CidrTree<'_> {
254 type Target = Cidr;
255
256 fn deref(&self) -> &Self::Target {
257 self.contents
258 }
259}
260
261impl<'a> CidrTree<'a> {
262 pub fn new(cidrs: &'a [Cidr]) -> Self {
263 let root = cidrs
264 .iter()
265 .min_by_key(|c| c.cidr.prefix_len())
266 .expect("failed to find root CIDR");
267 Self::with_root(cidrs, root)
268 }
269
270 pub fn with_root(cidrs: &'a [Cidr], root: &'a Cidr) -> Self {
271 Self {
272 cidrs,
273 contents: root,
274 }
275 }
276
277 pub fn children(&self) -> impl Iterator<Item = CidrTree<'_>> {
278 self.cidrs
279 .iter()
280 .filter(move |c| c.parent == Some(self.contents.id))
281 .map(move |c| CidrTree {
282 cidrs: self.cidrs,
283 contents: c,
284 })
285 }
286
287 pub fn leaves(&self) -> Vec<Cidr> {
288 if !self.cidrs.iter().any(|cidr| cidr.parent == Some(self.id)) {
289 vec![self.contents.clone()]
290 } else {
291 self.children().flat_map(|child| child.leaves()).collect()
292 }
293 }
294
295 pub fn ip_net_for(&self, ip: IpAddr) -> Result<IpNet, PrefixLenError> {
296 IpNet::new(ip, self.contents.prefix_len())
297 }
298}
299
300#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
301pub struct RedeemContents {
302 pub public_key: String,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Args)]
306pub struct InstallOpts {
307 #[clap(long, conflicts_with = "default_name")]
309 pub name: Option<String>,
310
311 #[clap(long = "default-name")]
313 pub default_name: bool,
314
315 #[clap(short, long)]
317 pub delete_invite: bool,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Args)]
321pub struct AddPeerOpts {
322 #[clap(long)]
324 pub name: Option<Hostname>,
325
326 #[clap(long, conflicts_with = "auto_ip")]
328 pub ip: Option<IpAddr>,
329
330 #[clap(long = "auto-ip")]
332 pub auto_ip: bool,
333
334 #[clap(long)]
336 pub cidr: Option<String>,
337
338 #[clap(long)]
340 pub admin: Option<bool>,
341
342 #[clap(long)]
344 pub yes: bool,
345
346 #[clap(long)]
348 pub save_config: Option<String>,
349
350 #[clap(long)]
352 pub invite_expires: Option<Timestring>,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Args)]
356pub struct RenamePeerOpts {
357 #[clap(long)]
359 pub name: Option<Hostname>,
360
361 #[clap(long)]
363 pub new_name: Option<Hostname>,
364
365 #[clap(long)]
367 pub yes: bool,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Args)]
371pub struct EnableDisablePeerOpts {
372 #[clap(long)]
374 pub name: Option<Hostname>,
375
376 #[clap(long, requires("name"))]
378 pub yes: bool,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq, Args)]
382pub struct AddCidrOpts {
383 #[clap(long)]
385 pub name: Option<Hostname>,
386
387 #[clap(long)]
389 pub cidr: Option<IpNet>,
390
391 #[clap(long)]
393 pub parent: Option<String>,
394
395 #[clap(long)]
397 pub yes: bool,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Args)]
401pub struct RenameCidrOpts {
402 #[clap(long)]
404 pub name: Option<String>,
405
406 #[clap(long)]
408 pub new_name: Option<String>,
409
410 #[clap(long)]
412 pub yes: bool,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Args)]
416pub struct DeleteCidrOpts {
417 #[clap(long)]
419 pub name: Option<String>,
420
421 #[clap(long)]
423 pub yes: bool,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Args)]
427pub struct AddDeleteAssociationOpts {
428 pub cidr1: Option<String>,
430
431 pub cidr2: Option<String>,
433
434 #[clap(long)]
436 pub yes: bool,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Args)]
440pub struct ListenPortOpts {
441 #[clap(short, long)]
443 pub listen_port: Option<u16>,
444
445 #[clap(short, long, conflicts_with = "listen_port")]
447 pub unset: bool,
448
449 #[clap(long)]
451 pub yes: bool,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Args)]
455pub struct OverrideEndpointOpts {
456 #[clap(short, long)]
461 pub endpoint: Option<Endpoint>,
462
463 #[clap(short, long, conflicts_with = "endpoint")]
465 pub unset: bool,
466
467 #[clap(long)]
469 pub yes: bool,
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, Args)]
473pub struct OverridePeerEndpointOpts {
474 #[clap(long)]
476 pub name: Option<Hostname>,
477
478 #[clap(short, long)]
480 pub endpoint: Option<Endpoint>,
481
482 #[clap(short, long, conflicts_with = "endpoint")]
484 pub unset: bool,
485
486 #[clap(long)]
488 pub yes: bool,
489}
490
491#[derive(Debug, Clone, Args)]
492pub struct NatOpts {
493 #[clap(long)]
494 pub no_nat_traversal: bool,
497
498 #[clap(long)]
499 pub exclude_nat_candidates: Vec<IpNet>,
502
503 #[clap(long, conflicts_with = "exclude_nat_candidates")]
504 pub no_nat_candidates: bool,
507}
508
509impl NatOpts {
510 pub fn all_disabled() -> Self {
511 Self {
512 no_nat_traversal: true,
513 exclude_nat_candidates: vec![],
514 no_nat_candidates: true,
515 }
516 }
517
518 pub fn is_excluded(&self, ip: IpAddr) -> bool {
520 self.no_nat_candidates
521 || self
522 .exclude_nat_candidates
523 .iter()
524 .any(|network| network.contains(&ip))
525 }
526}
527
528#[derive(Debug, Clone, Copy, Args)]
529pub struct NetworkOpts {
530 #[clap(long)]
531 pub no_routing: bool,
534
535 #[clap(long, default_value_t, value_parser = PossibleValuesParser::new(Backend::variants()).map(|s| s.parse::<Backend>().unwrap()))]
536 pub backend: Backend,
539
540 #[clap(long)]
541 pub mtu: Option<u32>,
543}
544
545#[derive(Clone, Debug, Args)]
546pub struct HostsOpts {
547 #[clap(long = "hosts-path", default_value = DEFAULT_HOSTS_PATH)]
549 pub hosts_path: PathBuf,
550
551 #[clap(long = "no-write-hosts", conflicts_with = "hosts_path")]
553 pub no_write_hosts: bool,
554
555 #[clap(long = "host-suffix")]
559 pub host_suffix: Option<String>,
560}
561
562#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
563pub struct PeerContents {
564 pub name: Hostname,
565 pub ip: IpAddr,
566 pub cidr_id: i64,
567 pub public_key: String,
568 pub endpoint: Option<Endpoint>,
569 pub persistent_keepalive_interval: Option<u16>,
570 pub is_admin: bool,
571 pub is_disabled: bool,
572 pub is_redeemed: bool,
573 pub invite_expires: Option<SystemTime>,
574 #[serde(default)]
575 pub candidates: Vec<Endpoint>,
576}
577
578#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
579pub struct Peer {
580 pub id: i64,
581
582 #[serde(flatten)]
583 pub contents: PeerContents,
584}
585
586impl AsRef<Peer> for Peer {
587 fn as_ref(&self) -> &Peer {
588 self
589 }
590}
591
592impl Deref for Peer {
593 type Target = PeerContents;
594
595 fn deref(&self) -> &Self::Target {
596 &self.contents
597 }
598}
599
600impl DerefMut for Peer {
601 fn deref_mut(&mut self) -> &mut Self::Target {
602 &mut self.contents
603 }
604}
605
606impl Display for Peer {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 write!(f, "{} ({})", &self.name, &self.public_key)
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
613pub enum PeerChange {
614 AllowedIPs {
615 old: Vec<AllowedIp>,
616 new: Vec<AllowedIp>,
617 },
618 PersistentKeepalive {
619 old: Option<u16>,
620 new: Option<u16>,
621 },
622 Endpoint {
623 old: Option<SocketAddr>,
624 new: Option<SocketAddr>,
625 },
626 NatTraverseReattempt,
627}
628
629impl Display for PeerChange {
630 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
631 match self {
632 Self::AllowedIPs { old, new } => write!(f, "Allowed IPs: {old:?} => {new:?}"),
633 Self::PersistentKeepalive { old, new } => write!(
634 f,
635 "Persistent Keepalive: {} => {}",
636 old.display_string(),
637 new.display_string()
638 ),
639 Self::Endpoint { old, new } => write!(
640 f,
641 "Endpoint: {} => {}",
642 old.display_string(),
643 new.display_string()
644 ),
645 Self::NatTraverseReattempt => write!(f, "NAT Traversal Reattempt"),
646 }
647 }
648}
649
650trait OptionExt {
651 fn display_string(&self) -> String;
652}
653
654impl<T: std::fmt::Debug> OptionExt for Option<T> {
655 fn display_string(&self) -> String {
656 match self {
657 Some(x) => {
658 format!("{x:?}")
659 },
660 None => "[none]".to_string(),
661 }
662 }
663}
664
665#[derive(Clone, Debug, PartialEq, Eq)]
668pub struct PeerDiff<'a> {
669 pub old: Option<&'a PeerConfig>,
670 pub new: Option<&'a Peer>,
671 builder: PeerConfigBuilder,
672 changes: Vec<PeerChange>,
673}
674
675impl<'a> PeerDiff<'a> {
676 pub fn new(
677 old_info: Option<&'a PeerInfo>,
678 new: Option<&'a Peer>,
679 ) -> Result<Option<Self>, Error> {
680 let old = old_info.map(|p| &p.config);
681 match (old_info, new) {
682 (Some(old), Some(new)) if old.config.public_key.to_base64() != new.public_key => Err(
683 anyhow!("old and new peer configs have different public keys"),
684 ),
685 (None, None) => Ok(None),
686 _ => Ok(
687 Self::peer_config_builder(old_info, new).map(|(builder, changes)| Self {
688 old,
689 new,
690 builder,
691 changes,
692 }),
693 ),
694 }
695 }
696
697 pub fn public_key(&self) -> &Key {
698 self.builder.public_key()
699 }
700
701 pub fn changes(&self) -> &[PeerChange] {
702 &self.changes
703 }
704
705 fn peer_config_builder(
706 old_info: Option<&PeerInfo>,
707 new: Option<&Peer>,
708 ) -> Option<(PeerConfigBuilder, Vec<PeerChange>)> {
709 let old = old_info.map(|p| &p.config);
710 let public_key = match (old, new) {
711 (Some(old), _) => old.public_key.clone(),
712 (_, Some(new)) => Key::from_base64(&new.public_key).unwrap(),
713 _ => return None,
714 };
715 let mut builder = PeerConfigBuilder::new(&public_key);
716 let mut changes = vec![];
717
718 if new.is_none() || matches!(new, Some(new) if new.is_disabled) {
720 return Some((builder.remove(), changes));
721 }
722 let new = new.unwrap();
724
725 let new_allowed_ips = &[AllowedIp {
726 address: new.ip,
727 cidr: if new.ip.is_ipv4() { 32 } else { 128 },
728 }];
729 if old.is_none() || matches!(old, Some(old) if old.allowed_ips != new_allowed_ips) {
730 builder = builder
731 .replace_allowed_ips()
732 .add_allowed_ips(new_allowed_ips);
733 changes.push(PeerChange::AllowedIPs {
734 old: old.map(|o| o.allowed_ips.clone()).unwrap_or_default(),
735 new: new_allowed_ips.to_vec(),
736 });
737 }
738
739 if old.is_none()
740 || matches!(old, Some(old) if old.persistent_keepalive_interval != new.persistent_keepalive_interval)
741 {
742 builder = match new.persistent_keepalive_interval {
743 Some(interval) => builder.set_persistent_keepalive_interval(interval),
744 None => builder.unset_persistent_keepalive(),
745 };
746 changes.push(PeerChange::PersistentKeepalive {
747 old: old.and_then(|p| p.persistent_keepalive_interval),
748 new: new.persistent_keepalive_interval,
749 });
750 }
751
752 if !old_info
754 .map(|info| info.is_recently_connected())
755 .unwrap_or_default()
756 {
757 let mut endpoint_changed = false;
758 let resolved = new.endpoint.as_ref().and_then(|e| e.resolve().ok());
759 if let Some(addr) = resolved {
760 if old.is_none() || matches!(old, Some(old) if old.endpoint != resolved) {
761 builder = builder.set_endpoint(addr);
762 changes.push(PeerChange::Endpoint {
763 old: old.and_then(|p| p.endpoint),
764 new: Some(addr),
765 });
766 endpoint_changed = true;
767 }
768 }
769 if !endpoint_changed && !new.candidates.is_empty() {
770 changes.push(PeerChange::NatTraverseReattempt)
771 }
772 }
773
774 if !changes.is_empty() {
775 Some((builder, changes))
776 } else {
777 None
778 }
779 }
780}
781
782impl From<&Peer> for PeerConfigBuilder {
783 fn from(peer: &Peer) -> Self {
784 PeerDiff::new(None, Some(peer))
785 .expect("No Err on explicitly set peer data")
786 .expect("None -> Some(peer) will always create a PeerDiff")
787 .into()
788 }
789}
790
791impl From<PeerDiff<'_>> for PeerConfigBuilder {
792 fn from(diff: PeerDiff) -> Self {
795 diff.builder
796 }
797}
798
799#[derive(Debug, Clone, Deserialize, Serialize)]
802pub struct State {
803 pub peers: Vec<Peer>,
807
808 pub cidrs: Vec<Cidr>,
811}
812
813#[derive(Debug, Default, Clone, Deserialize, Serialize)]
815pub struct ServerCapabilities {
816 #[serde(default)]
817 pub unspecified_ip_in_override_endpoint: bool,
818}
819
820#[derive(Clone, Debug, PartialEq, Eq)]
821pub struct Timestring {
822 timestring: String,
823 seconds: u64,
824}
825
826impl Display for Timestring {
827 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828 f.write_str(&self.timestring)
829 }
830}
831
832impl FromStr for Timestring {
833 type Err = &'static str;
834
835 fn from_str(timestring: &str) -> Result<Self, Self::Err> {
836 if timestring.len() < 2 {
837 Err("timestring isn't long enough!")
838 } else {
839 let (n, suffix) = timestring.split_at(timestring.len() - 1);
840 let n: u64 = n.parse().map_err(|_| {
841 "invalid timestring (a number followed by a time unit character, eg. '15m')"
842 })?;
843 let multiplier = match suffix {
844 "s" => Ok(1),
845 "m" => Ok(60),
846 "h" => Ok(60 * 60),
847 "d" => Ok(60 * 60 * 24),
848 "w" => Ok(60 * 60 * 24 * 7),
849 _ => Err("invalid timestring suffix (must be one of 's', 'm', 'h', 'd', or 'w')"),
850 }?;
851
852 Ok(Self {
853 timestring: timestring.to_string(),
854 seconds: n * multiplier,
855 })
856 }
857 }
858}
859
860impl From<Timestring> for Duration {
861 fn from(timestring: Timestring) -> Self {
862 Duration::from_secs(timestring.seconds)
863 }
864}
865
866#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
867pub struct Hostname(String);
868
869static HOSTNAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^([a-z0-9]-?)*[a-z0-9]$").unwrap());
872
873impl Hostname {
874 pub fn is_valid(name: &str) -> bool {
875 name.len() < 64 && HOSTNAME_REGEX.is_match(name)
876 }
877}
878
879impl FromStr for Hostname {
880 type Err = &'static str;
881
882 fn from_str(name: &str) -> Result<Self, Self::Err> {
883 if Self::is_valid(name) {
884 Ok(Self(name.to_string()))
885 } else {
886 Err("invalid hostname string (only alphanumeric with dashes)")
887 }
888 }
889}
890
891impl Deref for Hostname {
892 type Target = str;
893
894 fn deref(&self) -> &Self::Target {
895 &self.0
896 }
897}
898
899impl Display for Hostname {
900 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
901 f.write_str(&self.0)
902 }
903}
904
905pub trait IoErrorContext<T> {
906 fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T, WrappedIoError>;
907 fn with_str<S: Into<String>>(self, context: S) -> Result<T, WrappedIoError>;
908}
909
910impl<T> IoErrorContext<T> for Result<T, std::io::Error> {
911 fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T, WrappedIoError> {
912 self.with_str(path.as_ref().to_string_lossy())
913 }
914
915 fn with_str<S: Into<String>>(self, context: S) -> Result<T, WrappedIoError> {
916 self.map_err(|e| WrappedIoError {
917 io_error: e,
918 context: context.into(),
919 })
920 }
921}
922
923#[derive(Debug)]
924pub struct WrappedIoError {
925 io_error: std::io::Error,
926 context: String,
927}
928
929impl Display for WrappedIoError {
930 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
931 write!(f, "{} - {}", self.context, self.io_error)
932 }
933}
934
935impl Deref for WrappedIoError {
936 type Target = std::io::Error;
937
938 fn deref(&self) -> &Self::Target {
939 &self.io_error
940 }
941}
942
943impl std::error::Error for WrappedIoError {}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use std::net::IpAddr;
949 use wireguard_control::{Key, PeerConfigBuilder, PeerStats};
950
951 #[test]
952 fn test_peer_no_diff() {
953 const PUBKEY: &str = "4CNZorWVtohO64n6AAaH/JyFjIIgBFrfJK2SGtKjzEE=";
954 let ip: IpAddr = "10.0.0.1".parse().unwrap();
955 let peer = Peer {
956 id: 1,
957 contents: PeerContents {
958 name: "peer1".parse().unwrap(),
959 ip,
960 cidr_id: 1,
961 public_key: PUBKEY.to_owned(),
962 endpoint: None,
963 persistent_keepalive_interval: None,
964 is_admin: false,
965 is_disabled: false,
966 is_redeemed: true,
967 invite_expires: None,
968 candidates: vec![],
969 },
970 };
971 let builder =
972 PeerConfigBuilder::new(&Key::from_base64(PUBKEY).unwrap()).add_allowed_ip(ip, 32);
973
974 let config = builder.into_peer_config();
975 let info = PeerInfo {
976 config,
977 stats: Default::default(),
978 };
979
980 let diff = PeerDiff::new(Some(&info), Some(&peer)).unwrap();
981
982 println!("{diff:?}");
983 assert_eq!(diff, None);
984 }
985
986 #[test]
987 fn test_peer_diff() {
988 const PUBKEY: &str = "4CNZorWVtohO64n6AAaH/JyFjIIgBFrfJK2SGtKjzEE=";
989 let ip: IpAddr = "10.0.0.1".parse().unwrap();
990 let peer = Peer {
991 id: 1,
992 contents: PeerContents {
993 name: "peer1".parse().unwrap(),
994 ip,
995 cidr_id: 1,
996 public_key: PUBKEY.to_owned(),
997 endpoint: None,
998 persistent_keepalive_interval: Some(15),
999 is_admin: false,
1000 is_disabled: false,
1001 is_redeemed: true,
1002 invite_expires: None,
1003 candidates: vec![],
1004 },
1005 };
1006 let builder =
1007 PeerConfigBuilder::new(&Key::from_base64(PUBKEY).unwrap()).add_allowed_ip(ip, 32);
1008
1009 let config = builder.into_peer_config();
1010 let info = PeerInfo {
1011 config,
1012 stats: Default::default(),
1013 };
1014 let diff = PeerDiff::new(Some(&info), Some(&peer)).unwrap();
1015
1016 println!("{peer:?}");
1017 println!("{:?}", info.config);
1018 assert!(diff.is_some());
1019 }
1020
1021 #[test]
1022 fn test_peer_diff_handshake_time() {
1023 const PUBKEY: &str = "4CNZorWVtohO64n6AAaH/JyFjIIgBFrfJK2SGtKjzEE=";
1024 let ip: IpAddr = "10.0.0.1".parse().unwrap();
1025 let peer = Peer {
1026 id: 1,
1027 contents: PeerContents {
1028 name: "peer1".parse().unwrap(),
1029 ip,
1030 cidr_id: 1,
1031 public_key: PUBKEY.to_owned(),
1032 endpoint: Some("1.1.1.1:1111".parse().unwrap()),
1033 persistent_keepalive_interval: None,
1034 is_admin: false,
1035 is_disabled: false,
1036 is_redeemed: true,
1037 invite_expires: None,
1038 candidates: vec![],
1039 },
1040 };
1041 let builder =
1042 PeerConfigBuilder::new(&Key::from_base64(PUBKEY).unwrap()).add_allowed_ip(ip, 32);
1043
1044 let config = builder.into_peer_config();
1045 let mut info = PeerInfo {
1046 config,
1047 stats: PeerStats {
1048 last_handshake_time: Some(SystemTime::now() - Duration::from_secs(200)),
1049 ..Default::default()
1050 },
1051 };
1052
1053 assert!(matches!(
1055 PeerDiff::new(Some(&info), Some(&peer)),
1056 Ok(Some(_))
1057 ));
1058
1059 info.stats.last_handshake_time = Some(SystemTime::now());
1061 assert!(matches!(PeerDiff::new(Some(&info), Some(&peer)), Ok(None)));
1062 }
1063}