Skip to main content

innernet_shared/
lib.rs

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