Skip to main content

innernet_shared/
interface_config.rs

1use crate::{chmod, ensure_dirs_exist, Endpoint, Error, IoErrorContext, Peer, WrappedIoError};
2use indoc::writedoc;
3use ipnet::IpNet;
4use serde::{Deserialize, Serialize};
5use std::{
6    collections::BTreeMap,
7    fs::{File, OpenOptions},
8    io::{self, Write},
9    net::{IpAddr, SocketAddr},
10    path::{Path, PathBuf},
11};
12use wireguard_control::{InterfaceName, KeyPair};
13
14/// This struct contains everything necessary to establish an innernet connection: information about
15/// a local innernet interface and a remote innernet server.
16#[derive(Clone, Deserialize, Serialize, Debug)]
17#[serde(rename_all = "kebab-case")]
18pub struct InterfaceConfig {
19    /// The information to bring up the interface.
20    pub interface: InterfaceInfo,
21
22    /// The necessary contact information for the server.
23    pub server: ServerInfo,
24
25    /// A configurable map of peer IP addresses to Endpoints which should
26    /// be used as the WireGuard endpoint for that peer.
27    #[serde(default)]
28    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
29    peer_endpoint_overrides: BTreeMap<IpAddr, Endpoint>,
30}
31
32#[derive(Clone, Deserialize, Serialize, Debug)]
33#[serde(rename_all = "kebab-case")]
34pub struct InterfaceInfo {
35    /// The interface name (i.e. "tonari")
36    pub network_name: String,
37
38    /// The invited peer's internal IP address that's been allocated to it, inside
39    /// the entire network's CIDR prefix.
40    pub address: IpNet,
41
42    /// WireGuard private key (base64)
43    pub private_key: String,
44
45    /// The local listen port. A random port will be used if `None`.
46    pub listen_port: Option<u16>,
47}
48
49impl InterfaceInfo {
50    pub fn new(network_name: &InterfaceName, keypair: &KeyPair, address: IpNet) -> Self {
51        Self {
52            network_name: network_name.to_string(),
53            private_key: keypair.private.to_base64(),
54            address,
55            listen_port: None,
56        }
57    }
58}
59
60#[derive(Clone, Deserialize, Serialize, Debug)]
61#[serde(rename_all = "kebab-case")]
62pub struct ServerInfo {
63    /// The server's WireGuard public key
64    pub public_key: String,
65
66    /// The external internet endpoint to reach the server.
67    pub external_endpoint: Endpoint,
68
69    /// An internal endpoint in the WireGuard network that hosts the coordination API.
70    pub internal_endpoint: SocketAddr,
71}
72
73impl ServerInfo {
74    pub fn new(server_peer: &Peer, internal_endpoint: SocketAddr) -> Self {
75        Self {
76            external_endpoint: server_peer
77                .endpoint
78                .clone()
79                .expect("The innernet server should have a WireGuard endpoint"),
80            internal_endpoint,
81            public_key: server_peer.public_key.clone(),
82        }
83    }
84}
85
86impl InterfaceConfig {
87    fn new(interface: InterfaceInfo, server: ServerInfo) -> Self {
88        InterfaceConfig {
89            interface,
90            server,
91            peer_endpoint_overrides: BTreeMap::new(),
92        }
93    }
94
95    /// Save a new config file, failing if it already exists.
96    pub fn save_new(&self, path: impl AsRef<Path>, mode: u32) -> Result<(), WrappedIoError> {
97        let path = path.as_ref();
98        let mut file = OpenOptions::new()
99            .create_new(true)
100            .write(true)
101            .open(path)
102            .with_path(path)?;
103
104        chmod(&file, mode).with_path(path)?;
105
106        file.write_all(self.as_toml().as_bytes()).with_path(path)?;
107
108        Ok(())
109    }
110
111    /// Overwrites the config file if it already exists.
112    pub fn save(&self, config_dir: &Path, interface: &InterfaceName) -> Result<PathBuf, Error> {
113        let path = Self::build_config_file_path(config_dir, interface)?;
114        File::create(&path)
115            .with_path(&path)?
116            .write_all(self.as_toml().as_bytes())?;
117
118        Ok(path)
119    }
120
121    fn as_toml(&self) -> String {
122        toml::to_string(self).unwrap()
123    }
124
125    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
126        Ok(toml::from_str(
127            &std::fs::read_to_string(&path).with_path(path)?,
128        )?)
129    }
130
131    pub fn from_interface(config_dir: &Path, interface: &InterfaceName) -> Result<Self, Error> {
132        let path = Self::build_config_file_path(config_dir, interface)?;
133        crate::warn_on_dangerous_mode(&path).with_path(&path)?;
134        Self::from_file(path)
135    }
136
137    pub fn get_path(config_dir: &Path, interface: &InterfaceName) -> PathBuf {
138        config_dir
139            .join(interface.to_string())
140            .with_extension("conf")
141    }
142
143    pub fn build_config_file_path(
144        config_dir: &Path,
145        interface: &InterfaceName,
146    ) -> Result<PathBuf, WrappedIoError> {
147        ensure_dirs_exist(&[config_dir])?;
148        Ok(Self::get_path(config_dir, interface))
149    }
150
151    pub fn peer_endpoint_overrides(&self) -> &BTreeMap<IpAddr, Endpoint> {
152        &self.peer_endpoint_overrides
153    }
154
155    pub fn set_endpoint_override_for_peer(&mut self, peer_ip: IpAddr, endpoint: Endpoint) {
156        self.peer_endpoint_overrides.insert(peer_ip, endpoint);
157    }
158
159    pub fn unset_endpoint_override_for_peer(&mut self, peer_ip: IpAddr) {
160        self.peer_endpoint_overrides.remove(&peer_ip);
161    }
162}
163
164impl InterfaceInfo {
165    pub fn public_key(&self) -> Result<String, Error> {
166        Ok(wireguard_control::Key::from_base64(&self.private_key)?
167            .get_public()
168            .to_base64())
169    }
170}
171
172#[must_use]
173pub struct PeerInvitation {
174    interface_config: InterfaceConfig,
175}
176
177impl PeerInvitation {
178    pub fn new(interface: InterfaceInfo, server: ServerInfo) -> Self {
179        Self {
180            interface_config: InterfaceConfig::new(interface, server),
181        }
182    }
183
184    /// Save a new invitation file, failing if it already exists.
185    pub fn save_new(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
186        let mut file = OpenOptions::new()
187            .read(true)
188            .write(true)
189            .create_new(true)
190            .open(path)?;
191
192        writedoc!(
193            file,
194            r"
195                    # This is an invitation file to an innernet network.
196                    #
197                    # To join, you must install innernet.
198                    # See https://github.com/tonarino/innernet for instructions.
199                    #
200                    # If you have innernet, just run:
201                    #
202                    #   innernet install <this file>
203                    #
204                    # Don't edit the contents below unless you love chaos and dysfunction.
205                "
206        )?;
207
208        file.write_all(self.interface_config.as_toml().as_bytes())?;
209
210        Ok(())
211    }
212}