1pub use anyhow::Error;
2use colored::Colorize;
3use hostsfile::HostsBuilder;
4use ipnet::IpNet;
5use std::{
6 fs::{self, File, Permissions},
7 io,
8 net::{IpAddr, Ipv6Addr},
9 os::unix::fs::PermissionsExt,
10 path::Path,
11 time::Duration,
12};
13use wireguard_control::InterfaceName;
14
15pub mod interface_config;
16#[cfg(target_os = "linux")]
17mod netlink;
18pub mod prompts;
19pub mod types;
20pub mod wg;
21
22pub use types::*;
23
24pub const REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5);
25pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
26pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
27
28pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
29 for dir in dirs {
30 match fs::create_dir(dir).with_path(dir) {
31 Ok(()) => {
32 log::debug!("created dir {}", dir.to_string_lossy());
33 std::fs::set_permissions(dir, Permissions::from_mode(0o700)).with_path(dir)?;
34 },
35 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
36 warn_on_dangerous_mode(dir).with_path(dir)?;
37 },
38 Err(e) => {
39 return Err(e);
40 },
41 }
42 }
43 Ok(())
44}
45
46pub fn warn_on_dangerous_mode(path: &Path) -> Result<(), io::Error> {
47 let file = File::open(path)?;
48 let metadata = file.metadata()?;
49 let permissions = metadata.permissions();
50 let mode = permissions.mode() & 0o777;
51
52 if mode & 0o007 != 0 {
53 log::warn!(
54 "{} is world-accessible (mode is {:#05o}). This is probably not what you want.",
55 path.to_string_lossy(),
56 mode
57 );
58 }
59 Ok(())
60}
61
62pub fn chmod(file: &File, new_mode: u32) -> Result<bool, io::Error> {
66 let metadata = file.metadata()?;
67 let mut permissions = metadata.permissions();
68 let mode = permissions.mode() & 0o777;
69 let updated = if mode != new_mode {
70 permissions.set_mode(new_mode);
71 file.set_permissions(permissions)?;
72 true
73 } else {
74 false
75 };
76
77 Ok(updated)
78}
79
80#[cfg(any(target_os = "macos", target_os = "openbsd"))]
81pub fn _get_local_addrs() -> Result<impl Iterator<Item = std::net::IpAddr>, io::Error> {
82 use std::net::Ipv4Addr;
83
84 use nix::net::if_::InterfaceFlags;
85
86 let addrs = nix::ifaddrs::getifaddrs()?
87 .filter(|addr| {
88 addr.flags.contains(InterfaceFlags::IFF_UP)
89 && !addr.flags.intersects(
90 InterfaceFlags::IFF_LOOPBACK
91 | InterfaceFlags::IFF_POINTOPOINT
92 | InterfaceFlags::IFF_PROMISC,
93 )
94 })
95 .filter_map(|interface_addr| {
96 interface_addr.address.and_then(|addr| {
97 if let Some(sockaddr_in) = addr.as_sockaddr_in() {
98 Some(IpAddr::V4(Ipv4Addr::from(sockaddr_in.ip())))
99 } else {
100 addr.as_sockaddr_in6()
101 .map(|sockaddr_in6| IpAddr::V6(sockaddr_in6.ip()))
102 }
103 })
104 });
105
106 Ok(addrs)
107}
108
109#[cfg(target_os = "linux")]
110pub use netlink::get_local_addrs as _get_local_addrs;
111
112pub fn get_local_addrs() -> Result<impl Iterator<Item = std::net::IpAddr>, io::Error> {
113 fn is_unicast_global(ip: &Ipv6Addr) -> bool {
115 !((ip.segments()[0] & 0xff00) == 0xff00 || ip.is_loopback()
117 || ip.is_unspecified()
118 || ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) || (ip.segments()[0] & 0xffc0) == 0xfe80 || (ip.segments()[0] & 0xfe00) == 0xfc00) }
122
123 Ok(_get_local_addrs()?
124 .filter(|ip| {
125 ip.is_ipv4()
126 || matches!(ip,
127 IpAddr::V6(v6) if is_unicast_global(v6))
128 })
129 .take(10))
130}
131
132pub trait IpNetExt {
133 fn is_assignable(&self, ip: &IpAddr) -> bool;
134}
135
136impl IpNetExt for IpNet {
137 fn is_assignable(&self, ip: &IpAddr) -> bool {
138 self.contains(ip)
139 && match self {
140 IpNet::V4(_) => {
141 self.prefix_len() >= 31 || (ip != &self.network() && ip != &self.broadcast())
142 },
143 IpNet::V6(_) => self.prefix_len() >= 127 || ip != &self.network(),
144 }
145 }
146}
147
148pub fn update_hosts_file(
149 interface: &InterfaceName,
150 hosts_path: &Path,
151 peers: impl IntoIterator<Item = impl AsRef<Peer>>,
152) -> Result<(), WrappedIoError> {
153 let mut hosts_builder = HostsBuilder::new(format!("innernet {interface}"));
154 for peer in peers {
155 let peer = peer.as_ref();
156 hosts_builder.add_hostname(
157 peer.contents.ip,
158 format!("{}.{}.wg", peer.contents.name, interface),
159 );
160 }
161 match hosts_builder.write_to(hosts_path).with_path(hosts_path) {
162 Ok(has_written) if has_written => {
163 log::info!(
164 "updated {} with the latest peers.",
165 hosts_path.to_string_lossy().yellow()
166 )
167 },
168 Ok(_) => {},
169 Err(e) => log::warn!("failed to update hosts ({})", e),
170 };
171
172 Ok(())
173}