innernet/server/src/initialize.rs

240 lines
7.4 KiB
Rust

use crate::*;
use anyhow::anyhow;
use clap::Parser;
use db::DatabaseCidr;
use dialoguer::{theme::ColorfulTheme, Input};
use indoc::printdoc;
use ipnet::IpNet;
use publicip::Preference;
use rusqlite::{params, Connection};
use shared::{
prompts, CidrContents, Endpoint, IpNetExt, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
};
use wireguard_control::KeyPair;
fn create_database<P: AsRef<Path>>(
database_path: P,
) -> Result<Connection, Box<dyn std::error::Error>> {
let conn = Connection::open(&database_path)?;
conn.pragma_update(None, "foreign_keys", &1)?;
conn.execute(db::peer::CREATE_TABLE_SQL, params![])?;
conn.execute(db::association::CREATE_TABLE_SQL, params![])?;
conn.execute(db::cidr::CREATE_TABLE_SQL, params![])?;
conn.pragma_update(None, "user_version", &db::CURRENT_VERSION)?;
log::debug!("set database version to db::CURRENT_VERSION");
Ok(conn)
}
#[derive(Debug, Default, Clone, PartialEq, Parser)]
pub struct InitializeOpts {
/// The network name (ex: evilcorp)
#[clap(long)]
pub network_name: Option<Interface>,
/// The network CIDR (ex: 10.42.0.0/16)
#[clap(long)]
pub network_cidr: Option<IpNet>,
/// This server's external endpoint (ex: 100.100.100.100:51820)
#[clap(long, conflicts_with = "auto-external-endpoint")]
pub external_endpoint: Option<Endpoint>,
/// Auto-resolve external endpoint
#[clap(long = "auto-external-endpoint")]
pub auto_external_endpoint: bool,
/// Port to listen on (for the WireGuard interface)
#[clap(long)]
pub listen_port: Option<u16>,
}
struct DbInitData {
network_name: String,
network_cidr: IpNet,
server_cidr: IpNet,
our_ip: IpAddr,
public_key_base64: String,
endpoint: Endpoint,
}
fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), Error> {
const SERVER_NAME: &str = "innernet-server";
let root_cidr = DatabaseCidr::create(
conn,
CidrContents {
name: db_init_data.network_name.clone(),
cidr: db_init_data.network_cidr,
parent: None,
},
)
.map_err(|_| anyhow!("failed to create root CIDR"))?;
let server_cidr = DatabaseCidr::create(
conn,
CidrContents {
name: SERVER_NAME.into(),
cidr: db_init_data.server_cidr,
parent: Some(root_cidr.id),
},
)
.map_err(|_| anyhow!("failed to create innernet-server CIDR"))?;
let _me = DatabasePeer::create(
conn,
PeerContents {
name: SERVER_NAME.parse().map_err(|e: &str| anyhow!(e))?,
ip: db_init_data.our_ip,
cidr_id: server_cidr.id,
public_key: db_init_data.public_key_base64,
endpoint: Some(db_init_data.endpoint),
is_admin: true,
is_disabled: false,
is_redeemed: true,
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
invite_expires: None,
candidates: vec![],
},
)
.map_err(|_| anyhow!("failed to create innernet peer."))?;
Ok(())
}
pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Error> {
let theme = ColorfulTheme::default();
shared::ensure_dirs_exist(&[conf.config_dir(), conf.database_dir()]).map_err(|_| {
anyhow!(
"Failed to create config and database directories {}",
"(are you not running as root?)".bold()
)
})?;
printdoc!(
"\nTime to setup your innernet network.
Your network name can be any hostname-valid string, i.e. \"evilcorp\", and
your network CIDR should be in the RFC1918 IPv4 (10/8, 172.16/12, or 192.168/16),
or RFC4193 IPv6 (fd00::/8) ranges.
The external endpoint specified is a <host>:<port> string that is the address clients
will connect to. It's up to you to forward/open ports in your routers/firewalls
as needed.
For more usage instructions, see https://github.com/tonarino/innernet#usage
\n"
);
let name: Interface = if let Some(name) = opts.network_name {
name
} else {
Input::with_theme(&theme)
.with_prompt("Network name")
.interact()?
};
let root_cidr: IpNet = if let Some(cidr) = opts.network_cidr {
cidr
} else {
Input::with_theme(&theme)
.with_prompt("Network CIDR")
.with_initial_text("10.42.0.0/16")
.interact()?
};
let listen_port: u16 = if let Some(listen_port) = opts.listen_port {
listen_port
} else {
Input::with_theme(&theme)
.with_prompt("Listen port")
.default(51820)
.interact()
.map_err(|_| anyhow!("failed to get listen port."))?
};
log::info!("listen port: {}", listen_port);
let endpoint: Endpoint = if let Some(endpoint) = opts.external_endpoint {
endpoint
} else if opts.auto_external_endpoint {
let ip = publicip::get_any(Preference::Ipv4)
.ok_or_else(|| anyhow!("couldn't get external IP"))?;
SocketAddr::new(ip, listen_port).into()
} else {
prompts::ask_endpoint(listen_port)?
};
let our_ip = root_cidr
.hosts()
.find(|ip| root_cidr.is_assignable(ip))
.unwrap();
let config_path = conf.config_path(&name);
let our_keypair = KeyPair::generate();
let config = ConfigFile {
private_key: our_keypair.private.to_base64(),
listen_port,
address: our_ip,
network_cidr_prefix: root_cidr.prefix_len(),
};
config.write_to_path(&config_path)?;
let db_init_data = DbInitData {
network_name: name.to_string(),
network_cidr: root_cidr,
server_cidr: IpNet::new(our_ip, root_cidr.max_prefix_len())?,
our_ip,
public_key_base64: our_keypair.public.to_base64(),
endpoint,
};
// TODO(bschwind) - Clean up the config file and database
// if any errors occur in these init calls.
let database_path = conf.database_path(&name);
let conn = create_database(&database_path).map_err(|_| {
anyhow!(
"failed to create database {}",
"(are you not running as root?)".bold()
)
})?;
populate_database(&conn, db_init_data)?;
println!(
"{} Created database at {}\n",
"[*]".dimmed(),
database_path.to_string_lossy().bold()
);
printdoc!(
"
{star} Setup finished.
Network {interface} has been {created}, but it's not started yet!
Your new network starts with only one peer: this innernet server. Next,
you'll want to create additional CIDRs and peers using the commands:
{wg_manage_server} {add_cidr} {interface}, and
{wg_manage_server} {add_peer} {interface}
See https://github.com/tonarino/innernet for more detailed instruction
on designing your network.
When you're ready to start the network, you can auto-start the server:
{systemctl_enable}{interface}
",
star = "[*]".dimmed(),
interface = name.to_string().yellow(),
created = "created".green(),
wg_manage_server = "innernet-server".yellow(),
add_cidr = "add-cidr".yellow(),
add_peer = "add-peer".yellow(),
systemctl_enable = "systemctl enable --now innernet-server@".yellow(),
);
Ok(())
}