Merge branch 'master' into threading

This commit is contained in:
Dennis Schwerdel 2021-02-15 12:49:37 +01:00
commit caedec6fae
13 changed files with 785 additions and 42 deletions

View File

@ -3,12 +3,27 @@ linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" } objcopy = { path = "arm-linux-gnueabihf-objcopy" }
strip = { path = "arm-linux-gnueabihf-strip" } strip = { path = "arm-linux-gnueabihf-strip" }
[target.armv7-unknown-linux-musleabihf]
linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
strip = { path = "arm-linux-gnueabihf-strip" }
[target.arm-unknown-linux-gnueabihf] [target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc" linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" } objcopy = { path = "arm-linux-gnueabihf-objcopy" }
strip = { path = "arm-linux-gnueabihf-strip" } strip = { path = "arm-linux-gnueabihf-strip" }
[target.arm-unknown-linux-musleabihf]
linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
strip = { path = "arm-linux-gnueabihf-strip" }
[target.aarch64-unknown-linux-gnu] [target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc" linker = "aarch64-linux-gnu-gcc"
objcopy = { path = "aarch64-linux-gnu-objcopy" } objcopy = { path = "aarch64-linux-gnu-objcopy" }
strip = { path = "aarch64-linux-gnu-strip" } strip = { path = "aarch64-linux-gnu-strip" }
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path = "aarch64-linux-gnu-objcopy" }
strip = { path = "aarch64-linux-gnu-strip" }

View File

@ -11,7 +11,7 @@ jobs:
- name: Run builder - name: Run builder
uses: ./.github/actions/build-deb uses: ./.github/actions/build-deb
with: with:
rust: '1.49.0' rust: '1.50.0'
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
@ -31,7 +31,7 @@ jobs:
- name: Run builder - name: Run builder
uses: ./.github/actions/build-rpm uses: ./.github/actions/build-rpm
with: with:
rust: '1.49.0' rust: '1.50.0'
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:

View File

@ -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

55
Cargo.lock generated
View File

@ -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"
@ -913,9 +947,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.8.16" version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd2af560da3c1fdc02cb80965289254fc35dff869810061e2d8290ee48848ae" checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [ dependencies = [
"dtoa", "dtoa",
"linked-hash-map", "linked-hash-map",
@ -1077,6 +1111,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"
@ -1280,6 +1324,7 @@ dependencies = [
"byteorder", "byteorder",
"criterion", "criterion",
"daemonize", "daemonize",
"dialoguer",
"fnv", "fnv",
"iai", "iai",
"igd", "igd",
@ -1443,3 +1488,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"

View File

@ -22,7 +22,6 @@ 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"
@ -30,8 +29,10 @@ byteorder = "1.4"
thiserror = "1.0" thiserror = "1.0"
parking_lot = "*" parking_lot = "*"
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"
@ -39,9 +40,11 @@ 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"]
installer = []
[[bench]] [[bench]]
name = "criterion" name = "criterion"

View File

@ -11,6 +11,7 @@ RUN apt-get update \
libc6-dev-i386 \ libc6-dev-i386 \
gcc-5-multilib \ gcc-5-multilib \
asciidoctor \ asciidoctor \
musl musl-dev musl-tools \
&& rm -rf /var/cache/dpkg && rm -rf /var/cache/dpkg
RUN ln -s asm-generic/ /usr/include/asm RUN ln -s asm-generic/ /usr/include/asm
@ -19,7 +20,7 @@ RUN useradd -ms /bin/bash user
USER user USER user
WORKDIR /home/user WORKDIR /home/user
ENV RUST=1.49.0 ENV RUST=1.50.0
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST} RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}
@ -27,12 +28,18 @@ ENV PATH=/home/user/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
RUN rustup target add i686-unknown-linux-gnu \ RUN rustup target add i686-unknown-linux-gnu \
&& rustup target add armv7-unknown-linux-gnueabihf \ && rustup target add armv7-unknown-linux-gnueabihf \
&& rustup target add aarch64-unknown-linux-gnu && rustup target add aarch64-unknown-linux-gnu \
&& rustup target add x86_64-unknown-linux-musl \
&& rustup target add i686-unknown-linux-musl \
&& rustup target add armv7-unknown-linux-musleabihf \
&& rustup target add aarch64-unknown-linux-musl
RUN cargo install cargo-deb \ RUN cargo install cargo-deb \
&& rm -rf /home/user/.cargo/{git,tmp,registry} && rm -rf /home/user/.cargo/{git,tmp,registry}
ENV UPX_VER=3.96
RUN curl https://github.com/upx/upx/releases/download/v${UPX_VER}/upx-${UPX_VER}-amd64_linux.tar.xz -Lf | tar -xJ --strip-components=1 -C /home/user/.cargo/bin
VOLUME /home/user/.cargo/tmp VOLUME /home/user/.cargo/tmp
VOLUME /home/user/.cargo/git VOLUME /home/user/.cargo/git
VOLUME /home/user/.cargo/registry VOLUME /home/user/.cargo/registry

View File

@ -7,7 +7,7 @@ RUN useradd -ms /bin/bash user
USER user USER user
WORKDIR /home/user WORKDIR /home/user
ENV RUST=1.49.0 ENV RUST=1.50.0
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST} RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}

View File

@ -32,24 +32,44 @@ mkdir -p ../dist
docker build --rm -f=Dockerfile-deb -t vpncloud-builder-deb . docker build --rm -f=Dockerfile-deb -t vpncloud-builder-deb .
# x86_64 deb # x86_64 deb
docker_cmd deb 'cd code && cargo deb' if ! [ -f ../dist/vpncloud_${DEB_VERSION}_amd64.deb ]; then
cp $CACHE/deb/target/debian/vpncloud_${DEB_VERSION}_amd64.deb ../dist/vpncloud_${DEB_VERSION}_amd64.deb docker_cmd deb 'cd code && cargo deb'
cp $CACHE/deb/target/debian/vpncloud_${DEB_VERSION}_amd64.deb ../dist/vpncloud_${DEB_VERSION}_amd64.deb
fi
# i386 deb build_deb() {
docker_cmd deb 'cd code && cargo deb --target i686-unknown-linux-gnu' ARCH=$1
cp $CACHE/deb/target/i686-unknown-linux-gnu/debian/vpncloud_${DEB_VERSION}_i386.deb ../dist/vpncloud_${DEB_VERSION}_i386.deb TARGET=$2
if ! [ -f ../dist/vpncloud_${DEB_VERSION}_${ARCH}.deb ]; then
docker_cmd deb "cd code && cargo deb --target ${TARGET}"
cp $CACHE/deb/target/${TARGET}/debian/vpncloud_${DEB_VERSION}_${ARCH}.deb ../dist/vpncloud_${DEB_VERSION}_${ARCH}.deb
fi
}
# arm7hf deb build_deb i386 i686-unknown-linux-gnu
docker_cmd deb 'cd code && cargo deb --target armv7-unknown-linux-gnueabihf' build_deb armhf armv7-unknown-linux-gnueabihf
cp $CACHE/deb/target/armv7-unknown-linux-gnueabihf/debian/vpncloud_${DEB_VERSION}_armhf.deb ../dist/vpncloud_${DEB_VERSION}_armhf.deb build_deb arm64 aarch64-unknown-linux-gnu
# aarch64 deb
docker_cmd deb 'cd code && cargo deb --target aarch64-unknown-linux-gnu' build_static() {
cp $CACHE/deb/target/aarch64-unknown-linux-gnu/debian/vpncloud_${DEB_VERSION}_arm64.deb ../dist/vpncloud_${DEB_VERSION}_arm64.deb ARCH=$1
TARGET=$2
if ! [ -f ../dist/vpncloud_${VERSION}_static_${ARCH} ]; then
docker_cmd deb "cd code && cargo build --release --features installer --target ${TARGET} && upx --lzma target/${TARGET}/release/vpncloud"
cp $CACHE/deb/target/${TARGET}/release/vpncloud ../dist/vpncloud_${VERSION}_static_${ARCH}
fi
}
build_static amd64 x86_64-unknown-linux-musl
build_static i386 i686-unknown-linux-gnu
build_static armhf armv7-unknown-linux-musleabihf
#build_static arm64 aarch64-unknown-linux-musl # fails for unknown reason
docker build --rm -f=Dockerfile-rpm -t vpncloud-builder-rpm . docker build --rm -f=Dockerfile-rpm -t vpncloud-builder-rpm .
# x86_64 rpm if ! [ -f ../dist/vpncloud_${RPM_VERSION}.x86_64.rpm ]; then
docker_cmd rpm 'cd code && cargo rpm build' # x86_64 rpm
cp $CACHE/rpm/target/release/rpmbuild/RPMS/x86_64/vpncloud-${RPM_VERSION}.x86_64.rpm ../dist/vpncloud_${RPM_VERSION}.x86_64.rpm docker_cmd rpm 'cd code && cargo rpm build'
cp $CACHE/rpm/target/release/rpmbuild/RPMS/x86_64/vpncloud-${RPM_VERSION}.x86_64.rpm ../dist/vpncloud_${RPM_VERSION}.x86_64.rpm
fi

View File

@ -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,22 @@ 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>
},
/// Install required utility files
#[cfg(feature = "installer")]
Install {
/// Remove installed files again
#[structopt(long)]
uninstall: bool
} }
} }

View File

@ -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!(

49
src/installer.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::error::Error;
use std::{
env,
fs::{self, File},
io::Write,
os::unix::fs::PermissionsExt
};
const MANPAGE: &[u8] = include_bytes!("../target/vpncloud.1.gz");
const SERVICE_FILE: &[u8] = include_bytes!("../assets/vpncloud@.service");
const WS_PROXY_SERVICE_FILE: &[u8] = include_bytes!("../assets/vpncloud-wsproxy.service");
const EXAMPLE_CONFIG: &[u8] = include_bytes!("../assets/example.net.disabled");
pub fn install() -> Result<(), Error> {
env::current_exe()
.and_then(|p| fs::copy(p, "/usr/bin/vpncloud"))
.map_err(|e| Error::FileIo("Failed to copy binary", e))?;
fs::set_permissions("/usr/bin/vpncloud", fs::Permissions::from_mode(755))
.map_err(|e| Error::FileIo("Failed to set permissions for binary", e))?;
fs::create_dir_all("/etc/vpncloud").map_err(|e| Error::FileIo("Failed to create config folder", e))?;
fs::set_permissions("/etc/vpncloud", fs::Permissions::from_mode(600))
.map_err(|e| Error::FileIo("Failed to set permissions for config folder", e))?;
File::create("/etc/vpncloud/example.net.disabled")
.and_then(|mut f| f.write_all(EXAMPLE_CONFIG))
.map_err(|e| Error::FileIo("Failed to create example config", e))?;
File::create("/usr/share/man/man1/vpncloud.1.gz")
.and_then(|mut f| f.write_all(MANPAGE))
.map_err(|e| Error::FileIo("Failed to create manpage", e))?;
File::create("/lib/systemd/system/vpncloud@.service")
.and_then(|mut f| f.write_all(SERVICE_FILE))
.map_err(|e| Error::FileIo("Failed to create service file", e))?;
File::create("/lib/systemd/system/vpncloud-wsproxy.service")
.and_then(|mut f| f.write_all(WS_PROXY_SERVICE_FILE))
.map_err(|e| Error::FileIo("Failed to create wsporxy service file", e))?;
info!("Install successful");
Ok(())
}
pub fn uninstall() -> Result<(), Error> {
fs::remove_file("/etc/vpncloud/example.net.disabled").map_err(|e| Error::FileIo("Failed to remove binary", e))?;
fs::remove_file("/usr/share/man/man1/vpncloud.1.gz").map_err(|e| Error::FileIo("Failed to remove manpage", e))?;
fs::remove_file("/lib/systemd/system/vpncloud@.service")
.map_err(|e| Error::FileIo("Failed to remove service file", e))?;
fs::remove_file("/lib/systemd/system/vpncloud-wsproxy.service")
.map_err(|e| Error::FileIo("Failed to remove wsproxy service file", e))?;
fs::remove_file("/usr/bin/vpncloud").map_err(|e| Error::FileIo("Failed to remove binary", e))?;
info!("Uninstall successful");
Ok(())
}

View File

@ -27,7 +27,9 @@ 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;
#[cfg(feature = "installer")] pub mod installer;
use structopt::StructOpt; use structopt::StructOpt;
@ -272,12 +274,23 @@ 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: {}");
}
#[cfg(feature = "installer")]
Command::Install { uninstall } => {
if uninstall {
try_fail!(installer::uninstall(), "Uninstall failed: {}");
} else {
try_fail!(installer::install(), "Install failed: {}");
}
}
} }
return return
} }

512
src/wizard.rs Normal file
View File

@ -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(())
}