use crate::{chmod, ensure_dirs_exist, Endpoint, Error, IoErrorContext, Peer, WrappedIoError};
use indoc::writedoc;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
fs::{File, OpenOptions},
io::{self, Write},
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
};
use wireguard_control::{InterfaceName, KeyPair};
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct InterfaceConfig {
pub interface: InterfaceInfo,
pub server: ServerInfo,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
peer_endpoint_overrides: BTreeMap<IpAddr, Endpoint>,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct InterfaceInfo {
pub network_name: String,
pub address: IpNet,
pub private_key: String,
pub listen_port: Option<u16>,
}
impl InterfaceInfo {
pub fn new(network_name: &InterfaceName, keypair: &KeyPair, address: IpNet) -> Self {
Self {
network_name: network_name.to_string(),
private_key: keypair.private.to_base64(),
address,
listen_port: None,
}
}
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct ServerInfo {
pub public_key: String,
pub external_endpoint: Endpoint,
pub internal_endpoint: SocketAddr,
}
impl ServerInfo {
pub fn new(server_peer: &Peer, internal_endpoint: SocketAddr) -> Self {
Self {
external_endpoint: server_peer
.endpoint
.clone()
.expect("The innernet server should have a WireGuard endpoint"),
internal_endpoint,
public_key: server_peer.public_key.clone(),
}
}
}
impl InterfaceConfig {
fn new(interface: InterfaceInfo, server: ServerInfo) -> Self {
InterfaceConfig {
interface,
server,
peer_endpoint_overrides: BTreeMap::new(),
}
}
pub fn save_new(&self, path: impl AsRef<Path>, mode: u32) -> Result<(), WrappedIoError> {
let path = path.as_ref();
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.with_path(path)?;
chmod(&file, mode).with_path(path)?;
file.write_all(self.as_toml().as_bytes()).with_path(path)?;
Ok(())
}
pub fn save(&self, config_dir: &Path, interface: &InterfaceName) -> Result<PathBuf, Error> {
let path = Self::build_config_file_path(config_dir, interface)?;
File::create(&path)
.with_path(&path)?
.write_all(self.as_toml().as_bytes())?;
Ok(path)
}
fn as_toml(&self) -> String {
toml::to_string(self).unwrap()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
Ok(toml::from_str(
&std::fs::read_to_string(&path).with_path(path)?,
)?)
}
pub fn from_interface(config_dir: &Path, interface: &InterfaceName) -> Result<Self, Error> {
let path = Self::build_config_file_path(config_dir, interface)?;
crate::warn_on_dangerous_mode(&path).with_path(&path)?;
Self::from_file(path)
}
pub fn get_path(config_dir: &Path, interface: &InterfaceName) -> PathBuf {
config_dir
.join(interface.to_string())
.with_extension("conf")
}
pub fn build_config_file_path(
config_dir: &Path,
interface: &InterfaceName,
) -> Result<PathBuf, WrappedIoError> {
ensure_dirs_exist(&[config_dir])?;
Ok(Self::get_path(config_dir, interface))
}
pub fn peer_endpoint_overrides(&self) -> &BTreeMap<IpAddr, Endpoint> {
&self.peer_endpoint_overrides
}
pub fn set_endpoint_override_for_peer(&mut self, peer_ip: IpAddr, endpoint: Endpoint) {
self.peer_endpoint_overrides.insert(peer_ip, endpoint);
}
pub fn unset_endpoint_override_for_peer(&mut self, peer_ip: IpAddr) {
self.peer_endpoint_overrides.remove(&peer_ip);
}
}
impl InterfaceInfo {
pub fn public_key(&self) -> Result<String, Error> {
Ok(wireguard_control::Key::from_base64(&self.private_key)?
.get_public()
.to_base64())
}
}
#[must_use]
pub struct PeerInvitation {
interface_config: InterfaceConfig,
}
impl PeerInvitation {
pub fn new(interface: InterfaceInfo, server: ServerInfo) -> Self {
Self {
interface_config: InterfaceConfig::new(interface, server),
}
}
pub fn save_new(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(path)?;
writedoc!(
file,
r"
# This is an invitation file to an innernet network.
#
# To join, you must install innernet.
# See https://github.com/tonarino/innernet for instructions.
#
# If you have innernet, just run:
#
# innernet install <this file>
#
# Don't edit the contents below unless you love chaos and dysfunction.
"
)?;
file.write_all(self.interface_config.as_toml().as_bytes())?;
Ok(())
}
}