Compare commits

...

12 Commits

Author SHA1 Message Date
Dennis Schwerdel d118b96e03 Changes 2021-02-20 01:33:32 +01:00
Dennis Schwerdel c9a0cc85ab Some more code 2021-02-20 00:17:06 +01:00
Dennis Schwerdel 450101d8ec Merge branch 'master' into threading 2021-02-17 22:14:46 +01:00
Dennis Schwerdel 3da0d27eb7 Up deps 2021-02-17 21:18:36 +01:00
Dennis Schwerdel 8e49311fef Container for asciinema recorder 2021-02-16 22:41:14 +01:00
Dennis Schwerdel c63b2d1cd5 Organize contrib folder 2021-02-16 22:37:18 +01:00
Dennis Schwerdel dd168139f0 Compiles but does not work 2021-02-15 13:46:55 +01:00
Dennis Schwerdel caedec6fae Merge branch 'master' into threading 2021-02-15 12:49:37 +01:00
dswd ac95f34402
Merge pull request #158 from dswd/dependabot/cargo/serde_yaml-0.8.17
Bump serde_yaml from 0.8.16 to 0.8.17
2021-02-15 08:19:38 +01:00
Dennis Schwerdel 4950753548 Static builds that can install themselves 2021-02-14 13:27:26 +01:00
Dennis Schwerdel c4ff0ed198 Add config wizard 2021-02-14 00:09:11 +01:00
dependabot[bot] 6b88ac5733
Bump serde_yaml from 0.8.16 to 0.8.17
Bumps [serde_yaml](https://github.com/dtolnay/serde-yaml) from 0.8.16 to 0.8.17.
- [Release notes](https://github.com/dtolnay/serde-yaml/releases)
- [Commits](https://github.com/dtolnay/serde-yaml/compare/0.8.16...0.8.17)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-11 05:20:09 +00:00
38 changed files with 1568 additions and 1118 deletions

View File

@ -3,12 +3,27 @@ linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
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]
linker = "arm-linux-gnueabihf-gcc"
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
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]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path = "aarch64-linux-gnu-objcopy" }
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
uses: ./.github/actions/build-deb
with:
rust: '1.49.0'
rust: '1.50.0'
- name: Archive artifacts
uses: actions/upload-artifact@v1
with:
@ -31,7 +31,7 @@ jobs:
- name: Run builder
uses: ./.github/actions/build-rpm
with:
rust: '1.49.0'
rust: '1.50.0'
- name: Archive artifacts
uses: actions/upload-artifact@v1
with:

View File

@ -2,6 +2,15 @@
This project follows [semantic versioning](http://semver.org).
### UNRELEASED
- [added] Added interactive configuration wizard
- [added] Support for (un-)installation
- [added] Building static binaries
- [changed] Restructured example config
- [changed] Changed Rust version to 1.50.0
- [changed] Updated dependencies
### v2.1.0 (2021-02-06)
- [added] Support for websocket proxy mode

85
Cargo.lock generated
View File

@ -143,6 +143,22 @@ dependencies = [
"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]]
name = "const_fn"
version = "0.4.5"
@ -269,6 +285,18 @@ dependencies = [
"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]]
name = "digest"
version = "0.9.0"
@ -296,6 +324,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "fnv"
version = "1.0.7"
@ -587,14 +621,14 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall 0.1.57",
"redox_syscall",
"smallvec",
"winapi",
]
@ -690,9 +724,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
@ -721,9 +755,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom",
]
@ -764,15 +798,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.1.57"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
"bitflags",
]
@ -913,9 +941,9 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.8.16"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd2af560da3c1fdc02cb80965289254fc35dff869810061e2d8290ee48848ae"
checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [
"dtoa",
"linked-hash-map",
@ -925,9 +953,9 @@ dependencies = [
[[package]]
name = "sha-1"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d"
checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
@ -1072,11 +1100,21 @@ dependencies = [
"cfg-if 1.0.0",
"libc",
"rand",
"redox_syscall 0.2.4",
"redox_syscall",
"remove_dir_all",
"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]]
name = "textwrap"
version = "0.11.0"
@ -1280,6 +1318,7 @@ dependencies = [
"byteorder",
"criterion",
"daemonize",
"dialoguer",
"fnv",
"iai",
"igd",
@ -1443,3 +1482,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"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"
fnv = "1"
yaml-rust = "0.4"
igd = { version = "0.12", optional = true }
daemonize = "0.4"
ring = "0.16"
privdrop = "0.5"
@ -30,8 +29,10 @@ byteorder = "1.4"
thiserror = "1.0"
parking_lot = "*"
smallvec = "1.6"
dialoguer = { version = "0.7", optional = true }
tungstenite = { version = "0.13", optional = true, default-features = false }
url = { version = "2.2", optional = true }
igd = { version = "0.12", optional = true }
[dev-dependencies]
tempfile = "3"
@ -39,9 +40,11 @@ criterion = { version = "0.3", features = ["html_reports"] }
iai = "0.1"
[features]
default = ["nat", "websocket"]
default = ["nat", "websocket", "wizard"]
nat = ["igd"]
websocket = ["tungstenite", "url"]
wizard = ["dialoguer"]
installer = []
[[bench]]
name = "criterion"

View File

@ -11,6 +11,7 @@ RUN apt-get update \
libc6-dev-i386 \
gcc-5-multilib \
asciidoctor \
musl musl-dev musl-tools \
&& rm -rf /var/cache/dpkg
RUN ln -s asm-generic/ /usr/include/asm
@ -19,7 +20,7 @@ RUN useradd -ms /bin/bash user
USER 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}
@ -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 \
&& 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 \
&& 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/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
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}

View File

@ -32,24 +32,44 @@ mkdir -p ../dist
docker build --rm -f=Dockerfile-deb -t vpncloud-builder-deb .
# x86_64 deb
docker_cmd deb 'cd code && cargo deb'
cp $CACHE/deb/target/debian/vpncloud_${DEB_VERSION}_amd64.deb ../dist/vpncloud_${DEB_VERSION}_amd64.deb
if ! [ -f ../dist/vpncloud_${DEB_VERSION}_amd64.deb ]; then
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
docker_cmd deb 'cd code && cargo deb --target i686-unknown-linux-gnu'
cp $CACHE/deb/target/i686-unknown-linux-gnu/debian/vpncloud_${DEB_VERSION}_i386.deb ../dist/vpncloud_${DEB_VERSION}_i386.deb
build_deb() {
ARCH=$1
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
docker_cmd deb 'cd code && cargo deb --target 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 i386 i686-unknown-linux-gnu
build_deb armhf armv7-unknown-linux-gnueabihf
build_deb arm64 aarch64-unknown-linux-gnu
# aarch64 deb
docker_cmd deb 'cd code && cargo deb --target aarch64-unknown-linux-gnu'
cp $CACHE/deb/target/aarch64-unknown-linux-gnu/debian/vpncloud_${DEB_VERSION}_arm64.deb ../dist/vpncloud_${DEB_VERSION}_arm64.deb
build_static() {
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 .
# 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
if ! [ -f ../dist/vpncloud_${RPM_VERSION}.x86_64.rpm ]; then
# 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

@ -0,0 +1,9 @@
FROM ubuntu
RUN apt-get update && apt-get install -y asciinema
RUN mkdir /root/.asciinema
RUN mkdir /etc/vpncloud
WORKDIR /data
ADD config /root/.asciinema/config
RUN echo 'PS1="\[\e[00;34m\]\[\e[01;31m\]\u\[\e[00;01;34m\]@\[\e[00;34m\]node\[\e[01;31m\]:\[\e[00;34m\]\w\[\e[01;31m\]> \[\e[00m\]"' >> /root/.bashrc

View File

@ -0,0 +1,3 @@
[record]
command = /usr/bin/bash -l
idle_time_limit = 2.5

View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
docker build -t asciinema-recorder .
docker run -it --rm --network host -v $(pwd)/../../target/release/:/usr/local/bin/ -v $(pwd):/data asciinema-recorder asciinema "$@"

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 {
match self.keepalive {
Some(dur) => dur,
@ -310,6 +350,34 @@ impl Config {
}
}
pub fn is_learning(&self) -> bool {
match self.mode {
Mode::Normal => {
match self.device_type {
Type::Tap => true,
Type::Tun => false
}
}
Mode::Router => false,
Mode::Switch => true,
Mode::Hub => false
}
}
pub fn is_broadcasting(&self) -> bool {
match self.mode {
Mode::Normal => {
match self.device_type {
Type::Tap => true,
Type::Tun => false
}
}
Mode::Router => false,
Mode::Switch => true,
Mode::Hub => true
}
}
pub fn call_hook(
&self, event: &'static str, envs: impl IntoIterator<Item = (&'static str, impl AsRef<OsStr>)>, detach: bool
) {
@ -525,6 +593,22 @@ pub enum Command {
/// Shell to create completions for
#[structopt(long, default_value="bash")]
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

@ -1,7 +1,7 @@
use super::{core::test_speed, rotate::RotationState};
pub use super::{
core::{CryptoCore, EXTRA_LEN, TAG_LEN},
init::{is_init_message, INIT_MESSAGE_FIRST_BYTE, InitState, InitResult}
init::{is_init_message, InitResult, InitState, INIT_MESSAGE_FIRST_BYTE}
};
use crate::{
error::Error,
@ -69,6 +69,27 @@ pub struct 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> {
let key_pair = if let Some(priv_key) = &config.private_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());
trusted_keys.push(key);
}
let mut algos = Algorithms { algorithm_speeds: smallvec![], allow_unencrypted: false };
let algorithms = config.algorithms.iter().map(|a| a as &str).collect::<Vec<_>>();
let allowed = if algorithms.is_empty() { &DEFAULT_ALGORITHMS } else { &algorithms as &[&str] };
let (unencrypted, allowed_algos) = Self::parse_algorithms(&config.algorithms)?;
if unencrypted {
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 mut speeds = Vec::new();
for name in allowed {
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"))
};
for algo in allowed_algos {
let speed = test_speed(algo, &duration);
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() {
info!(
@ -170,6 +182,14 @@ impl Crypto {
Ok(keypair)
}
pub fn public_key_from_private_key(privkey: &str) -> Result<String, Error> {
let privkey = from_base62(privkey).map_err(|_| Error::InvalidConfig("Failed to parse private key"))?;
let keypair = Ed25519KeyPair::from_seed_unchecked(&privkey)
.map_err(|_| Error::InvalidConfig("Key rejected by crypto library"))?;
let pubkey = to_base62(keypair.public_key().as_ref());
Ok(pubkey)
}
fn parse_public_key(pubkey: &str) -> Result<Ed25519PublicKey, Error> {
let pubkey = from_base62(pubkey).map_err(|_| Error::InvalidConfig("Failed to parse public key"))?;
if pubkey.len() != ED25519_PUBLIC_KEY_LEN {
@ -283,7 +303,7 @@ impl PeerCrypto {
}
}
pub fn every_second(&mut self, out: &mut MsgBuffer) -> MessageResult {
pub fn every_second(&mut self, out: &mut MsgBuffer) {
out.clear();
if let PeerCrypto::Encrypted { core, rotation, rotate_counter, algorithm, .. } = self {
core.every_second();
@ -297,11 +317,9 @@ impl PeerCrypto {
if !out.is_empty() {
out.prepend_byte(MESSAGE_TYPE_ROTATION);
self.encrypt_message(out);
return MessageResult::Reply
}
}
}
MessageResult::None
}
}
@ -345,9 +363,9 @@ mod tests {
assert_eq!(res, InitResult::Success { peer_payload: vec![], is_initiator: true });
assert!(msg.is_empty());
let node1 = node1.finish(&mut msg);
let mut node1 = node1.finish(&mut msg);
assert!(msg.is_empty());
let node2 = node2.finish(&mut msg);
let mut node2 = node2.finish(&mut msg);
assert!(msg.is_empty());
debug!("Node1 <- Node2");
@ -365,21 +383,15 @@ mod tests {
let res = node2.handle_message(&mut buffer).unwrap();
assert_eq!(res, MessageResult::Message(1));
match node1.every_second(&mut msg) {
MessageResult::None => (),
MessageResult::Reply => {
let res = node2.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::None);
}
other => assert_eq!(other, MessageResult::None)
node1.every_second(&mut msg);
if !msg.is_empty() {
let res = node2.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::None);
}
match node2.every_second(&mut msg) {
MessageResult::None => (),
MessageResult::Reply => {
let res = node1.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::None);
}
other => assert_eq!(other, MessageResult::None)
node2.every_second(&mut msg);
if !msg.is_empty() {
let res = node1.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::None);
}
}
}

View File

@ -258,7 +258,7 @@ pub fn create_dummy_pair(algo: &'static aead::Algorithm) -> (CryptoCore, CryptoC
pub fn test_speed(algo: &'static aead::Algorithm, max_time: &Duration) -> f64 {
let mut buffer = MsgBuffer::new(EXTRA_LEN);
buffer.set_length(1000);
let (mut sender, mut receiver) = create_dummy_pair(algo);
let (sender, receiver) = create_dummy_pair(algo);
let mut iterations = 0;
let start = Instant::now();
while (Instant::now() - start).as_nanos() < max_time.as_nanos() {
@ -290,7 +290,7 @@ mod tests {
}
fn test_encrypt_decrypt(algo: &'static aead::Algorithm) {
let (mut sender, mut receiver) = create_dummy_pair(algo);
let (sender, receiver) = create_dummy_pair(algo);
let plain = random_data(1000);
let mut buffer = MsgBuffer::new(EXTRA_LEN);
buffer.clone_from(&plain);
@ -318,7 +318,7 @@ mod tests {
fn test_tampering(algo: &'static aead::Algorithm) {
let (mut sender, mut receiver) = create_dummy_pair(algo);
let (sender, receiver) = create_dummy_pair(algo);
let plain = random_data(1000);
let mut buffer = MsgBuffer::new(EXTRA_LEN);
buffer.clone_from(&plain);
@ -358,7 +358,7 @@ mod tests {
}
fn test_nonce_pinning(algo: &'static aead::Algorithm) {
let (mut sender, mut receiver) = create_dummy_pair(algo);
let (sender, receiver) = create_dummy_pair(algo);
let plain = random_data(1000);
let mut buffer = MsgBuffer::new(EXTRA_LEN);
buffer.clone_from(&plain);
@ -399,7 +399,7 @@ mod tests {
}
fn test_key_rotation(algo: &'static aead::Algorithm) {
let (mut sender, mut receiver) = create_dummy_pair(algo);
let (sender, receiver) = create_dummy_pair(algo);
let plain = random_data(1000);
let mut buffer = MsgBuffer::new(EXTRA_LEN);
buffer.clone_from(&plain);

View File

@ -2,6 +2,7 @@
// Copyright (C) 2015-2021 Dennis Schwerdel
// This software is licensed under GPL-3 or newer (see LICENSE.md)
use parking_lot::Mutex;
use std::{
cmp,
collections::VecDeque,
@ -12,7 +13,8 @@ use std::{
net::{Ipv4Addr, UdpSocket},
os::unix::io::{AsRawFd, RawFd},
str,
str::FromStr
str::FromStr,
sync::Arc
};
use crate::{crypto, error::Error, util::MsgBuffer};
@ -75,7 +77,7 @@ impl FromStr for Type {
}
}
pub trait Device: AsRawFd {
pub trait Device: AsRawFd + Clone + Send + 'static {
/// Returns the type of this device
fn get_type(&self) -> Type;
@ -118,6 +120,16 @@ pub struct TunTapDevice {
}
impl Clone for TunTapDevice {
fn clone(&self) -> Self {
Self {
fd: try_fail!(self.fd.try_clone(), "Failed to clone device: {}"),
ifname: self.ifname.clone(),
type_: self.type_
}
}
}
impl TunTapDevice {
/// Creates a new tun/tap device
///
@ -300,9 +312,10 @@ impl AsRawFd for TunTapDevice {
}
#[derive(Clone)]
pub struct MockDevice {
inbound: VecDeque<Vec<u8>>,
outbound: VecDeque<Vec<u8>>
inbound: Arc<Mutex<VecDeque<Vec<u8>>>>,
outbound: Arc<Mutex<VecDeque<Vec<u8>>>>
}
impl MockDevice {
@ -311,15 +324,15 @@ impl MockDevice {
}
pub fn put_inbound(&mut self, data: Vec<u8>) {
self.inbound.push_back(data)
self.inbound.lock().push_back(data)
}
pub fn pop_outbound(&mut self) -> Option<Vec<u8>> {
self.outbound.pop_front()
self.outbound.lock().pop_front()
}
pub fn has_inbound(&self) -> bool {
!self.inbound.is_empty()
!self.inbound.lock().is_empty()
}
}
@ -333,7 +346,7 @@ impl Device for MockDevice {
}
fn read(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
if let Some(data) = self.inbound.pop_front() {
if let Some(data) = self.inbound.lock().pop_front() {
buffer.clear();
buffer.set_length(data.len());
buffer.message_mut().copy_from_slice(&data);
@ -344,7 +357,7 @@ impl Device for MockDevice {
}
fn write(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
self.outbound.push_back(buffer.message().into());
self.outbound.lock().push_back(buffer.message().into());
Ok(())
}
@ -355,7 +368,10 @@ impl Device for MockDevice {
impl Default for MockDevice {
fn default() -> Self {
Self { outbound: VecDeque::with_capacity(10), inbound: VecDeque::with_capacity(10) }
Self {
outbound: Arc::new(Mutex::new(VecDeque::with_capacity(10))),
inbound: Arc::new(Mutex::new(VecDeque::with_capacity(10)))
}
}
}

View File

@ -8,7 +8,8 @@ use crate::{
messages::MESSAGE_TYPE_DATA,
net::Socket,
util::{MsgBuffer, Time, TimeSource},
Protocol
Protocol,
config::Config
};
use std::{marker::PhantomData, net::SocketAddr};
@ -28,6 +29,20 @@ pub struct DeviceThread<S: Socket, D: Device, P: Protocol, TS: TimeSource> {
}
impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> DeviceThread<S, D, P, TS> {
pub fn new(config: Config, device: D, socket: S, traffic: SharedTraffic, peer_crypto: SharedPeerCrypto, table: SharedTable<TS>) -> Self {
Self {
_dummy_ts: PhantomData,
_dummy_p: PhantomData,
broadcast: config.is_broadcasting(),
socket,
device,
next_housekeep: TS::now(),
traffic,
peer_crypto,
table
}
}
#[inline]
fn send_to(&mut self, addr: SocketAddr, msg: &mut MsgBuffer) -> Result<(), Error> {
debug!("Sending msg with {} bytes to {}", msg.len(), addr);
@ -51,6 +66,8 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> DeviceThread<S, D, P, TS
fn broadcast_msg(&mut self, type_: u8, msg: &mut MsgBuffer) -> Result<(), Error> {
debug!("Broadcasting message type {}, {:?} bytes to {} peers", type_, msg.len(), self.peer_crypto.count());
let mut msg_data = MsgBuffer::new(100);
let traffic = &mut self.traffic;
let socket = &mut self.socket;
self.peer_crypto.for_each(|addr, crypto| {
msg_data.set_start(msg.get_start());
msg_data.set_length(msg.len());
@ -59,8 +76,8 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> DeviceThread<S, D, P, TS
if let Some(crypto) = crypto {
crypto.encrypt(&mut msg_data);
}
self.traffic.count_out_traffic(addr, msg_data.len());
match self.socket.send(msg_data.message(), addr) {
traffic.count_out_traffic(addr, msg_data.len());
match socket.send(msg_data.message(), addr) {
Ok(written) if written == msg_data.len() => Ok(()),
Ok(_) => Err(Error::Socket("Sent out truncated packet")),
Err(e) => Err(Error::SocketIo("IOError when sending", e))
@ -102,15 +119,16 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> DeviceThread<S, D, P, TS
let mut buffer = MsgBuffer::new(SPACE_BEFORE);
loop {
try_fail!(self.device.read(&mut buffer), "Failed to read from device: {}");
//TODO: set and handle timeout
if let Err(e) = self.forward_packet(&mut buffer) {
error!("{}", e);
}
let now = TS::now();
if self.next_housekeep < TS::now() {
if self.next_housekeep < now {
if let Err(e) = self.housekeep() {
error!("{}", e)
}
self.next_housekeep = TS::now() + 1
self.next_housekeep = now + 1
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,31 @@
use crate::error::Error;
use crate::{
config::Config,
crypto::CryptoCore,
engine::{Hash, PeerData, TimeSource},
messages::NodeInfo,
engine::{Hash, TimeSource},
error::Error,
table::ClaimTable,
traffic::TrafficStats,
types::{Address, NodeId, RangeList},
util::MsgBuffer
traffic::{TrafficEntry, TrafficStats},
types::{Address, RangeList},
util::{Duration, MsgBuffer}
};
use parking_lot::Mutex;
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use std::{
collections::HashMap,
io::{self, Write},
net::SocketAddr,
sync::Arc
};
#[derive(Clone)]
pub struct SharedPeerCrypto {
peers: Arc<Mutex<HashMap<SocketAddr, Option<Arc<CryptoCore>>, Hash>>>
}
impl SharedPeerCrypto {
pub fn new() -> Self {
SharedPeerCrypto { peers: Arc::new(Mutex::new(HashMap::default())) }
}
pub fn sync(&mut self) {
// TODO sync if needed
}
@ -25,11 +35,16 @@ impl SharedPeerCrypto {
match peers.get_mut(&peer) {
None => Err(Error::InvalidCryptoState("No crypto found for peer")),
Some(None) => Ok(()),
Some(Some(crypto)) => Ok(crypto.encrypt(data))
}
Some(Some(crypto)) => {
crypto.encrypt(data);
Ok(())
}
}
}
pub fn for_each(&mut self, mut callback: impl FnMut(SocketAddr, Option<Arc<CryptoCore>>) -> Result<(), Error>) -> Result<(), Error> {
pub fn for_each(
&mut self, mut callback: impl FnMut(SocketAddr, Option<Arc<CryptoCore>>) -> Result<(), Error>
) -> Result<(), Error> {
let mut peers = self.peers.lock();
for (k, v) in peers.iter_mut() {
callback(*k, v.clone())?
@ -43,11 +58,16 @@ impl SharedPeerCrypto {
}
#[derive(Clone)]
pub struct SharedTraffic {
traffic: Arc<Mutex<TrafficStats>>
}
impl SharedTraffic {
pub fn new() -> Self {
Self { traffic: Arc::new(Mutex::new(Default::default())) }
}
pub fn sync(&mut self) {
// TODO sync if needed
}
@ -75,31 +95,73 @@ impl SharedTraffic {
pub fn count_invalid_protocol(&self, bytes: usize) {
self.traffic.lock().count_invalid_protocol(bytes);
}
pub fn period(&mut self, cleanup_idle: Option<usize>) {
self.traffic.lock().period(cleanup_idle)
}
pub fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error> {
self.traffic.lock().write_out(out)
}
pub fn total_peer_traffic(&self) -> TrafficEntry {
self.traffic.lock().total_peer_traffic()
}
pub fn total_payload_traffic(&self) -> TrafficEntry {
self.traffic.lock().total_payload_traffic()
}
pub fn dropped(&self) -> TrafficEntry {
self.traffic.lock().dropped.clone()
}
}
#[derive(Clone)]
pub struct SharedTable<TS: TimeSource> {
table: Arc<Mutex<ClaimTable<TS>>>
}
impl<TS: TimeSource> SharedTable<TS> {
pub fn new(config: &Config) -> Self {
let table = ClaimTable::new(config.switch_timeout as Duration, config.peer_timeout as Duration);
SharedTable { table: Arc::new(Mutex::new(table)) }
}
pub fn sync(&mut self) {
// TODO sync if needed
}
pub fn lookup(&self, addr: Address) -> Option<SocketAddr> {
pub fn lookup(&mut self, addr: Address) -> Option<SocketAddr> {
self.table.lock().lookup(addr)
}
pub fn set_claims(&self, peer: SocketAddr, claims: RangeList) {
pub fn set_claims(&mut self, peer: SocketAddr, claims: RangeList) {
self.table.lock().set_claims(peer, claims)
}
pub fn remove_claims(&self, peer: SocketAddr) {
pub fn remove_claims(&mut self, peer: SocketAddr) {
self.table.lock().remove_claims(peer)
}
pub fn cache(&self, addr: Address, peer: SocketAddr) {
pub fn cache(&mut self, addr: Address, peer: SocketAddr) {
self.table.lock().cache(addr, peer)
}
pub fn housekeep(&mut self) {
self.table.lock().housekeep()
}
pub fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error> {
self.table.lock().write_out(out)
}
pub fn cache_len(&self) -> usize {
self.table.lock().cache_len()
}
pub fn claim_len(&self) -> usize {
self.table.lock().claim_len()
}
}

View File

@ -4,26 +4,53 @@ use super::{
};
use crate::{
config::DEFAULT_PEER_TIMEOUT,
crypto::{is_init_message, MessageResult, PeerCrypto, InitState, InitResult},
beacon::BeaconSerializer,
config::{DEFAULT_PEER_TIMEOUT, DEFAULT_PORT},
crypto::{is_init_message, InitResult, InitState, MessageResult},
device::Type,
engine::{addr_nice, resolve, Hash, PeerData},
error::Error,
messages::{AddrList, NodeInfo, PeerInfo},
messages::{
AddrList, NodeInfo, PeerInfo, MESSAGE_TYPE_CLOSE, MESSAGE_TYPE_DATA, MESSAGE_TYPE_KEEPALIVE,
MESSAGE_TYPE_NODE_INFO
},
net::{mapped_addr, Socket},
types::{NodeId, RangeList},
util::{MsgBuffer, Time, TimeSource},
port_forwarding::PortForwarding,
types::{Address, NodeId, Range, RangeList},
util::{MsgBuffer, StatsdMsg, Time, TimeSource},
Config, Crypto, Device, Protocol
};
use rand::{seq::SliceRandom};
use rand::{random, seq::SliceRandom, thread_rng};
use smallvec::{smallvec, SmallVec};
use std::{
cmp::{max, min},
collections::HashMap,
fmt,
io::Cursor,
fs::File,
io,
io::{Cursor, Seek, SeekFrom, Write},
marker::PhantomData,
net::{SocketAddr, ToSocketAddrs},
str::FromStr
};
const MAX_RECONNECT_INTERVAL: u16 = 3600;
const RESOLVE_INTERVAL: Time = 300;
const OWN_ADDRESS_RESET_INTERVAL: Time = 300;
pub const STATS_INTERVAL: Time = 60;
#[derive(Clone)]
pub struct ReconnectEntry {
address: Option<(String, Time)>,
resolved: AddrList,
tries: u16,
timeout: u16,
next: Time,
final_timeout: Option<Time>
}
pub struct SocketThread<S: Socket, D: Device, P: Protocol, TS: TimeSource> {
// Read-only fields
node_id: NodeId,
@ -31,6 +58,7 @@ pub struct SocketThread<S: Socket, D: Device, P: Protocol, TS: TimeSource> {
config: Config,
peer_timeout_publish: u16,
learning: bool,
update_freq: u16,
_dummy_ts: PhantomData<TS>,
_dummy_p: PhantomData<P>,
// Socket-only fields
@ -38,15 +66,83 @@ pub struct SocketThread<S: Socket, D: Device, P: Protocol, TS: TimeSource> {
device: D,
next_housekeep: Time,
own_addresses: AddrList,
next_own_address_reset: Time,
pending_inits: HashMap<SocketAddr, InitState<NodeInfo>, Hash>,
crypto: Crypto,
peers: HashMap<SocketAddr, PeerData, Hash>,
next_peers: Time,
next_stats_out: Time,
next_beacon: Time,
beacon_serializer: BeaconSerializer<TS>,
stats_file: Option<File>,
statsd_server: Option<String>,
reconnect_peers: SmallVec<[ReconnectEntry; 3]>,
// Shared fields
peer_crypto: SharedPeerCrypto,
traffic: SharedTraffic,
table: SharedTable<TS>
table: SharedTable<TS>,
// Should not be here
port_forwarding: Option<PortForwarding> // TODO: 3rd thread
}
impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS> {
pub fn new(
config: Config, device: D, socket: S, traffic: SharedTraffic, peer_crypto: SharedPeerCrypto,
table: SharedTable<TS>, port_forwarding: Option<PortForwarding>, stats_file: Option<File>
) -> Self {
let mut claims = SmallVec::with_capacity(config.claims.len());
for s in &config.claims {
claims.push(try_fail!(Range::from_str(s), "Invalid subnet format: {} ({})", s));
}
if device.get_type() == Type::Tun && config.auto_claim {
match device.get_ip() {
Ok(ip) => {
let range = Range { base: Address::from_ipv4(ip), prefix_len: 32 };
info!("Auto-claiming {} due to interface address", range);
claims.push(range);
}
Err(Error::DeviceIo(_, e)) if e.kind() == io::ErrorKind::AddrNotAvailable => {
info!("No address set on interface.")
}
Err(e) => error!("{}", e)
}
}
let now = TS::now();
let update_freq = config.get_keepalive() as u16;
let node_id = random();
let crypto = Crypto::new(node_id, &config.crypto).unwrap();
let beacon_key = config.beacon_password.as_ref().map(|s| s.as_bytes()).unwrap_or(&[]);
Self {
_dummy_p: PhantomData,
_dummy_ts: PhantomData,
node_id,
claims,
device,
socket,
peer_crypto,
traffic,
table,
learning: config.is_learning(),
next_housekeep: now,
next_beacon: now,
next_peers: now,
next_stats_out: now + STATS_INTERVAL,
next_own_address_reset: now + OWN_ADDRESS_RESET_INTERVAL,
pending_inits: HashMap::default(),
reconnect_peers: SmallVec::new(),
own_addresses: SmallVec::new(),
peers: HashMap::default(),
peer_timeout_publish: config.peer_timeout as u16,
beacon_serializer: BeaconSerializer::new(beacon_key),
port_forwarding,
stats_file,
update_freq,
statsd_server: config.statsd_server.clone(),
crypto: Crypto::new(node_id, &config.crypto).unwrap(),
config
}
}
#[inline]
fn send_to(&mut self, addr: SocketAddr, msg: &MsgBuffer) -> Result<(), Error> {
debug!("Sending msg with {} bytes to {}", msg.len(), addr);
@ -58,6 +154,26 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
}
}
#[inline]
fn broadcast_msg(&mut self, type_: u8, msg: &mut MsgBuffer) -> Result<(), Error> {
debug!("Broadcasting message type {}, {:?} bytes to {} peers", type_, msg.len(), self.peers.len());
let mut msg_data = MsgBuffer::new(100);
for (addr, peer) in &mut self.peers {
msg_data.set_start(msg.get_start());
msg_data.set_length(msg.len());
msg_data.message_mut().clone_from_slice(msg.message());
msg_data.prepend_byte(type_);
peer.crypto.encrypt_message(&mut msg_data);
self.traffic.count_out_traffic(*addr, msg_data.len());
match self.socket.send(msg_data.message(), *addr) {
Ok(written) if written == msg_data.len() => Ok(()),
Ok(_) => Err(Error::Socket("Sent out truncated packet")),
Err(e) => Err(Error::SocketIo("IOError when sending", e))
}?
}
Ok(())
}
fn connect_sock(&mut self, addr: SocketAddr) -> Result<(), Error> {
let addr = mapped_addr(addr);
if self.peers.contains_key(&addr)
@ -72,7 +188,7 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
let mut msg = MsgBuffer::new(SPACE_BEFORE);
init.send_ping(&mut msg);
self.pending_inits.insert(addr, init);
self.send_to(addr, &mut msg)
self.send_to(addr, &msg)
}
pub fn connect<Addr: ToSocketAddrs + fmt::Debug + Clone>(&mut self, addr: Addr) -> Result<(), Error> {
@ -133,7 +249,7 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
info!("Added peer {}", addr_nice(addr));
if let Some(init) = self.pending_inits.remove(&addr) {
msg.clear();
let crypto = init.finish(&mut msg);
let crypto = init.finish(msg);
self.peers.insert(addr, PeerData {
addrs: info.addrs.clone(),
crypto,
@ -231,12 +347,10 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
fn handle_message(&mut self, src: SocketAddr, data: &mut MsgBuffer) -> Result<(), Error> {
let src = mapped_addr(src);
debug!("Received {} bytes from {}", data.len(), src);
if let Some(result) = self.peers.get_mut(&src).map(|peer| {
peer.crypto.handle_message(data)
}) {
if let Some(result) = self.peers.get_mut(&src).map(|peer| peer.crypto.handle_message(data)) {
return self.process_message(src, result?, data)
}
let is_init = is_init_message(data.message());
let is_init = is_init_message(data.message());
if let Some(result) = self.pending_inits.get_mut(&src).map(|init| {
if is_init {
init.handle_init(data)
@ -251,10 +365,8 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
if !data.is_empty() {
self.send_to(src, data)?
}
},
InitResult::Success { peer_payload, is_initiator } => {
self.add_new_peer(src, peer_payload, data)?
}
InitResult::Success { peer_payload, .. } => self.add_new_peer(src, peer_payload, data)?
}
return Ok(())
}
@ -266,61 +378,337 @@ impl<S: Socket, D: Device, P: Protocol, TS: TimeSource> SocketThread<S, D, P, TS
let mut init = self.crypto.peer_instance(self.create_node_info());
let msg_result = init.handle_init(data);
match msg_result {
Ok(res) => {
Ok(_) => {
self.pending_inits.insert(src, init);
self.send_to(src, data)
}
Err(err) => {
self.traffic.count_invalid_protocol(data.len());
return Err(err)
Err(err)
}
}
}
fn housekeep(&mut self) -> Result<(), Error> {
// self.shared.sync();
// * = can be in different thread, ** only with caching/sync
//TODO: peers: timeout **
//TODO: table: timeout **
//TODO: rotate crypto keys
//TODO: time out pending inits
//TODO: extend port forwarding *
//TODO: reset own address **
//TODO: send peer lists **
//TODO: reconnect to peers **
//TODO: write to statsd **
//TODO: write to stats file **
//TODO: read beacon **
//TODO: write beacon **
// TODO: sync
let now = TS::now();
let mut buffer = MsgBuffer::new(SPACE_BEFORE);
let mut del: SmallVec<[SocketAddr; 3]> = SmallVec::new();
for (&addr, ref data) in &self.peers {
if data.timeout < now {
del.push(addr);
}
}
for addr in del {
info!("Forgot peer {} due to timeout", addr_nice(addr));
self.peers.remove(&addr);
self.table.remove_claims(addr);
self.connect_sock(addr)?; // Try to reconnect
}
self.table.housekeep();
self.crypto_housekeep()?;
// Periodically extend the port-forwarding
if let Some(ref mut pfw) = self.port_forwarding {
pfw.check_extend();
}
let now = TS::now();
// Periodically reset own peers
if self.next_own_address_reset <= now {
self.reset_own_addresses().map_err(|err| Error::SocketIo("Failed to get own addresses", err))?;
self.next_own_address_reset = now + OWN_ADDRESS_RESET_INTERVAL;
}
// Periodically send peer list to peers
if self.next_peers <= now {
debug!("Send peer list to all peers");
let info = self.create_node_info();
info.encode(&mut buffer);
self.broadcast_msg(MESSAGE_TYPE_NODE_INFO, &mut buffer)?;
// Reschedule for next update
let min_peer_timeout = self.peers.iter().map(|p| p.1.peer_timeout).min().unwrap_or(DEFAULT_PEER_TIMEOUT);
let interval = min(self.update_freq as u16, max(min_peer_timeout / 2 - 60, 1));
self.next_peers = now + Time::from(interval);
}
self.reconnect_to_peers()?;
if self.next_stats_out < now {
// Write out the statistics
self.write_out_stats().map_err(|err| Error::FileIo("Failed to write stats file", err))?;
self.send_stats_to_statsd()?;
self.next_stats_out = now + STATS_INTERVAL;
self.traffic.period(Some(5));
}
if let Some(peers) = self.beacon_serializer.get_cmd_results() {
debug!("Loaded beacon with peers: {:?}", peers);
for peer in peers {
self.connect_sock(peer)?;
}
}
if self.next_beacon < now {
self.store_beacon()?;
self.load_beacon()?;
self.next_beacon = now + Time::from(self.config.beacon_interval);
}
// TODO: sync peer_crypto
self.table.sync();
self.traffic.sync();
unimplemented!();
}
fn crypto_housekeep(&mut self) -> Result<(), Error> {
let mut msg = MsgBuffer::new(SPACE_BEFORE);
let mut del: SmallVec<[SocketAddr; 4]> = smallvec![];
for addr in self.pending_inits.keys().copied().collect::<SmallVec<[SocketAddr; 4]>>() {
msg.clear();
if self.pending_inits.get_mut(&addr).unwrap().every_second(&mut msg).is_err() {
del.push(addr)
} else if !msg.is_empty() {
self.send_to(addr, &msg)?
}
}
for addr in self.peers.keys().copied().collect::<SmallVec<[SocketAddr; 16]>>() {
msg.clear();
self.peers.get_mut(&addr).unwrap().crypto.every_second(&mut msg);
if !msg.is_empty() {
self.send_to(addr, &msg)?
}
}
for addr in del {
self.pending_inits.remove(&addr);
if self.peers.remove(&addr).is_some() {
self.connect_sock(addr)?;
}
}
Ok(())
}
fn reset_own_addresses(&mut self) -> io::Result<()> {
self.own_addresses.clear();
self.own_addresses.push(self.socket.address().map(mapped_addr)?);
if let Some(ref pfw) = self.port_forwarding {
self.own_addresses.push(pfw.get_internal_ip().into());
self.own_addresses.push(pfw.get_external_ip().into());
}
debug!("Own addresses: {:?}", self.own_addresses);
// TODO: detect address changes and call event
Ok(())
}
/// Stores the beacon
fn store_beacon(&mut self) -> Result<(), Error> {
if let Some(ref path) = self.config.beacon_store {
let peers: SmallVec<[SocketAddr; 3]> =
self.own_addresses.choose_multiple(&mut thread_rng(), 3).cloned().collect();
if let Some(path) = path.strip_prefix('|') {
self.beacon_serializer
.write_to_cmd(&peers, path)
.map_err(|e| Error::BeaconIo("Failed to call beacon command", e))?;
} else {
self.beacon_serializer
.write_to_file(&peers, &path)
.map_err(|e| Error::BeaconIo("Failed to write beacon to file", e))?;
}
}
Ok(())
}
/// Loads the beacon
fn load_beacon(&mut self) -> Result<(), Error> {
let peers;
if let Some(ref path) = self.config.beacon_load {
if let Some(path) = path.strip_prefix('|') {
self.beacon_serializer
.read_from_cmd(path, Some(50))
.map_err(|e| Error::BeaconIo("Failed to call beacon command", e))?;
return Ok(())
} else {
peers = self
.beacon_serializer
.read_from_file(&path, Some(50))
.map_err(|e| Error::BeaconIo("Failed to read beacon from file", e))?;
}
} else {
return Ok(())
}
debug!("Loaded beacon with peers: {:?}", peers);
for peer in peers {
self.connect_sock(peer)?;
}
Ok(())
}
/// Writes out the statistics to a file
fn write_out_stats(&mut self) -> Result<(), io::Error> {
if let Some(ref mut f) = self.stats_file {
debug!("Writing out stats");
f.seek(SeekFrom::Start(0))?;
f.set_len(0)?;
writeln!(f, "peers:")?;
let now = TS::now();
for (addr, data) in &self.peers {
writeln!(
f,
" - \"{}\": {{ ttl_secs: {}, crypto: {} }}",
addr_nice(*addr),
data.timeout - now,
data.crypto.algorithm_name()
)?;
}
writeln!(f)?;
self.table.write_out(f)?;
writeln!(f)?;
self.traffic.write_out(f)?;
writeln!(f)?;
}
Ok(())
}
/// Sends the statistics to a statsd endpoint
fn send_stats_to_statsd(&mut self) -> Result<(), Error> {
if let Some(ref endpoint) = self.statsd_server {
let peer_traffic = self.traffic.total_peer_traffic();
let payload_traffic = self.traffic.total_payload_traffic();
let dropped = &self.traffic.dropped();
let prefix = self.config.statsd_prefix.as_ref().map(|s| s as &str).unwrap_or("vpncloud");
let msg = StatsdMsg::new()
.with_ns(prefix, |msg| {
msg.add("peer_count", self.peers.len(), "g");
msg.add("table_cache_entries", self.table.cache_len(), "g");
msg.add("table_claims", self.table.claim_len(), "g");
msg.with_ns("traffic", |msg| {
msg.with_ns("protocol", |msg| {
msg.with_ns("inbound", |msg| {
msg.add("bytes", peer_traffic.in_bytes, "c");
msg.add("packets", peer_traffic.in_packets, "c");
});
msg.with_ns("outbound", |msg| {
msg.add("bytes", peer_traffic.out_bytes, "c");
msg.add("packets", peer_traffic.out_packets, "c");
});
});
msg.with_ns("payload", |msg| {
msg.with_ns("inbound", |msg| {
msg.add("bytes", payload_traffic.in_bytes, "c");
msg.add("packets", payload_traffic.in_packets, "c");
});
msg.with_ns("outbound", |msg| {
msg.add("bytes", payload_traffic.out_bytes, "c");
msg.add("packets", payload_traffic.out_packets, "c");
});
});
});
msg.with_ns("invalid_protocol_traffic", |msg| {
msg.add("bytes", dropped.in_bytes, "c");
msg.add("packets", dropped.in_packets, "c");
});
msg.with_ns("dropped_payload", |msg| {
msg.add("bytes", dropped.out_bytes, "c");
msg.add("packets", dropped.out_packets, "c");
});
})
.build();
let msg_data = msg.as_bytes();
let addrs = resolve(endpoint)?;
if let Some(addr) = addrs.first() {
match self.socket.send(msg_data, *addr) {
Ok(written) if written == msg_data.len() => Ok(()),
Ok(_) => Err(Error::Socket("Sent out truncated packet")),
Err(e) => Err(Error::SocketIo("IOError when sending", e))
}?
} else {
error!("Failed to resolve statsd server {}", endpoint);
}
}
Ok(())
}
fn reconnect_to_peers(&mut self) -> Result<(), Error> {
let now = TS::now();
// Connect to those reconnect_peers that are due
for entry in self.reconnect_peers.clone() {
if entry.next > now {
continue
}
self.connect(&entry.resolved as &[SocketAddr])?;
}
for entry in &mut self.reconnect_peers {
// Schedule for next second if node is connected
for addr in &entry.resolved {
if self.peers.contains_key(&addr) {
entry.tries = 0;
entry.timeout = 1;
entry.next = now + 1;
continue
}
}
// Resolve entries anew
if let Some((ref address, ref mut next_resolve)) = entry.address {
if *next_resolve <= now {
match resolve(address as &str) {
Ok(addrs) => entry.resolved = addrs,
Err(_) => {
match resolve(&format!("{}:{}", address, DEFAULT_PORT)) {
Ok(addrs) => entry.resolved = addrs,
Err(err) => warn!("Failed to resolve {}: {}", address, err)
}
}
}
*next_resolve = now + RESOLVE_INTERVAL;
}
}
// Ignore if next attempt is already in the future
if entry.next > now {
continue
}
// Exponential back-off: every 10 tries, the interval doubles
entry.tries += 1;
if entry.tries > 10 {
entry.tries = 0;
entry.timeout *= 2;
}
// Maximum interval is one hour
if entry.timeout > MAX_RECONNECT_INTERVAL {
entry.timeout = MAX_RECONNECT_INTERVAL;
}
// Schedule next connection attempt
entry.next = now + Time::from(entry.timeout);
}
self.reconnect_peers.retain(|e| e.final_timeout.unwrap_or(now) >= now);
Ok(())
}
pub fn run(mut self) {
let mut buffer = MsgBuffer::new(SPACE_BEFORE);
loop {
let src = try_fail!(self.socket.receive(&mut buffer), "Failed to read from network socket: {}");
match self.handle_message(src, &mut buffer) {
Err(e @ Error::CryptoInitFatal(_)) => {
debug!("Fatal crypto init error from {}: {}", src, e);
info!("Closing pending connection to {} due to error in crypto init", addr_nice(src));
self.pending_inits.remove(&src);
match self.socket.receive(&mut buffer) {
Err(err) => {
if err.kind() == io::ErrorKind::TimedOut || err.kind() == io::ErrorKind::WouldBlock {
// ok, this is a normal timeout
} else {
fail!("Failed to read from network socket: {}", err);
}
}
Err(e @ Error::CryptoInit(_)) => {
debug!("Recoverable init error from {}: {}", src, e);
info!("Ignoring invalid init message from peer {}", addr_nice(src));
Ok(src) => {
match self.handle_message(src, &mut buffer) {
Err(e @ Error::CryptoInitFatal(_)) => {
debug!("Fatal crypto init error from {}: {}", src, e);
info!("Closing pending connection to {} due to error in crypto init", addr_nice(src));
self.pending_inits.remove(&src);
}
Err(e @ Error::CryptoInit(_)) => {
debug!("Recoverable init error from {}: {}", src, e);
info!("Ignoring invalid init message from peer {}", addr_nice(src));
}
Err(e) => {
error!("{}", e);
}
Ok(_) => {}
}
}
Err(e) => {
error!("{}", e);
}
Ok(_) => {}
}
let now = TS::now();
if self.next_housekeep < TS::now() {
if self.next_housekeep < now {
if let Err(e) = self.housekeep() {
error!("{}", e)
}
self.next_housekeep = TS::now() + 1
self.next_housekeep = now + 1
}
}
}

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,14 +27,16 @@ pub mod port_forwarding;
pub mod table;
pub mod traffic;
pub mod types;
#[cfg(feature = "wizard")] pub mod wizard;
#[cfg(feature = "websocket")] pub mod wsproxy;
#[cfg(feature = "installer")] pub mod installer;
use structopt::StructOpt;
use std::{
fs::{self, File, Permissions},
io::{self, Write},
net::{Ipv4Addr, UdpSocket},
net::{Ipv4Addr},
os::unix::fs::PermissionsExt,
path::Path,
process,
@ -48,7 +50,7 @@ use crate::{
config::{Args, Command, Config, DEFAULT_PORT},
crypto::Crypto,
device::{Device, TunTapDevice, Type},
net::Socket,
net::{Socket, NetSocket},
oldconfig::OldConfigFile,
payload::Protocol,
util::SystemTimeSource,
@ -183,6 +185,7 @@ fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
Some(file)
}
};
let ifname = device.ifname().to_string();
let mut cloud =
GenericCloud::<TunTapDevice, P, S, SystemTimeSource>::new(&config, socket, device, port_forwarding, stats_file);
for mut addr in config.peers {
@ -190,8 +193,7 @@ fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
// : not present or only in IPv6 address
addr = format!("{}:{}", addr, DEFAULT_PORT)
}
try_fail!(cloud.connect(&addr as &str), "Failed to send message to {}: {}", &addr);
cloud.add_reconnect_peer(addr);
try_fail!(cloud.add_peer(addr.clone()), "Failed to send message to {}: {}", &addr);
}
if config.daemonize {
info!("Running process as daemon");
@ -221,7 +223,7 @@ fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
}
cloud.run();
if let Some(script) = config.ifdown {
run_script(&script, cloud.ifname());
run_script(&script, &ifname);
}
}
@ -272,12 +274,23 @@ fn main() {
}
Command::Completion { shell } => {
Args::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
return
}
#[cfg(feature = "websocket")]
Command::WsProxy { listen } => {
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
}
@ -315,7 +328,7 @@ fn main() {
}
return
}
let socket = try_fail!(UdpSocket::listen(&config.listen), "Failed to open socket {}: {}", config.listen);
let socket = try_fail!(NetSocket::listen(&config.listen), "Failed to open socket {}: {}", config.listen);
match config.device_type {
Type::Tap => run::<payload::Frame, _>(config, socket),
Type::Tun => run::<payload::Packet, _>(config, socket)

View File

@ -2,17 +2,21 @@
// Copyright (C) 2015-2021 Dennis Schwerdel
// This software is licensed under GPL-3 or newer (see LICENSE.md)
use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource};
use crate::port_forwarding::PortForwarding;
use parking_lot::Mutex;
use std::{
collections::{HashMap, VecDeque},
io::{self, ErrorKind},
net::{IpAddr, SocketAddr, UdpSocket, Ipv6Addr},
net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket},
os::unix::io::{AsRawFd, RawFd},
sync::atomic::{AtomicBool, Ordering}
sync::{
atomic::{AtomicBool, Ordering},
Arc
},
time::Duration
};
use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource};
use crate::port_forwarding::PortForwarding;
pub fn mapped_addr(addr: SocketAddr) -> SocketAddr {
// HOT PATH
match addr {
@ -27,7 +31,7 @@ pub fn get_ip() -> IpAddr {
s.local_addr().unwrap().ip()
}
pub trait Socket: AsRawFd + Sized {
pub trait Socket: AsRawFd + Sized + Clone + Send + 'static {
fn listen(addr: &str) -> Result<Self, io::Error>;
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error>;
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error>;
@ -47,25 +51,42 @@ pub fn parse_listen(addr: &str) -> SocketAddr {
}
}
impl Socket for UdpSocket {
pub struct NetSocket(UdpSocket);
impl Clone for NetSocket {
fn clone(&self) -> Self {
Self(try_fail!(self.0.try_clone(), "Failed to clone socket: {}"))
}
}
impl AsRawFd for NetSocket {
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
impl Socket for NetSocket {
fn listen(addr: &str) -> Result<Self, io::Error> {
let addr = parse_listen(addr);
UdpSocket::bind(addr)
Ok(NetSocket(UdpSocket::bind(addr).and_then(|s| {
s.set_read_timeout(Some(Duration::from_secs(1)))?;
Ok(s)
})?))
}
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
buffer.clear();
let (size, addr) = self.recv_from(buffer.buffer())?;
let (size, addr) = self.0.recv_from(buffer.buffer())?;
buffer.set_length(size);
Ok(addr)
}
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
self.send_to(data, addr)
self.0.send_to(data, addr)
}
fn address(&self) -> Result<SocketAddr, io::Error> {
let mut addr = self.local_addr()?;
let mut addr = self.0.local_addr()?;
addr.set_ip(get_ip());
Ok(addr)
}
@ -79,22 +100,24 @@ thread_local! {
static MOCK_SOCKET_NAT: AtomicBool = AtomicBool::new(false);
}
#[derive(Clone)]
pub struct MockSocket {
nat: bool,
nat_peers: HashMap<SocketAddr, Time>,
nat_peers: Arc<Mutex<HashMap<SocketAddr, Time>>>,
address: SocketAddr,
outbound: VecDeque<(SocketAddr, Vec<u8>)>,
inbound: VecDeque<(SocketAddr, Vec<u8>)>
outbound: Arc<Mutex<VecDeque<(SocketAddr, Vec<u8>)>>>,
inbound: Arc<Mutex<VecDeque<(SocketAddr, Vec<u8>)>>>
}
impl MockSocket {
pub fn new(address: SocketAddr) -> Self {
Self {
nat: Self::get_nat(),
nat_peers: HashMap::new(),
nat_peers: Default::default(),
address,
outbound: VecDeque::with_capacity(10),
inbound: VecDeque::with_capacity(10)
outbound: Arc::new(Mutex::new(VecDeque::with_capacity(10))),
inbound: Arc::new(Mutex::new(VecDeque::with_capacity(10)))
}
}
@ -108,12 +131,12 @@ impl MockSocket {
pub fn put_inbound(&mut self, from: SocketAddr, data: Vec<u8>) -> bool {
if !self.nat {
self.inbound.push_back((from, data));
self.inbound.lock().push_back((from, data));
return true
}
if let Some(timeout) = self.nat_peers.get(&from) {
if let Some(timeout) = self.nat_peers.lock().get(&from) {
if *timeout >= MockTimeSource::now() {
self.inbound.push_back((from, data));
self.inbound.lock().push_back((from, data));
return true
}
}
@ -122,7 +145,7 @@ impl MockSocket {
}
pub fn pop_outbound(&mut self) -> Option<(SocketAddr, Vec<u8>)> {
self.outbound.pop_front()
self.outbound.lock().pop_front()
}
}
@ -138,7 +161,7 @@ impl Socket for MockSocket {
}
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
if let Some((addr, data)) = self.inbound.pop_front() {
if let Some((addr, data)) = self.inbound.lock().pop_front() {
buffer.clear();
buffer.set_length(data.len());
buffer.message_mut().copy_from_slice(&data);
@ -149,9 +172,9 @@ impl Socket for MockSocket {
}
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
self.outbound.push_back((addr, data.into()));
self.outbound.lock().push_back((addr, data.into()));
if self.nat {
self.nat_peers.insert(addr, MockTimeSource::now() + 300);
self.nat_peers.lock().insert(addr, MockTimeSource::now() + 300);
}
Ok(data.len())
}
@ -178,4 +201,4 @@ mod bench {
b.iter(|| sock.send_to(&data, &addr).unwrap());
b.bytes = 1400;
}
}
}

View File

@ -5,7 +5,7 @@
use crate::{error::Error, types::Address};
use std::io::{Cursor, Read};
pub trait Protocol: Sized {
pub trait Protocol: Sized + Send + 'static {
fn parse(_: &[u8]) -> Result<(Address, Address), Error>;
}

View File

@ -16,7 +16,7 @@ use super::{
};
#[derive(Default)]
#[derive(Default, Clone)]
pub struct TrafficEntry {
pub out_bytes_total: u64,
pub out_packets_total: usize,

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

View File

@ -13,6 +13,7 @@ use std::{
io::{self, Cursor, Read, Write},
net::{Ipv6Addr, SocketAddr, SocketAddrV6, TcpListener, TcpStream, UdpSocket},
os::unix::io::{AsRawFd, RawFd},
sync::Arc,
thread::spawn
};
use tungstenite::{client::AutoStream, connect, protocol::WebSocket, server::accept, Message};
@ -108,17 +109,21 @@ pub fn run_proxy(listen: &str) -> Result<(), io::Error> {
Ok(())
}
#[derive(Clone)]
pub struct ProxyConnection {
addr: SocketAddr,
socket: WebSocket<AutoStream>
socket: Arc<WebSocket<AutoStream>>
}
impl ProxyConnection {
fn read_message(&mut self) -> Result<Vec<u8>, io::Error> {
loop {
unimplemented!();
/*
if let Message::Binary(data) = io_error!(self.socket.read_message(), "Failed to read from ws proxy: {}")? {
return Ok(data)
}
*/
}
}
}
@ -128,14 +133,14 @@ impl AsRawFd for ProxyConnection {
self.socket.get_ref().as_raw_fd()
}
}
impl Socket for ProxyConnection {
fn listen(url: &str) -> Result<Self, io::Error> {
let parsed_url = io_error!(Url::parse(url), "Invalid URL {}: {}", url)?;
let (mut socket, _) = io_error!(connect(parsed_url), "Failed to connect to URL {}: {}", url)?;
socket.get_mut().set_nodelay(true)?;
let addr = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
let mut con = ProxyConnection { addr, socket };
let mut con = ProxyConnection { addr, socket: Arc::new(socket) };
let addr_data = con.read_message()?;
con.addr = read_addr(Cursor::new(&addr_data))?;
Ok(con)
@ -153,8 +158,11 @@ impl Socket for ProxyConnection {
let mut msg = Vec::with_capacity(data.len() + 18);
write_addr(addr, &mut msg)?;
msg.write_all(data)?;
unimplemented!();
/*
io_error!(self.socket.write_message(Message::Binary(msg)), "Failed to write to ws proxy: {}")?;
Ok(data.len())
*/
}
fn address(&self) -> Result<SocketAddr, io::Error> {