Repository: tonarino/innernet (language: rust)

Issue:
Configurable `SERVER_DATABASE_DIR` (or all directories in general)
I'm using `innernet-server` on OpenWRT, and it works great. However, by default, OpenWRT uses `tmpfs` for `/var` directory, and everything gets wiped out during restarts or upgrades. That includes innernet sqlite database.

Currently I'm using custom compilation, with paths altered, but is it possible, to make those directories configurable (for example via commandline flag)?


Discussion / hints:
Yes! I think there were a couple of other cases (there were a few folks that packaged it for the reMarkable OS as well, IIRC, that could use a config like this) that justify this.

Thanks for the reminder and for opening the issue.

Fix patch:
diff --git a/client/src/data_store.rs b/client/src/data_store.rs
index a817d822..945fd34e 100644
--- a/client/src/data_store.rs
+++ b/client/src/data_store.rs
@@ -1,9 +1,7 @@
 use crate::Error;
 use anyhow::bail;
 use serde::{Deserialize, Serialize};
-use shared::{
-    chmod, ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_DIR,
-};
+use shared::{chmod, ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError};
 use std::{
     fs::{File, OpenOptions},
     io::{self, Read, Seek, SeekFrom, Write},
@@ -55,23 +53,28 @@ impl DataStore {
         Ok(Self { file, contents })
     }
 
-    pub fn get_path(interface: &InterfaceName) -> PathBuf {
-        CLIENT_DATA_DIR
-            .join(interface.to_string())
-            .with_extension("json")
+    pub fn get_path(data_dir: &Path, interface: &InterfaceName) -> PathBuf {
+        data_dir.join(interface.to_string()).with_extension("json")
     }
 
-    fn _open(interface: &InterfaceName, create: bool) -> Result<Self, WrappedIoError> {
-        ensure_dirs_exist(&[*CLIENT_DATA_DIR])?;
-        Self::open_with_path(Self::get_path(interface), create)
+    fn _open(
+        data_dir: &Path,
+        interface: &InterfaceName,
+        create: bool,
+    ) -> Result<Self, WrappedIoError> {
+        ensure_dirs_exist(&[data_dir])?;
+        Self::open_with_path(Self::get_path(data_dir, interface), create)
     }
 
-    pub fn open(interface: &InterfaceName) -> Result<Self, WrappedIoError> {
-        Self::_open(interface, false)
+    pub fn open(data_dir: &Path, interface: &InterfaceName) -> Result<Self, WrappedIoError> {
+        Self::_open(data_dir, interface, false)
     }
 
-    pub fn open_or_create(interface: &InterfaceName) -> Result<Self, WrappedIoError> {
-        Self::_open(interface, true)
+    pub fn open_or_create(
+        data_dir: &Path,
+        interface: &InterfaceName,
+    ) -> Result<Self, WrappedIoError> {
+        Self::_open(data_dir, interface, true)
     }
 
     pub fn peers(&self) -> &[Peer] {
diff --git a/client/src/main.rs b/client/src/main.rs
index 6b8ee460..e459194c 100644
--- a/client/src/main.rs
+++ b/client/src/main.rs
@@ -10,7 +10,7 @@ use shared::{
     wg::{DeviceExt, PeerInfoExt},
     AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree,
     DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NatOpts,
-    NetworkOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, CLIENT_CONFIG_DIR,
+    NetworkOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError,
     REDEEM_TRANSITION_WAIT,
 };
 use std::{
@@ -44,7 +44,7 @@ macro_rules! println_pad {
     }
 }
 
-#[derive(Debug, StructOpt)]
+#[derive(Clone, Debug, StructOpt)]
 #[structopt(name = "innernet", about, global_settings(&[AppSettings::ColoredHelp, AppSettings::DeriveDisplayOrder, AppSettings::VersionlessSubcommands, AppSettings::UnifiedHelpMessage]))]
 struct Opts {
     #[structopt(subcommand)]
@@ -54,11 +54,17 @@ struct Opts {
     #[structopt(short, long, parse(from_occurrences))]
     verbose: u64,
 
+    #[structopt(short, long, default_value = "/etc/innernet")]
+    config_dir: PathBuf,
+
+    #[structopt(short, long, default_value = "/var/lib/innernet")]
+    data_dir: PathBuf,
+
     #[structopt(flatten)]
     network: NetworkOpts,
 }
 
-#[derive(Debug, StructOpt)]
+#[derive(Clone, Debug, StructOpt)]
 struct HostsOpt {
     /// The path to write hosts to.
     #[structopt(long = "hosts-path", default_value = "/etc/hosts")]
@@ -75,7 +81,7 @@ impl From<HostsOpt> for Option<PathBuf> {
     }
 }
 
-#[derive(Debug, StructOpt)]
+#[derive(Clone, Debug, StructOpt)]
 enum Command {
     /// Install a new innernet config.
     #[structopt(alias = "redeem")]
@@ -87,7 +93,7 @@ enum Command {
         hosts: HostsOpt,
 
         #[structopt(flatten)]
-        opts: InstallOpts,
+        install_opts: InstallOpts,
 
         #[structopt(flatten)]
         nat: NatOpts,
@@ -155,7 +161,7 @@ enum Command {
         interface: Interface,
 
         #[structopt(flatten)]
-        opts: AddPeerOpts,
+        sub_opts: AddPeerOpts,
     },
 
     /// Rename a peer.
@@ -168,7 +174,7 @@ enum Command {
         interface: Interface,
 
         #[structopt(flatten)]
-        opts: RenamePeerOpts,
+        sub_opts: RenamePeerOpts,
     },
 
     /// Add a new CIDR.
@@ -176,7 +182,7 @@ enum Command {
         interface: Interface,
 
         #[structopt(flatten)]
-        opts: AddCidrOpts,
+        sub_opts: AddCidrOpts,
     },
 
     /// Delete a CIDR.
@@ -184,7 +190,7 @@ enum Command {
         interface: Interface,
 
         #[structopt(flatten)]
-        opts: DeleteCidrOpts,
+        sub_opts: DeleteCidrOpts,
     },
 
     /// List CIDRs.
@@ -207,7 +213,7 @@ enum Command {
         interface: Interface,
 
         #[structopt(flatten)]
-        opts: AddAssociationOpts,
+        sub_opts: AddAssociationOpts,
     },
 
     /// Delete an association between CIDRs.
@@ -279,18 +285,18 @@ fn update_hosts_file(
 }
 
 fn install(
+    opts: &Opts,
     invite: &Path,
     hosts_file: Option<PathBuf>,
-    opts: InstallOpts,
-    network: NetworkOpts,
+    install_opts: InstallOpts,
     nat: &NatOpts,
 ) -> Result<(), Error> {
-    shared::ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?;
+    shared::ensure_dirs_exist(&[&opts.config_dir])?;
     let config = InterfaceConfig::from_file(invite)?;
 
-    let iface = if opts.default_name {
+    let iface = if install_opts.default_name {
         config.interface.network_name.clone()
-    } else if let Some(ref iface) = opts.name {
+    } else if let Some(ref iface) = install_opts.name {
         iface.clone()
     } else {
         Input::with_theme(&*prompts::THEME)
@@ -299,7 +305,7 @@ fn install(
             .interact()?
     };
 
-    let target_conf = CLIENT_CONFIG_DIR.join(&iface).with_extension("conf");
+    let target_conf = opts.config_dir.join(&iface).with_extension("conf");
     if target_conf.exists() {
         bail!(
             "An existing innernet network with the name \"{}\" already exists.",
@@ -307,7 +313,7 @@ fn install(
         );
     }
     let iface = iface.parse()?;
-    if Device::list(network.backend)
+    if Device::list(opts.network.backend)
         .iter()
         .flatten()
         .any(|name| name == &iface)
@@ -318,10 +324,10 @@ fn install(
         );
     }
 
-    redeem_invite(&iface, config, target_conf, network).map_err(|e| {
+    redeem_invite(&iface, config, target_conf, opts.network).map_err(|e| {
         log::error!("failed to start the interface: {}.", e);
         log::info!("bringing down the interface.");
-        if let Err(e) = wg::down(&iface, network.backend) {
+        if let Err(e) = wg::down(&iface, opts.network.backend) {
             log::warn!("failed to bring down interface: {}.", e.to_string());
         };
         log::error!("Failed to redeem invite. Now's a good time to make sure the server is started and accessible!");
@@ -330,15 +336,7 @@ fn install(
 
     let mut fetch_success = false;
     for _ in 0..3 {
-        if fetch(
-            &iface,
-            true,
-            hosts_file.clone(),
-            network,
-            nat,
-        )
-        .is_ok()
-        {
+        if fetch(&iface, opts, true, hosts_file.clone(), nat).is_ok() {
             fetch_success = true;
             break;
         }
@@ -350,7 +348,7 @@ fn install(
         );
     }
 
-    if opts.delete_invite
+    if install_opts.delete_invite
         || Confirm::with_theme(&*prompts::THEME)
             .wait_for_newline(true)
             .with_prompt(&format!(
@@ -472,20 +470,20 @@ fn redeem_invite(
         .set_private_key(keypair.private)
         .apply(iface, network.backend)
         .with_str(iface.to_string())?;
-    thread::sleep(*REDEEM_TRANSITION_WAIT);
+    thread::sleep(REDEEM_TRANSITION_WAIT);
 
     Ok(())
 }
 
 fn up(
     interface: &InterfaceName,
+    opts: &Opts,
     loop_interval: Option<Duration>,
     hosts_path: Option<PathBuf>,
-    routing: NetworkOpts,
     nat: &NatOpts,
 ) -> Result<(), Error> {
     loop {
-        fetch(interface, true, hosts_path.clone(), routing, nat)?;
+        fetch(interface, opts, true, hosts_path.clone(), nat)?;
         match loop_interval {
             Some(interval) => thread::sleep(interval),
             None => break,
@@ -497,13 +495,13 @@ fn up(
 
 fn fetch(
     interface: &InterfaceName,
+    opts: &Opts,
     bring_up_interface: bool,
     hosts_path: Option<PathBuf>,
-    network: NetworkOpts,
     nat: &NatOpts,
 ) -> Result<(), Error> {
-    let config = InterfaceConfig::from_interface(interface)?;
-    let interface_up = match Device::list(network.backend) {
+    let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
+    let interface_up = match Device::list(opts.network.backend) {
         Ok(interfaces) => interfaces.iter().any(|name| name == interface),
         _ => false,
     };
@@ -532,17 +530,17 @@ fn fetch(
                 config.server.internal_endpoint.ip(),
                 resolved_endpoint,
             )),
-            network,
+            opts.network,
         )
         .with_str(interface.to_string())?;
     }
 
     log::info!("fetching state from server.");
-    let mut store = DataStore::open_or_create(interface)?;
+    let mut store = DataStore::open_or_create(&opts.data_dir, interface)?;
     let api = Api::new(&config.server);
     let State { peers, cidrs } = api.http("GET", "/user/state")?;
 
-    let device = Device::get(interface, network.backend)?;
+    let device = Device::get(interface, opts.network.backend)?;
     let modifications = device.diff(&peers);
 
     let updates = modifications
@@ -555,7 +553,7 @@ fn fetch(
     if !updates.is_empty() || !interface_up {
         DeviceUpdate::new()
             .add_peers(&updates)
-            .apply(interface, network.backend)
+            .apply(interface, opts.network.backend)
             .with_str(interface.to_string())?;
 
         if let Some(path) = hosts_path {
@@ -594,7 +592,7 @@ fn fetch(
     if nat.no_nat_traversal {
         log::debug!("NAT traversal explicitly disabled, not attempting.");
     } else {
-        let mut nat_traverse = NatTraverse::new(interface, network.backend, &modifications)?;
+        let mut nat_traverse = NatTraverse::new(interface, opts.network.backend, &modifications)?;
 
         // Give time for handshakes with recently changed endpoints to complete before attempting traversal.
         if !nat_traverse.is_finished() {
@@ -615,7 +613,7 @@ fn fetch(
     Ok(())
 }
 
-fn uninstall(interface: &InterfaceName, network: NetworkOpts) -> Result<(), Error> {
+fn uninstall(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> {
     if Confirm::with_theme(&*prompts::THEME)
         .with_prompt(&format!(
             "Permanently delete network \"{}\"?",
@@ -626,9 +624,9 @@ fn uninstall(interface: &InterfaceName, network: NetworkOpts) -> Result<(), Erro
         .interact()?
     {
         log::info!("bringing down interface (if up).");
-        wg::down(interface, network.backend).ok();
-        let config = InterfaceConfig::get_path(interface);
-        let data = DataStore::get_path(interface);
+        wg::down(interface, opts.network.backend).ok();
+        let config = InterfaceConfig::get_path(&opts.config_dir, interface);
+        let data = DataStore::get_path(&opts.data_dir, interface);
         std::fs::remove_file(&config)
             .with_path(&config)
             .map_err(|e| log::warn!("{}", e.to_string().yellow()))
@@ -645,13 +643,14 @@ fn uninstall(interface: &InterfaceName, network: NetworkOpts) -> Result<(), Erro
     Ok(())
 }
 
-fn add_cidr(interface: &InterfaceName, opts: AddCidrOpts) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn add_cidr(interface: &InterfaceName, opts: &Opts, sub_opts: AddCidrOpts) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     log::info!("Fetching CIDRs");
     let api = Api::new(&server);
     let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
 
-    if let Some(cidr_request) = prompts::add_cidr(&cidrs, &opts)? {
+    if let Some(cidr_request) = prompts::add_cidr(&cidrs, &sub_opts)? {
         log::info!("Creating CIDR...");
         let cidr: Cidr = api.http_form("POST", "/admin/cidrs", cidr_request)?;
 
@@ -673,14 +672,19 @@ fn add_cidr(interface: &InterfaceName, opts: AddCidrOpts) -> Result<(), Error> {
     Ok(())
 }
 
-fn delete_cidr(interface: &InterfaceName, opts: DeleteCidrOpts) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn delete_cidr(
+    interface: &InterfaceName,
+    opts: &Opts,
+    sub_opts: DeleteCidrOpts,
+) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     println!("Fetching eligible CIDRs");
     let api = Api::new(&server);
     let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
     let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
 
-    let cidr_id = prompts::delete_cidr(&cidrs, &peers, &opts)?;
+    let cidr_id = prompts::delete_cidr(&cidrs, &peers, &sub_opts)?;
 
     println!("Deleting CIDR...");
     let _ = api.http("DELETE", &*format!("/admin/cidrs/{}", cidr_id))?;
@@ -690,8 +694,8 @@ fn delete_cidr(interface: &InterfaceName, opts: DeleteCidrOpts) -> Result<(), Er
     Ok(())
 }
 
-fn list_cidrs(interface: &InterfaceName, tree: bool) -> Result<(), Error> {
-    let data_store = DataStore::open(interface)?;
+fn list_cidrs(interface: &InterfaceName, opts: &Opts, tree: bool) -> Result<(), Error> {
+    let data_store = DataStore::open(&opts.data_dir, interface)?;
     if tree {
         let cidr_tree = CidrTree::new(data_store.cidrs());
         colored::control::set_override(false);
@@ -705,8 +709,9 @@ fn list_cidrs(interface: &InterfaceName, tree: bool) -> Result<(), Error> {
     Ok(())
 }
 
-fn add_peer(interface: &InterfaceName, opts: AddPeerOpts) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn add_peer(interface: &InterfaceName, opts: &Opts, sub_opts: AddPeerOpts) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching CIDRs");
@@ -715,7 +720,7 @@ fn add_peer(interface: &InterfaceName, opts: AddPeerOpts) -> Result<(), Error> {
     let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
     let cidr_tree = CidrTree::new(&cidrs[..]);
 
-    if let Some(result) = prompts::add_peer(&peers, &cidr_tree, &opts)? {
+    if let Some(result) = prompts::add_peer(&peers, &cidr_tree, &sub_opts)? {
         let (peer_request, keypair, target_path, mut target_file) = result;
         log::info!("Creating peer...");
         let peer: Peer = api.http_form("POST", "/admin/peers", peer_request)?;
@@ -736,14 +741,19 @@ fn add_peer(interface: &InterfaceName, opts: AddPeerOpts) -> Result<(), Error> {
     Ok(())
 }
 
-fn rename_peer(interface: &InterfaceName, opts: RenamePeerOpts) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn rename_peer(
+    interface: &InterfaceName,
+    opts: &Opts,
+    sub_opts: RenamePeerOpts,
+) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching peers");
     let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
 
-    if let Some((peer_request, old_name)) = prompts::rename_peer(&peers, &opts)? {
+    if let Some((peer_request, old_name)) = prompts::rename_peer(&peers, &sub_opts)? {
         log::info!("Renaming peer...");
 
         let id = peers
@@ -762,8 +772,13 @@ fn rename_peer(interface: &InterfaceName, opts: RenamePeerOpts) -> Result<(), Er
     Ok(())
 }
 
-fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn enable_or_disable_peer(
+    interface: &InterfaceName,
+    opts: &Opts,
+    enable: bool,
+) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching peers.");
@@ -780,14 +795,19 @@ fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(),
     Ok(())
 }
 
-fn add_association(interface: &InterfaceName, opts: AddAssociationOpts) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn add_association(
+    interface: &InterfaceName,
+    opts: &Opts,
+    sub_opts: AddAssociationOpts,
+) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching CIDRs");
     let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
 
-    let association = if let (Some(ref cidr1), Some(ref cidr2)) = (opts.cidr1, opts.cidr2) {
+    let association = if let (Some(ref cidr1), Some(ref cidr2)) = (sub_opts.cidr1, sub_opts.cidr2) {
         let cidr1 = cidrs
             .iter()
             .find(|c| &c.name == cidr1)
@@ -816,8 +836,9 @@ fn add_association(interface: &InterfaceName, opts: AddAssociationOpts) -> Resul
     Ok(())
 }
 
-fn delete_association(interface: &InterfaceName) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn delete_association(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching CIDRs");
@@ -834,8 +855,9 @@ fn delete_association(interface: &InterfaceName) -> Result<(), Error> {
     Ok(())
 }
 
-fn list_associations(interface: &InterfaceName) -> Result<(), Error> {
-    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+fn list_associations(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } =
+        InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let api = Api::new(&server);
 
     log::info!("Fetching CIDRs");
@@ -867,18 +889,18 @@ fn list_associations(interface: &InterfaceName) -> Result<(), Error> {
 
 fn set_listen_port(
     interface: &InterfaceName,
+    opts: &Opts,
     unset: bool,
-    network: NetworkOpts,
 ) -> Result<Option<u16>, Error> {
-    let mut config = InterfaceConfig::from_interface(interface)?;
+    let mut config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
 
     let listen_port = prompts::set_listen_port(&config.interface, unset)?;
     if let Some(listen_port) = listen_port {
-        wg::set_listen_port(interface, listen_port, network.backend)?;
+        wg::set_listen_port(interface, listen_port, opts.network.backend)?;
         log::info!("the interface is updated");
 
         config.interface.listen_port = listen_port;
-        config.write_to_interface(interface)?;
+        config.write_to_interface(&opts.config_dir, interface)?;
         log::info!("the config file is updated");
     } else {
         log::info!("exiting without updating the listen port.");
@@ -887,12 +909,8 @@ fn set_listen_port(
     Ok(listen_port.flatten())
 }
 
-fn override_endpoint(
-    interface: &InterfaceName,
-    unset: bool,
-    network: NetworkOpts,
-) -> Result<(), Error> {
-    let config = InterfaceConfig::from_interface(interface)?;
+fn override_endpoint(interface: &InterfaceName, opts: &Opts, unset: bool) -> Result<(), Error> {
+    let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
     let endpoint_contents = if unset {
         prompts::unset_override_endpoint()?.then(|| EndpointContents::Unset)
     } else {
@@ -903,7 +921,7 @@ fn override_endpoint(
                 "{}: you need to set a listen port for your interface first.",
                 "note".bold().yellow()
             );
-            set_listen_port(interface, unset, network)?
+            set_listen_port(interface, opts, unset)?
         };
         let endpoint = if let Some(port) = listen_port {
             prompts::override_endpoint(port)?
@@ -923,23 +941,19 @@ fn override_endpoint(
     Ok(())
 }
 
-fn show(
-    short: bool,
-    tree: bool,
-    interface: Option<Interface>,
-    network: NetworkOpts,
-) -> Result<(), Error> {
+fn show(opts: &Opts, short: bool, tree: bool, interface: Option<Interface>) -> Result<(), Error> {
     let interfaces = interface.map_or_else(
-        || Device::list(network.backend),
+        || Device::list(opts.network.backend),
         |interface| Ok(vec![*interface]),
     )?;
 
     let devices = interfaces
         .into_iter()
         .filter_map(|name| {
-            match DataStore::open(&name) {
+            match DataStore::open(&opts.data_dir, &name) {
                 Ok(store) => {
-                    let device = Device::get(&name, network.backend).with_str(name.as_str_lossy());
+                    let device =
+                        Device::get(&name, opts.network.backend).with_str(name.as_str_lossy());
                     Some(device.map(|device| (device, store)))
                 },
                 // Skip WireGuard interfaces that aren't managed by innernet.
@@ -1099,24 +1113,24 @@ fn print_peer(peer: &PeerState, short: bool, level: usize) {
 }
 
 fn main() {
-    let opt = Opts::from_args();
-    util::init_logger(opt.verbose);
+    let opts = Opts::from_args();
+    util::init_logger(opts.verbose);
 
-    if let Err(e) = run(opt) {
+    if let Err(e) = run(&opts) {
         println!();
         log::error!("{}\n", e);
         if let Some(e) = e.downcast_ref::<WrappedIoError>() {
-            util::permissions_helptext(e);
+            util::permissions_helptext(&opts.config_dir, &opts.data_dir, e);
         }
         if let Some(e) = e.downcast_ref::<io::Error>() {
-            util::permissions_helptext(e);
+            util::permissions_helptext(&opts.config_dir, &opts.data_dir, e);
         }
         std::process::exit(1);
     }
 }
 
-fn run(opt: Opts) -> Result<(), Error> {
-    let command = opt.command.unwrap_or(Command::Show {
+fn run(opts: &Opts) -> Result<(), Error> {
+    let command = opts.command.clone().unwrap_or(Command::Show {
         short: false,
         tree: false,
         interface: None,
@@ -1126,19 +1140,19 @@ fn run(opt: Opts) -> Result<(), Error> {
         Command::Install {
             invite,
             hosts,
-            opts,
+            install_opts,
             nat,
-        } => install(&invite, hosts.into(), opts, opt.network, &nat)?,
+        } => install(opts, &invite, hosts.into(), install_opts, &nat)?,
         Command::Show {
             short,
             tree,
             interface,
-        } => show(short, tree, interface, opt.network)?,
+        } => show(opts, short, tree, interface)?,
         Command::Fetch {
             interface,
             hosts,
             nat,
-        } => fetch(&interface, false, hosts.into(), opt.network, &nat)?,
+        } => fetch(&interface, opts, false, hosts.into(), &nat)?,
         Command::Up {
             interface,
             daemon,
@@ -1147,28 +1161,43 @@ fn run(opt: Opts) -> Result<(), Error> {
             interval,
         } => up(
             &interface,
+            opts,
             daemon.then(|| Duration::from_secs(interval)),
             hosts.into(),
-            opt.network,
             &nat,
         )?,
-        Command::Down { interface } => wg::down(&interface, opt.network.backend)?,
-        Command::Uninstall { interface } => uninstall(&interface, opt.network)?,
-        Command::AddPeer { interface, opts } => add_peer(&interface, opts)?,
-        Command::RenamePeer { interface, opts } => rename_peer(&interface, opts)?,
-        Command::AddCidr { interface, opts } => add_cidr(&interface, opts)?,
-        Command::DeleteCidr { interface, opts } => delete_cidr(&interface, opts)?,
-        Command::ListCidrs { interface, tree } => list_cidrs(&interface, tree)?,
-        Command::DisablePeer { interface } => enable_or_disable_peer(&interface, false)?,
-        Command::EnablePeer { interface } => enable_or_disable_peer(&interface, true)?,
-        Command::AddAssociation { interface, opts } => add_association(&interface, opts)?,
-        Command::DeleteAssociation { interface } => delete_association(&interface)?,
-        Command::ListAssociations { interface } => list_associations(&interface)?,
+        Command::Down { interface } => wg::down(&interface, opts.network.backend)?,
+        Command::Uninstall { interface } => uninstall(&interface, opts)?,
+        Command::AddPeer {
+            interface,
+            sub_opts,
+        } => add_peer(&interface, opts, sub_opts)?,
+        Command::RenamePeer {
+            interface,
+            sub_opts,
+        } => rename_peer(&interface, opts, sub_opts)?,
+        Command::AddCidr {
+            interface,
+            sub_opts,
+        } => add_cidr(&interface, opts, sub_opts)?,
+        Command::DeleteCidr {
+            interface,
+            sub_opts,
+        } => delete_cidr(&interface, opts, sub_opts)?,
+        Command::ListCidrs { interface, tree } => list_cidrs(&interface, opts, tree)?,
+        Command::DisablePeer { interface } => enable_or_disable_peer(&interface, opts, false)?,
+        Command::EnablePeer { interface } => enable_or_disable_peer(&interface, opts, true)?,
+        Command::AddAssociation {
+            interface,
+            sub_opts,
+        } => add_association(&interface, opts, sub_opts)?,
+        Command::DeleteAssociation { interface } => delete_association(&interface, opts)?,
+        Command::ListAssociations { interface } => list_associations(&interface, opts)?,
         Command::SetListenPort { interface, unset } => {
-            set_listen_port(&interface, unset, opt.network)?;
+            set_listen_port(&interface, opts, unset)?;
         },
         Command::OverrideEndpoint { interface, unset } => {
-            override_endpoint(&interface, unset, opt.network)?;
+            override_endpoint(&interface, opts, unset)?;
         },
         Command::Completions { shell } => {
             Opts::clap().gen_completions_to("innernet", shell, &mut std::io::stdout());
diff --git a/client/src/util.rs b/client/src/util.rs
index d8536857..cabd137f 100644
--- a/client/src/util.rs
+++ b/client/src/util.rs
@@ -4,7 +4,7 @@ use indoc::eprintdoc;
 use log::{Level, LevelFilter};
 use serde::{de::DeserializeOwned, Serialize};
 use shared::{interface_config::ServerInfo, PeerDiff, INNERNET_PUBKEY_HEADER};
-use std::{io, time::Duration};
+use std::{io, path::Path, time::Duration};
 use ureq::{Agent, AgentBuilder};
 
 static LOGGER: Logger = Logger;
@@ -102,7 +102,7 @@ pub fn human_size(bytes: u64) -> String {
     }
 }
 
-pub fn permissions_helptext(e: &io::Error) {
+pub fn permissions_helptext(config_dir: &Path, data_dir: &Path, e: &io::Error) {
     if e.raw_os_error() == Some(1) {
         let current_exe = std::env::current_exe()
             .ok()
@@ -131,8 +131,8 @@ pub fn permissions_helptext(e: &io::Error) {
                     sudo chmod -R g+rwX {config} {data}
             ",
             "ERROR".bold().red(),
-            config = shared::CLIENT_CONFIG_DIR.to_string_lossy(),
-            data = shared::CLIENT_DATA_DIR.to_string_lossy(),
+            config = config_dir.to_string_lossy(),
+            data = data_dir.to_string_lossy(),
         );
     }
 }
diff --git a/server/src/api/user.rs b/server/src/api/user.rs
index a6a8200b..d83e7607 100644
--- a/server/src/api/user.rs
+++ b/server/src/api/user.rs
@@ -109,7 +109,7 @@ mod handlers {
             //
             // Related: https://github.com/hyperium/hyper/issues/2181
             tokio::task::spawn(async move {
-                tokio::time::sleep(*REDEEM_TRANSITION_WAIT).await;
+                tokio::time::sleep(REDEEM_TRANSITION_WAIT).await;
                 log::info!(
                     "WireGuard: adding new peer {}, removing old pubkey {}",
                     &*selected_peer,
diff --git a/server/src/main.rs b/server/src/main.rs
index d85523bc..df7aad07 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -39,17 +39,23 @@ mod initialize;
 use db::{DatabaseCidr, DatabasePeer};
 pub use error::ServerError;
 use initialize::InitializeOpts;
-use shared::{prompts, wg, CidrTree, Error, Interface, SERVER_CONFIG_DIR, SERVER_DATABASE_DIR};
+use shared::{prompts, wg, CidrTree, Error, Interface};
 pub use shared::{Association, AssociationContents};
 
 pub const VERSION: &str = env!("CARGO_PKG_VERSION");
 
 #[derive(Debug, StructOpt)]
 #[structopt(name = "innernet-server", about, global_settings(&[AppSettings::ColoredHelp, AppSettings::DeriveDisplayOrder, AppSettings::VersionlessSubcommands, AppSettings::UnifiedHelpMessage]))]
-struct Opt {
+struct Opts {
     #[structopt(subcommand)]
     command: Command,
 
+    #[structopt(short, long, default_value = "/etc/innernet-server")]
+    config_dir: PathBuf,
+
+    #[structopt(short, long, default_value = "/var/lib/innernet-server")]
+    data_dir: PathBuf,
+
     #[structopt(flatten)]
     network: NetworkOpts,
 }
@@ -184,17 +190,22 @@ impl ConfigFile {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug)]
 pub struct ServerConfig {
-    wg_manage_dir_override: Option<PathBuf>,
-    wg_dir_override: Option<PathBuf>,
+    pub config_dir: PathBuf,
+    pub data_dir: PathBuf,
 }
 
 impl ServerConfig {
+    pub fn new(config_dir: PathBuf, data_dir: PathBuf) -> Self {
+        Self {
+            config_dir,
+            data_dir,
+        }
+    }
+
     fn database_dir(&self) -> &Path {
-        self.wg_manage_dir_override
-            .as_deref()
-            .unwrap_or(*SERVER_DATABASE_DIR)
+        &self.data_dir
     }
 
     fn database_path(&self, interface: &InterfaceName) -> PathBuf {
@@ -205,9 +216,7 @@ impl ServerConfig {
     }
 
     fn config_dir(&self) -> &Path {
-        self.wg_dir_override
-            .as_deref()
-            .unwrap_or(*SERVER_CONFIG_DIR)
+        &self.config_dir
     }
 
     fn config_path(&self, interface: &InterfaceName) -> PathBuf {
@@ -226,32 +235,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     }
 
     pretty_env_logger::init();
-    let opt = Opt::from_args();
+    let opts = Opts::from_args();
 
-    if unsafe { libc::getuid() } != 0 && !matches!(opt.command, Command::Completions { .. }) {
+    if unsafe { libc::getuid() } != 0 && !matches!(opts.command, Command::Completions { .. }) {
         return Err("innernet-server must run as root.".into());
     }
 
-    let conf = ServerConfig::default();
+    let conf = ServerConfig::new(opts.config_dir, opts.data_dir);
 
-    match opt.command {
+    match opts.command {
         Command::New { opts } => {
             if let Err(e) = initialize::init_wizard(&conf, opts) {
                 eprintln!("{}: {}.", "creation failed".red(), e);
                 std::process::exit(1);
             }
         },
-        Command::Uninstall { interface } => uninstall(&interface, &conf, opt.network)?,
+        Command::Uninstall { interface } => uninstall(&interface, &conf, opts.network)?,
         Command::Serve {
             interface,
             network: routing,
         } => serve(*interface, &conf, routing).await?,
-        Command::AddPeer { interface, args } => add_peer(&interface, &conf, args, opt.network)?,
+        Command::AddPeer { interface, args } => add_peer(&interface, &conf, args, opts.network)?,
         Command::RenamePeer { interface, args } => rename_peer(&interface, &conf, args)?,
         Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?,
         Command::DeleteCidr { interface, args } => delete_cidr(&interface, &conf, args)?,
         Command::Completions { shell } => {
-            Opt::clap().gen_completions_to("innernet-server", shell, &mut std::io::stdout());
+            Opts::clap().gen_completions_to("innernet-server", shell, &mut std::io::stdout());
             std::process::exit(0);
         },
     }
diff --git a/shared/src/interface_config.rs b/shared/src/interface_config.rs
index 9b791280..f772ddf2 100644
--- a/shared/src/interface_config.rs
+++ b/shared/src/interface_config.rs
@@ -1,6 +1,4 @@
-use crate::{
-    chmod, ensure_dirs_exist, Endpoint, Error, IoErrorContext, WrappedIoError, CLIENT_CONFIG_DIR,
-};
+use crate::{chmod, ensure_dirs_exist, Endpoint, Error, IoErrorContext, WrappedIoError};
 use indoc::writedoc;
 use ipnetwork::IpNetwork;
 use serde::{Deserialize, Serialize};
@@ -101,8 +99,12 @@ impl InterfaceConfig {
     }
 
     /// Overwrites the config file if it already exists.
-    pub fn write_to_interface(&self, interface: &InterfaceName) -> Result<PathBuf, Error> {
-        let path = Self::build_config_file_path(interface)?;
+    pub fn write_to_interface(
+        &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(toml::to_string(self).unwrap().as_bytes())?;
@@ -113,21 +115,24 @@ impl InterfaceConfig {
         Ok(toml::from_slice(&std::fs::read(&path).with_path(path)?)?)
     }
 
-    pub fn from_interface(interface: &InterfaceName) -> Result<Self, Error> {
-        let path = Self::build_config_file_path(interface)?;
+    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(interface: &InterfaceName) -> PathBuf {
-        CLIENT_CONFIG_DIR
+    pub fn get_path(config_dir: &Path, interface: &InterfaceName) -> PathBuf {
+        config_dir
             .join(interface.to_string())
             .with_extension("conf")
     }
 
-    fn build_config_file_path(interface: &InterfaceName) -> Result<PathBuf, WrappedIoError> {
-        ensure_dirs_exist(&[*CLIENT_CONFIG_DIR])?;
-        Ok(Self::get_path(interface))
+    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))
     }
 }
 
diff --git a/shared/src/lib.rs b/shared/src/lib.rs
index 05cda3c2..7df29379 100644
--- a/shared/src/lib.rs
+++ b/shared/src/lib.rs
@@ -1,5 +1,4 @@
 pub use anyhow::Error;
-use lazy_static::lazy_static;
 use std::{
     fs::{self, File, Permissions},
     io,
@@ -17,14 +16,7 @@ pub mod wg;
 
 pub use types::*;
 
-lazy_static! {
-    pub static ref CLIENT_CONFIG_DIR: &'static Path = Path::new("/etc/innernet");
-    pub static ref CLIENT_DATA_DIR: &'static Path = Path::new("/var/lib/innernet");
-    pub static ref SERVER_CONFIG_DIR: &'static Path = Path::new("/etc/innernet-server");
-    pub static ref SERVER_DATABASE_DIR: &'static Path = Path::new("/var/lib/innernet-server");
-    pub static ref REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5);
-}
-
+pub const REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5);
 pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
 pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
 
diff --git a/shared/src/types.rs b/shared/src/types.rs
index acce7653..1a1fb352 100644
--- a/shared/src/types.rs
+++ b/shared/src/types.rs
@@ -413,6 +413,7 @@ impl NatOpts {
             no_nat_candidates: true,
         }
     }
+
     /// Check if an IP is allowed to be reported as a candidate.
     pub fn is_excluded(&self, ip: IpAddr) -> bool {
         self.no_nat_candidates


Test patch:
diff --git a/server/src/test.rs b/server/src/test.rs
index c293a5e4..178a05e8 100644
--- a/server/src/test.rs
+++ b/server/src/test.rs
@@ -88,8 +88,8 @@ impl Server {
         // cidrs and peers.
         let interface = "test".to_string();
         let conf = ServerConfig {
-            wg_manage_dir_override: Some(test_dir_path.to_path_buf()),
-            wg_dir_override: Some(test_dir_path.to_path_buf()),
+            config_dir: test_dir_path.to_path_buf(),
+            data_dir: test_dir_path.to_path_buf(),
         };
 
         let opts = InitializeOpts {