innernet_shared/
lib.rs

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
62/// Updates the permissions of a file or directory. Returns `Ok(true)` if
63/// permissions had to be changed, `Ok(false)` if permissions were already
64/// correct.
65pub 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    // TODO(jake): this is temporary pending the stabilization of rust-lang/rust#27709
114    fn is_unicast_global(ip: &Ipv6Addr) -> bool {
115        !((ip.segments()[0] & 0xff00) == 0xff00 // multicast
116            || ip.is_loopback()
117            || ip.is_unspecified()
118            || ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // documentation
119            || (ip.segments()[0] & 0xffc0) == 0xfe80 // unicast link local
120            || (ip.segments()[0] & 0xfe00) == 0xfc00) // unicast local
121    }
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}