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