mirror of https://github.com/dswd/vpncloud.git
Add config wizard
This commit is contained in:
parent
ad2e2014eb
commit
c4ff0ed198
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
This project follows [semantic versioning](http://semver.org).
|
This project follows [semantic versioning](http://semver.org).
|
||||||
|
|
||||||
|
### UNRELEASED
|
||||||
|
|
||||||
|
- [added] Added interactive configuration wizard
|
||||||
|
- [changed] Restructured example config
|
||||||
|
|
||||||
### v2.1.0 (2021-02-06)
|
### v2.1.0 (2021-02-06)
|
||||||
|
|
||||||
- [added] Support for websocket proxy mode
|
- [added] Support for websocket proxy mode
|
||||||
|
|
|
@ -143,6 +143,22 @@ dependencies = [
|
||||||
"vec_map",
|
"vec_map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a50aab2529019abfabfa93f1e6c41ef392f91fbf179b347a7e96abb524884a08"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"regex",
|
||||||
|
"terminal_size",
|
||||||
|
"unicode-width",
|
||||||
|
"winapi",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_fn"
|
name = "const_fn"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
|
@ -269,6 +285,18 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70f807b2943dc90f9747497d9d65d7e92472149be0b88bf4ce1201b4ac979c26"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"lazy_static",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -296,6 +324,12 @@ version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -1028,6 +1062,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -1231,6 +1275,7 @@ dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"criterion",
|
"criterion",
|
||||||
"daemonize",
|
"daemonize",
|
||||||
|
"dialoguer",
|
||||||
"fnv",
|
"fnv",
|
||||||
"iai",
|
"iai",
|
||||||
"igd",
|
"igd",
|
||||||
|
@ -1393,3 +1438,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86"
|
||||||
|
|
|
@ -22,15 +22,16 @@ libc = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
fnv = "1"
|
fnv = "1"
|
||||||
yaml-rust = "0.4"
|
yaml-rust = "0.4"
|
||||||
igd = { version = "0.12", optional = true }
|
|
||||||
daemonize = "0.4"
|
daemonize = "0.4"
|
||||||
ring = "0.16"
|
ring = "0.16"
|
||||||
privdrop = "0.5"
|
privdrop = "0.5"
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
smallvec = "1.6"
|
smallvec = "1.6"
|
||||||
|
dialoguer = { version = "0.7", optional = true }
|
||||||
tungstenite = { version = "0.13", optional = true, default-features = false }
|
tungstenite = { version = "0.13", optional = true, default-features = false }
|
||||||
url = { version = "2.2", optional = true }
|
url = { version = "2.2", optional = true }
|
||||||
|
igd = { version = "0.12", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
@ -38,9 +39,10 @@ criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
iai = "0.1"
|
iai = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["nat", "websocket"]
|
default = ["nat", "websocket", "wizard"]
|
||||||
nat = ["igd"]
|
nat = ["igd"]
|
||||||
websocket = ["tungstenite", "url"]
|
websocket = ["tungstenite", "url"]
|
||||||
|
wizard = ["dialoguer"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
|
|
|
@ -303,6 +303,46 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_config_file(self) -> ConfigFile {
|
||||||
|
ConfigFile {
|
||||||
|
auto_claim: Some(self.auto_claim),
|
||||||
|
claims: Some(self.claims),
|
||||||
|
beacon: Some(ConfigFileBeacon {
|
||||||
|
store: self.beacon_store,
|
||||||
|
load: self.beacon_load,
|
||||||
|
interval: Some(self.beacon_interval),
|
||||||
|
password: self.beacon_password
|
||||||
|
}),
|
||||||
|
device: Some(ConfigFileDevice {
|
||||||
|
name: Some(self.device_name),
|
||||||
|
path: self.device_path,
|
||||||
|
type_: Some(self.device_type),
|
||||||
|
fix_rp_filter: Some(self.fix_rp_filter)
|
||||||
|
}),
|
||||||
|
crypto: self.crypto,
|
||||||
|
group: self.group,
|
||||||
|
user: self.user,
|
||||||
|
ifup: self.ifup,
|
||||||
|
ifdown: self.ifdown,
|
||||||
|
ip: self.ip,
|
||||||
|
keepalive: self.keepalive,
|
||||||
|
listen: Some(self.listen),
|
||||||
|
mode: Some(self.mode),
|
||||||
|
peer_timeout: Some(self.peer_timeout),
|
||||||
|
peers: Some(self.peers),
|
||||||
|
pid_file: self.pid_file,
|
||||||
|
port_forwarding: Some(self.port_forwarding),
|
||||||
|
stats_file: self.stats_file,
|
||||||
|
statsd: Some(ConfigFileStatsd {
|
||||||
|
server: self.statsd_server,
|
||||||
|
prefix: self.statsd_prefix
|
||||||
|
}),
|
||||||
|
switch_timeout: Some(self.switch_timeout),
|
||||||
|
hook: self.hook,
|
||||||
|
hooks: self.hooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_keepalive(&self) -> Duration {
|
pub fn get_keepalive(&self) -> Duration {
|
||||||
match self.keepalive {
|
match self.keepalive {
|
||||||
Some(dur) => dur,
|
Some(dur) => dur,
|
||||||
|
@ -525,6 +565,14 @@ pub enum Command {
|
||||||
/// Shell to create completions for
|
/// Shell to create completions for
|
||||||
#[structopt(long, default_value="bash")]
|
#[structopt(long, default_value="bash")]
|
||||||
shell: Shell
|
shell: Shell
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Edit the config of a network
|
||||||
|
#[cfg(feature = "wizard")]
|
||||||
|
Config {
|
||||||
|
/// Name of the network
|
||||||
|
#[structopt(short, long)]
|
||||||
|
name: Option<String>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,27 @@ pub struct Crypto {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crypto {
|
impl Crypto {
|
||||||
|
pub fn parse_algorithms(algos: &[String]) -> Result<(bool, Vec<&'static aead::Algorithm>), Error> {
|
||||||
|
let algorithms = algos.iter().map(|a| a as &str).collect::<Vec<_>>();
|
||||||
|
let allowed = if algorithms.is_empty() { &DEFAULT_ALGORITHMS } else { &algorithms as &[&str] };
|
||||||
|
let mut algos = vec![];
|
||||||
|
let mut unencrypted = false;
|
||||||
|
for name in allowed {
|
||||||
|
let algo = match &name.to_uppercase() as &str {
|
||||||
|
"UNENCRYPTED" | "NONE" | "PLAIN" => {
|
||||||
|
unencrypted = true;
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
"AES128" | "AES128_GCM" | "AES_128" | "AES_128_GCM" => &aead::AES_128_GCM,
|
||||||
|
"AES256" | "AES256_GCM" | "AES_256" | "AES_256_GCM" => &aead::AES_256_GCM,
|
||||||
|
"CHACHA" | "CHACHA20" | "CHACHA20_POLY1305" => &aead::CHACHA20_POLY1305,
|
||||||
|
_ => return Err(Error::InvalidConfig("Unknown crypto method"))
|
||||||
|
};
|
||||||
|
algos.push(algo)
|
||||||
|
}
|
||||||
|
Ok((unencrypted, algos))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(node_id: NodeId, config: &Config) -> Result<Self, Error> {
|
pub fn new(node_id: NodeId, config: &Config) -> Result<Self, Error> {
|
||||||
let key_pair = if let Some(priv_key) = &config.private_key {
|
let key_pair = if let Some(priv_key) = &config.private_key {
|
||||||
if let Some(pub_key) = &config.public_key {
|
if let Some(pub_key) = &config.public_key {
|
||||||
|
@ -91,26 +112,17 @@ impl Crypto {
|
||||||
key.clone_from_slice(key_pair.public_key().as_ref());
|
key.clone_from_slice(key_pair.public_key().as_ref());
|
||||||
trusted_keys.push(key);
|
trusted_keys.push(key);
|
||||||
}
|
}
|
||||||
let mut algos = Algorithms { algorithm_speeds: smallvec![], allow_unencrypted: false };
|
let (unencrypted, allowed_algos) = Self::parse_algorithms(&config.algorithms)?;
|
||||||
let algorithms = config.algorithms.iter().map(|a| a as &str).collect::<Vec<_>>();
|
if unencrypted {
|
||||||
let allowed = if algorithms.is_empty() { &DEFAULT_ALGORITHMS } else { &algorithms as &[&str] };
|
warn!("Crypto settings allow unencrypted connections")
|
||||||
|
}
|
||||||
|
let mut algos = Algorithms { algorithm_speeds: smallvec![], allow_unencrypted: unencrypted };
|
||||||
let duration = Duration::from_secs_f32(SPEED_TEST_TIME);
|
let duration = Duration::from_secs_f32(SPEED_TEST_TIME);
|
||||||
let mut speeds = Vec::new();
|
let mut speeds = Vec::new();
|
||||||
for name in allowed {
|
for algo in allowed_algos {
|
||||||
let algo = match &name.to_uppercase() as &str {
|
|
||||||
"UNENCRYPTED" | "NONE" | "PLAIN" => {
|
|
||||||
algos.allow_unencrypted = true;
|
|
||||||
warn!("Crypto settings allow unencrypted connections");
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
"AES128" | "AES128_GCM" | "AES_128" | "AES_128_GCM" => &aead::AES_128_GCM,
|
|
||||||
"AES256" | "AES256_GCM" | "AES_256" | "AES_256_GCM" => &aead::AES_256_GCM,
|
|
||||||
"CHACHA" | "CHACHA20" | "CHACHA20_POLY1305" => &aead::CHACHA20_POLY1305,
|
|
||||||
_ => return Err(Error::InvalidConfig("Unknown crypto method"))
|
|
||||||
};
|
|
||||||
let speed = test_speed(algo, &duration);
|
let speed = test_speed(algo, &duration);
|
||||||
algos.algorithm_speeds.push((algo, speed as f32));
|
algos.algorithm_speeds.push((algo, speed as f32));
|
||||||
speeds.push((name, speed as f32));
|
speeds.push((format!("{:?}", algo), speed as f32));
|
||||||
}
|
}
|
||||||
if !speeds.is_empty() {
|
if !speeds.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
|
@ -180,6 +192,11 @@ impl Crypto {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn public_key_from_private_key(privkey: &str) -> Result<String, Error> {
|
||||||
|
let keypair = Self::parse_private_key(privkey)?;
|
||||||
|
Ok(to_base62(keypair.public_key().as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn peer_instance<P: Payload>(&self, payload: P) -> PeerCrypto<P> {
|
pub fn peer_instance<P: Payload>(&self, payload: P) -> PeerCrypto<P> {
|
||||||
PeerCrypto::new(
|
PeerCrypto::new(
|
||||||
self.node_id,
|
self.node_id,
|
||||||
|
@ -216,8 +233,7 @@ impl<P: Payload> PeerCrypto<P> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
node_id: NodeId, init_payload: P, key_pair: Arc<Ed25519KeyPair>, trusted_keys: Arc<[Ed25519PublicKey]>,
|
node_id: NodeId, init_payload: P, key_pair: Arc<Ed25519KeyPair>, trusted_keys: Arc<[Ed25519PublicKey]>,
|
||||||
algorithms: Algorithms
|
algorithms: Algorithms
|
||||||
) -> Self
|
) -> Self {
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
node_id,
|
node_id,
|
||||||
init: Some(InitState::new(node_id, init_payload, key_pair, trusted_keys, algorithms)),
|
init: Some(InitState::new(node_id, init_payload, key_pair, trusted_keys, algorithms)),
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub mod port_forwarding;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod traffic;
|
pub mod traffic;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
#[cfg(feature = "wizard")] pub mod wizard;
|
||||||
#[cfg(feature = "websocket")] pub mod wsproxy;
|
#[cfg(feature = "websocket")] pub mod wsproxy;
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
@ -272,12 +273,15 @@ fn main() {
|
||||||
}
|
}
|
||||||
Command::Completion { shell } => {
|
Command::Completion { shell } => {
|
||||||
Args::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
|
Args::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
|
||||||
return
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "websocket")]
|
#[cfg(feature = "websocket")]
|
||||||
Command::WsProxy { listen } => {
|
Command::WsProxy { listen } => {
|
||||||
try_fail!(wsproxy::run_proxy(&listen), "Failed to run websocket proxy: {:?}");
|
try_fail!(wsproxy::run_proxy(&listen), "Failed to run websocket proxy: {:?}");
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "wizard")]
|
||||||
|
Command::Config { name } => {
|
||||||
|
try_fail!(wizard::configure(name), "Wizard failed: {}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,512 @@
|
||||||
|
use crate::{config::Config, crypto::Crypto, device, types::Mode};
|
||||||
|
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Password, Select};
|
||||||
|
use ring::aead;
|
||||||
|
use std::{collections::HashMap, fs, io, os::unix::fs::PermissionsExt, path::Path};
|
||||||
|
|
||||||
|
const MODE_SIMPLE: usize = 0;
|
||||||
|
const MODE_ADVANCED: usize = 1;
|
||||||
|
const MODE_EXPERT: usize = 2;
|
||||||
|
|
||||||
|
fn str_list(s: String) -> Vec<String> {
|
||||||
|
if s.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
s.split(',').map(|k| k.trim().to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_opt(s: String) -> Option<String> {
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_connectivity(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
config.listen =
|
||||||
|
Input::with_theme(theme).with_prompt("Listen address").default(config.listen.clone()).interact_text()?;
|
||||||
|
}
|
||||||
|
config.peers = str_list(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Peer addresses (comma separated)")
|
||||||
|
.default(config.peers.join(","))
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
config.port_forwarding = Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Enable automatic port forwarding?")
|
||||||
|
.default(config.port_forwarding)
|
||||||
|
.interact()?;
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
config.peer_timeout = Input::with_theme(theme)
|
||||||
|
.with_prompt("Peer timeout (in seconds)")
|
||||||
|
.default(config.peer_timeout)
|
||||||
|
.interact_text()?;
|
||||||
|
let val = Input::with_theme(theme)
|
||||||
|
.with_prompt("Keepalive interval (in seconds, 0 for default)")
|
||||||
|
.default(config.keepalive.unwrap_or_default())
|
||||||
|
.interact_text()?;
|
||||||
|
config.keepalive = if val == 0 { None } else { Some(val) };
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn configure_crypto(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if (config.crypto.password.is_some() || config.crypto.private_key.is_some())
|
||||||
|
&& !Confirm::with_theme(theme).with_prompt("Create new crypto config?").default(false).interact()?
|
||||||
|
{
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
let mut use_password = true;
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
use_password = Select::with_theme(theme)
|
||||||
|
.with_prompt("Crypto configuration method")
|
||||||
|
.items(&["Simple (Password)", "Complex (Key pair)"])
|
||||||
|
.default(if config.crypto.private_key.is_some() { 1 } else { 0 })
|
||||||
|
.interact()?
|
||||||
|
== 0
|
||||||
|
}
|
||||||
|
if use_password {
|
||||||
|
config.crypto.password = Some(
|
||||||
|
Password::with_theme(theme)
|
||||||
|
.with_prompt("Password")
|
||||||
|
.with_confirmation("Confirm password", "Passwords do not match")
|
||||||
|
.interact()?
|
||||||
|
);
|
||||||
|
config.crypto.private_key = None;
|
||||||
|
config.crypto.public_key = None;
|
||||||
|
config.crypto.trusted_keys = vec![];
|
||||||
|
} else {
|
||||||
|
config.crypto.password = None;
|
||||||
|
let (priv_key, pub_key) = match Select::with_theme(theme)
|
||||||
|
.with_prompt("Specify key pair")
|
||||||
|
.items(&["Generate new key pair", "Enter private key", "Generate from password"])
|
||||||
|
.default(0)
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
0 => {
|
||||||
|
let (priv_key, pub_key) = Crypto::generate_keypair(None);
|
||||||
|
info!("Private key: {}", priv_key);
|
||||||
|
info!("Public key: {}", pub_key);
|
||||||
|
(priv_key, pub_key)
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let priv_key = Password::with_theme(theme)
|
||||||
|
.with_prompt("Private key")
|
||||||
|
.with_confirmation("Confirm private key", "Keys do not match")
|
||||||
|
.interact()?;
|
||||||
|
let pub_key = try_fail!(Crypto::public_key_from_private_key(&priv_key), "Invalid private key: {:?}");
|
||||||
|
info!("Public key: {}", pub_key);
|
||||||
|
(priv_key, pub_key)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let password = Password::with_theme(theme)
|
||||||
|
.with_prompt("Password")
|
||||||
|
.with_confirmation("Confirm password", "Passwords do not match")
|
||||||
|
.interact()?;
|
||||||
|
let (priv_key, pub_key) = Crypto::generate_keypair(Some(&password));
|
||||||
|
info!("Private key: {}", priv_key);
|
||||||
|
info!("Public key: {}", pub_key);
|
||||||
|
(priv_key, pub_key)
|
||||||
|
}
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
config.crypto.trusted_keys = str_list(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Trusted keys (public keys, comma separated)")
|
||||||
|
.default(pub_key.clone())
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.crypto.private_key = Some(priv_key);
|
||||||
|
config.crypto.public_key = Some(pub_key);
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
let (unencrypted, allowed_algos) = Crypto::parse_algorithms(&config.crypto.algorithms)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid crypto algorithms"))?;
|
||||||
|
let algos = MultiSelect::with_theme(theme)
|
||||||
|
.with_prompt("Allowed encryption algorithms (select multiple)")
|
||||||
|
.items_checked(&[
|
||||||
|
("Unencrypted (dangerous)", unencrypted),
|
||||||
|
("AES-128 in GCM mode", allowed_algos.contains(&&aead::AES_128_GCM)),
|
||||||
|
("AES-256 in GCM mode", allowed_algos.contains(&&aead::AES_256_GCM)),
|
||||||
|
("ChaCha20-Poly1305 (RFC 7539)", allowed_algos.contains(&&aead::CHACHA20_POLY1305))
|
||||||
|
])
|
||||||
|
.interact()?;
|
||||||
|
config.crypto.algorithms = vec![];
|
||||||
|
for (id, name) in &[(0, "PLAIN"), (1, "AES128"), (2, "AES256"), (3, "CHACHA20")] {
|
||||||
|
if algos.contains(id) {
|
||||||
|
config.crypto.algorithms.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_device(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
config.device_type = match Select::with_theme(theme)
|
||||||
|
.with_prompt("Device type")
|
||||||
|
.items(&["Tun (IP based)", "Tap (Ethernet based)"])
|
||||||
|
.default(if config.device_type == device::Type::Tun { 0 } else { 1 })
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
0 => device::Type::Tun,
|
||||||
|
1 => device::Type::Tap,
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
config.device_name =
|
||||||
|
Input::with_theme(theme).with_prompt("Device name").default(config.device_name.clone()).interact_text()?;
|
||||||
|
config.device_path = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Device path (empty for default)")
|
||||||
|
.default(config.device_path.as_ref().cloned().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.fix_rp_filter = Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Automatically fix insecure rp_filter settings")
|
||||||
|
.default(config.fix_rp_filter)
|
||||||
|
.interact()?;
|
||||||
|
config.mode = match Select::with_theme(theme)
|
||||||
|
.with_prompt("Operation mode")
|
||||||
|
.items(&["Normal", "Router", "Switch", "Hub"])
|
||||||
|
.default(match config.mode {
|
||||||
|
Mode::Normal => 0,
|
||||||
|
Mode::Router => 1,
|
||||||
|
Mode::Switch => 2,
|
||||||
|
Mode::Hub => 3
|
||||||
|
})
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
0 => Mode::Normal,
|
||||||
|
1 => Mode::Router,
|
||||||
|
2 => Mode::Switch,
|
||||||
|
3 => Mode::Hub,
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
if config.mode == Mode::Switch {
|
||||||
|
config.switch_timeout = Input::with_theme(theme)
|
||||||
|
.with_prompt("Switch timeout (in seconds")
|
||||||
|
.default(config.switch_timeout)
|
||||||
|
.interact_text()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_addresses(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
config.ip = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Virtual IP address (e.g. 10.0.0.1, leave empty for none)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.default(config.ip.as_ref().cloned().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
if config.device_type == device::Type::Tun {
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
config.auto_claim = Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Automatically claim IP set on virtual interface?")
|
||||||
|
.default(config.auto_claim)
|
||||||
|
.interact()?;
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
config.claims = str_list(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Claim additional addresses (e.g. 10.0.0.0/24, comma separated, leave empty for none)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.default(config.claims.join(","))
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.claims = vec![];
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
config.ifup = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Interface setup command (leave empty for none)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.default(config.ifup.as_ref().cloned().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.ifdown = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Interface tear down command (leave empty for none)")
|
||||||
|
.allow_empty(true)
|
||||||
|
.default(config.ifdown.as_ref().cloned().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_beacon(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode == MODE_EXPERT
|
||||||
|
&& Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Configure beacons?")
|
||||||
|
.default(config.beacon_load.is_some() || config.beacon_store.is_some())
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
config.beacon_store = match Select::with_theme(theme)
|
||||||
|
.with_prompt("How to store beacons")
|
||||||
|
.items(&["Do not store beacons", "Store to file", "Execute command"])
|
||||||
|
.default(if let Some(v) = &config.beacon_store {
|
||||||
|
if v.starts_with('|') {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
})
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
0 => None,
|
||||||
|
1 => {
|
||||||
|
Some(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("File path")
|
||||||
|
.default(config.beacon_store.clone().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
Some(format!(
|
||||||
|
"|{}",
|
||||||
|
Input::<String>::with_theme(theme)
|
||||||
|
.with_prompt("Command")
|
||||||
|
.default(config.beacon_store.clone().unwrap_or_default().trim_start_matches('|').to_string())
|
||||||
|
.interact_text()?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
config.beacon_load = match Select::with_theme(theme)
|
||||||
|
.with_prompt("How to load beacons")
|
||||||
|
.items(&["Do not load beacons", "Load from file", "Execute command"])
|
||||||
|
.default(if let Some(v) = &config.beacon_load {
|
||||||
|
if v.starts_with('|') {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
})
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
0 => None,
|
||||||
|
1 => {
|
||||||
|
Some(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("File path")
|
||||||
|
.default(config.beacon_load.clone().unwrap_or_default())
|
||||||
|
.interact_text()?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
Some(format!(
|
||||||
|
"|{}",
|
||||||
|
Input::<String>::with_theme(theme)
|
||||||
|
.with_prompt("Command")
|
||||||
|
.default(config.beacon_load.clone().unwrap_or_default().trim_start_matches('|').to_string())
|
||||||
|
.interact_text()?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
config.beacon_interval = Input::with_theme(theme)
|
||||||
|
.with_prompt("Beacon interval (in seconds)")
|
||||||
|
.default(config.beacon_interval)
|
||||||
|
.interact_text()?;
|
||||||
|
config.beacon_password = str_opt(
|
||||||
|
Password::with_theme(theme)
|
||||||
|
.with_prompt("Beacon password (leave empty for none)")
|
||||||
|
.with_confirmation("Confirm password", "Passwords do not match")
|
||||||
|
.allow_empty_password(true)
|
||||||
|
.interact()?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_stats(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode >= MODE_ADVANCED {
|
||||||
|
config.stats_file = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Write stats to file (empty to disable)")
|
||||||
|
.default(config.stats_file.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
if Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Send statistics to statsd server?")
|
||||||
|
.default(config.statsd_server.is_some())
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
config.statsd_server = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Statsd server URL")
|
||||||
|
.default(config.statsd_server.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.statsd_prefix = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Statsd prefix")
|
||||||
|
.default(config.statsd_prefix.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
config.statsd_server = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_process(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
config.user = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Run as different user (empty to disable)")
|
||||||
|
.default(config.user.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.group = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Run as different group (empty to disable)")
|
||||||
|
.default(config.group.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
config.pid_file = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Write process id to file (empty to disable)")
|
||||||
|
.default(config.pid_file.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_hooks(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), io::Error> {
|
||||||
|
if mode == MODE_EXPERT {
|
||||||
|
if Confirm::with_theme(theme)
|
||||||
|
.with_prompt("Set hooks to react on events?")
|
||||||
|
.default(config.hook.is_some() || !config.hooks.is_empty())
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
config.hook = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt("Command to execute for all events (empty to disable)")
|
||||||
|
.default(config.hook.clone().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
);
|
||||||
|
let mut hooks: HashMap<String, String> = Default::default();
|
||||||
|
for event in &[
|
||||||
|
"peer_connecting",
|
||||||
|
"peer_connected",
|
||||||
|
"peer_disconnected",
|
||||||
|
"device_setup",
|
||||||
|
"device_configured",
|
||||||
|
"vpn_started",
|
||||||
|
"vpn_shutdown"
|
||||||
|
] {
|
||||||
|
if let Some(cmd) = str_opt(
|
||||||
|
Input::with_theme(theme)
|
||||||
|
.with_prompt(format!("Command to execute for event '{}' (empty to disable)", event))
|
||||||
|
.default(config.hooks.get(*event).cloned().unwrap_or_default())
|
||||||
|
.allow_empty(true)
|
||||||
|
.interact_text()?
|
||||||
|
) {
|
||||||
|
hooks.insert(event.to_string(), cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.hooks = hooks;
|
||||||
|
} else {
|
||||||
|
config.hook = None;
|
||||||
|
config.hooks = Default::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(name: Option<String>) -> Result<(), io::Error> {
|
||||||
|
let theme = ColorfulTheme::default();
|
||||||
|
|
||||||
|
let name = if let Some(name) = name {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
let mut names = vec![];
|
||||||
|
for file in fs::read_dir("/etc/vpncloud")? {
|
||||||
|
names.push(file?.path().file_stem().unwrap().to_str().unwrap().to_string());
|
||||||
|
}
|
||||||
|
let selection =
|
||||||
|
Select::with_theme(&theme).with_prompt("Which network?").item("New network").items(&names).interact()?;
|
||||||
|
if selection > 0 {
|
||||||
|
names[selection - 1].clone()
|
||||||
|
} else {
|
||||||
|
Input::with_theme(&theme).with_prompt("Network name").interact_text()?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
let file = Path::new("/etc/vpncloud").join(format!("{}.net", name));
|
||||||
|
if file.exists() {
|
||||||
|
let f = fs::File::open(&file)?;
|
||||||
|
let config_file = serde_yaml::from_reader(f)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to parse config file"))?;
|
||||||
|
config.merge_file(config_file);
|
||||||
|
}
|
||||||
|
if file.parent().unwrap().metadata()?.permissions().readonly() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "Config file not writable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mode = Select::with_theme(&theme)
|
||||||
|
.with_prompt("Configuration mode")
|
||||||
|
.items(&["Simple (minimal options)", "Advanced (some more options)", "Expert (all options)"])
|
||||||
|
.default(MODE_SIMPLE)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
configure_connectivity(&mut config, mode, &theme)?;
|
||||||
|
configure_crypto(&mut config, mode, &theme)?;
|
||||||
|
configure_device(&mut config, mode, &theme)?;
|
||||||
|
configure_addresses(&mut config, mode, &theme)?;
|
||||||
|
configure_beacon(&mut config, mode, &theme)?;
|
||||||
|
configure_stats(&mut config, mode, &theme)?;
|
||||||
|
configure_process(&mut config, mode, &theme)?;
|
||||||
|
configure_hooks(&mut config, mode, &theme)?;
|
||||||
|
if Confirm::with_theme(&theme).with_prompt("Finish configuration?").default(true).interact()? {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Confirm::with_theme(&theme).with_prompt("Save config?").default(true).interact()? {
|
||||||
|
let config_file = config.into_config_file();
|
||||||
|
let f = fs::File::create(&file)?;
|
||||||
|
serde_yaml::to_writer(f, &config_file)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to parse config file"))?;
|
||||||
|
fs::set_permissions(file, fs::Permissions::from_mode(600))?;
|
||||||
|
println!();
|
||||||
|
println!("Use the following commands to control your VPN:");
|
||||||
|
println!(" start the VPN: sudo service vpncloud@{0} start", name);
|
||||||
|
println!(" stop the VPN: sudo service vpncloud@{0} stop", name);
|
||||||
|
println!(" get the status: sudo service vpncloud@{0} status", name);
|
||||||
|
println!(" add VPN to autostart: sudo sysctl enable vpncloud@{0}", name);
|
||||||
|
println!(" remove VPN from autostart: sudo sysctl disable vpncloud@{0}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue