From d50490ac517fc64fdd5aa559a09d16843e83e2c6 Mon Sep 17 00:00:00 2001 From: Dennis Schwerdel Date: Sun, 20 Dec 2020 01:40:32 +0100 Subject: [PATCH] Client works --- Cargo.lock | 241 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/cloud.rs | 6 +- src/config.rs | 290 +++++++++++++++++++++++++------------------------ src/main.rs | 126 ++++++++++++--------- src/net.rs | 33 +++++- src/wsproxy.rs | 102 +++++++++++++++++ 7 files changed, 599 insertions(+), 201 deletions(-) create mode 100644 src/wsproxy.rs diff --git a/Cargo.lock b/Cargo.lock index fda0d79..c5ea152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,18 +31,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "base-x" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "boxfnonce" version = "0.1.1" @@ -106,6 +127,28 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826" +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + [[package]] name = "daemonize" version = "0.4.1" @@ -116,6 +159,15 @@ dependencies = [ "libc", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "discard" version = "1.0.4" @@ -134,6 +186,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.0" @@ -144,6 +211,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -195,6 +272,12 @@ dependencies = [ "itoa", ] +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + [[package]] name = "idna" version = "0.2.0" @@ -219,6 +302,15 @@ dependencies = [ "xmltree", ] +[[package]] +name = "input_buffer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" +dependencies = [ + "bytes", +] + [[package]] name = "itoa" version = "0.4.6" @@ -267,6 +359,24 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "native-tls" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.14.1" @@ -298,12 +408,57 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -494,6 +649,39 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "security-framework" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -549,6 +737,19 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.6.0" @@ -772,6 +973,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tungstenite" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "native-tls", + "rand 0.7.3", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -826,6 +1053,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" + +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + [[package]] name = "vec_map" version = "0.8.2" @@ -866,6 +1105,8 @@ dependencies = [ "tempfile", "thiserror", "time", + "tungstenite", + "url", "yaml-rust", ] diff --git a/Cargo.toml b/Cargo.toml index 9ff9d3a..31fe59f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ privdrop = "0.5" byteorder = "1.3" thiserror = "1.0" smallvec = "1.5" +tungstenite = "*" +url = "*" [dev-dependencies] tempfile = "3" diff --git a/src/cloud.rs b/src/cloud.rs index 1a3533a..23100ea 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -98,11 +98,7 @@ pub struct GenericCloud { impl GenericCloud { #[allow(clippy::too_many_arguments)] - pub fn new(config: &Config, device: D, port_forwarding: Option, stats_file: Option) -> Self { - let socket = match S::listen(config.listen) { - Ok(socket) => socket, - Err(err) => fail!("Failed to open socket {}: {}", config.listen, err) - }; + pub fn new(config: &Config, socket: S, device: D, port_forwarding: Option, stats_file: Option) -> Self { let (learning, broadcast) = match config.mode { Mode::Normal => { match config.device_type { diff --git a/src/config.rs b/src/config.rs index cec0309..0958c92 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,29 +5,12 @@ use super::{device::Type, types::Mode, util::Duration}; pub use crate::crypto::Config as CryptoConfig; -use std::{ - cmp::max, - net::{IpAddr, Ipv6Addr, SocketAddr} -}; +use std::cmp::max; use structopt::StructOpt; - pub const DEFAULT_PEER_TIMEOUT: u16 = 300; pub const DEFAULT_PORT: u16 = 3210; - -fn parse_listen(addr: &str) -> SocketAddr { - if let Some(addr) = addr.strip_prefix("*:") { - let port = try_fail!(addr.parse::(), "Invalid port: {}"); - SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port) - } else if addr.contains(':') { - try_fail!(addr.parse::(), "Invalid address: {}: {}", addr) - } else { - let port = try_fail!(addr.parse::(), "Invalid port: {}"); - SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port) - } -} - #[derive(Deserialize, Debug, PartialEq, Clone)] pub struct Config { pub device_type: Type, @@ -41,7 +24,7 @@ pub struct Config { pub crypto: CryptoConfig, - pub listen: SocketAddr, + pub listen: String, pub peers: Vec, pub peer_timeout: Duration, pub keepalive: Option, @@ -60,7 +43,7 @@ pub struct Config { pub statsd_server: Option, pub statsd_prefix: Option, pub user: Option, - pub group: Option + pub group: Option, } impl Default for Config { @@ -74,7 +57,7 @@ impl Default for Config { ifup: None, ifdown: None, crypto: CryptoConfig::default(), - listen: "[::]:3210".parse::().unwrap(), + listen: "[::]:3210".to_string(), peers: vec![], peer_timeout: DEFAULT_PEER_TIMEOUT as Duration, keepalive: None, @@ -93,7 +76,7 @@ impl Default for Config { statsd_server: None, statsd_prefix: None, user: None, - group: None + group: None, } } } @@ -125,7 +108,7 @@ impl Config { self.ifdown = Some(val); } if let Some(val) = file.listen { - self.listen = parse_listen(&val); + self.listen = val; } if let Some(mut val) = file.peers { self.peers.append(&mut val); @@ -223,7 +206,7 @@ impl Config { self.ifdown = Some(val); } if let Some(val) = args.listen { - self.listen = parse_listen(&val); + self.listen = val; } self.peers.append(&mut args.peers); if let Some(val) = args.peer_timeout { @@ -296,12 +279,11 @@ impl Config { pub fn get_keepalive(&self) -> Duration { match self.keepalive { Some(dur) => dur, - None => max(self.peer_timeout / 2 - 60, 1) + None => max(self.peer_timeout / 2 - 60, 1), } } } - #[derive(StructOpt, Debug, Default)] pub struct Args { /// Read configuration options from the specified file. @@ -325,7 +307,7 @@ pub struct Args { pub mode: Option, /// The shared password to encrypt all traffic - #[structopt(short, long, required_unless_one = &["private-key", "config", "genkey", "version"], env)] + #[structopt(short, long, env, global = true)] pub password: Option, /// The private key to use @@ -416,10 +398,6 @@ pub struct Args { #[structopt(long)] pub version: bool, - /// Generate and print a key-pair and exit - #[structopt(long, conflicts_with = "private_key")] - pub genkey: bool, - /// Disable automatic port forwarding #[structopt(long)] pub no_port_forwarding: bool, @@ -456,9 +434,28 @@ pub struct Args { #[structopt(long)] pub log_file: Option, - /// Migrate an old config file - #[structopt(long, alias = "migrate", requires = "config")] - pub migrate_config: bool + #[structopt(subcommand)] + pub cmd: Option, +} + +#[derive(StructOpt, Debug)] +pub enum Command { + /// Generate and print a key-pair and exit + #[structopt(name = "genkey", alias = "gen-key")] + GenKey, + + WsProxy, + + WsClient { + url: String, + }, + + #[structopt(alias = "migrate")] + MigrateConfig { + /// Config file + #[structopt(long)] + config_file: String, + }, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] @@ -468,7 +465,7 @@ pub struct ConfigFileDevice { pub type_: Option, pub name: Option, pub path: Option, - pub fix_rp_filter: Option + pub fix_rp_filter: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] @@ -477,14 +474,14 @@ pub struct ConfigFileBeacon { pub store: Option, pub load: Option, pub interval: Option, - pub password: Option + pub password: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] #[serde(rename_all = "kebab-case", deny_unknown_fields, default)] pub struct ConfigFileStatsd { pub server: Option, - pub prefix: Option + pub prefix: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] @@ -512,10 +509,9 @@ pub struct ConfigFile { pub stats_file: Option, pub statsd: Option, pub user: Option, - pub group: Option + pub group: Option, } - #[test] fn config_file() { let config_file = " @@ -549,41 +545,44 @@ statsd: server: example.com:1234 prefix: prefix "; - assert_eq!(serde_yaml::from_str::(config_file).unwrap(), ConfigFile { - device: Some(ConfigFileDevice { - type_: Some(Type::Tun), - name: Some("vpncloud%d".to_string()), - path: Some("/dev/net/tun".to_string()), - fix_rp_filter: None - }), - ip: Some("10.0.1.1/16".to_string()), - ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), - ifdown: Some("true".to_string()), - crypto: CryptoConfig::default(), - listen: None, - peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]), - peer_timeout: Some(600), - keepalive: Some(840), - beacon: Some(ConfigFileBeacon { - store: Some("/run/vpncloud.beacon.out".to_string()), - load: Some("/run/vpncloud.beacon.in".to_string()), - interval: Some(3600), - password: Some("test123".to_string()) - }), - mode: Some(Mode::Normal), - switch_timeout: Some(300), - claims: Some(vec!["10.0.1.0/24".to_string()]), - auto_claim: None, - port_forwarding: Some(true), - user: Some("nobody".to_string()), - group: Some("nogroup".to_string()), - pid_file: Some("/run/vpncloud.run".to_string()), - stats_file: Some("/var/log/vpncloud.stats".to_string()), - statsd: Some(ConfigFileStatsd { - server: Some("example.com:1234".to_string()), - prefix: Some("prefix".to_string()) - }) - }) + assert_eq!( + serde_yaml::from_str::(config_file).unwrap(), + ConfigFile { + device: Some(ConfigFileDevice { + type_: Some(Type::Tun), + name: Some("vpncloud%d".to_string()), + path: Some("/dev/net/tun".to_string()), + fix_rp_filter: None + }), + ip: Some("10.0.1.1/16".to_string()), + ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), + ifdown: Some("true".to_string()), + crypto: CryptoConfig::default(), + listen: None, + peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]), + peer_timeout: Some(600), + keepalive: Some(840), + beacon: Some(ConfigFileBeacon { + store: Some("/run/vpncloud.beacon.out".to_string()), + load: Some("/run/vpncloud.beacon.in".to_string()), + interval: Some(3600), + password: Some("test123".to_string()) + }), + mode: Some(Mode::Normal), + switch_timeout: Some(300), + claims: Some(vec!["10.0.1.0/24".to_string()]), + auto_claim: None, + port_forwarding: Some(true), + user: Some("nobody".to_string()), + group: Some("nogroup".to_string()), + pid_file: Some("/run/vpncloud.run".to_string()), + stats_file: Some("/var/log/vpncloud.stats".to_string()), + statsd: Some(ConfigFileStatsd { + server: Some("example.com:1234".to_string()), + prefix: Some("prefix".to_string()) + }) + } + ) } #[test] @@ -597,7 +596,7 @@ fn default_config_as_default() { ifup: None, ifdown: None, crypto: CryptoConfig::default(), - listen: "[::]:3210".parse::().unwrap(), + listen: "[::]:3210".to_string(), peers: vec![], peer_timeout: 0, keepalive: None, @@ -616,9 +615,10 @@ fn default_config_as_default() { statsd_server: None, statsd_prefix: None, user: None, - group: None + group: None, }; - let default_config_file = serde_yaml::from_str::(include_str!("../assets/example.net.disabled")).unwrap(); + let default_config_file = + serde_yaml::from_str::(include_str!("../assets/example.net.disabled")).unwrap(); default_config.merge_file(default_config_file); assert_eq!(default_config, Config::default()); } @@ -631,7 +631,7 @@ fn config_merge() { type_: Some(Type::Tun), name: Some("vpncloud%d".to_string()), path: None, - fix_rp_filter: None + fix_rp_filter: None, }), ip: None, ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), @@ -645,7 +645,7 @@ fn config_merge() { store: Some("/run/vpncloud.beacon.out".to_string()), load: Some("/run/vpncloud.beacon.in".to_string()), interval: Some(7200), - password: Some("test123".to_string()) + password: Some("test123".to_string()), }), mode: Some(Mode::Normal), switch_timeout: Some(300), @@ -658,36 +658,39 @@ fn config_merge() { stats_file: Some("/var/log/vpncloud.stats".to_string()), statsd: Some(ConfigFileStatsd { server: Some("example.com:1234".to_string()), - prefix: Some("prefix".to_string()) - }) - }); - assert_eq!(config, Config { - device_type: Type::Tun, - device_name: "vpncloud%d".to_string(), - device_path: None, - ip: None, - ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), - ifdown: Some("true".to_string()), - listen: "[::]:3210".parse::().unwrap(), - peers: vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()], - peer_timeout: 600, - keepalive: Some(840), - switch_timeout: 300, - beacon_store: Some("/run/vpncloud.beacon.out".to_string()), - beacon_load: Some("/run/vpncloud.beacon.in".to_string()), - beacon_interval: 7200, - beacon_password: Some("test123".to_string()), - mode: Mode::Normal, - port_forwarding: true, - claims: vec!["10.0.1.0/24".to_string()], - user: Some("nobody".to_string()), - group: Some("nogroup".to_string()), - pid_file: Some("/run/vpncloud.run".to_string()), - stats_file: Some("/var/log/vpncloud.stats".to_string()), - statsd_server: Some("example.com:1234".to_string()), - statsd_prefix: Some("prefix".to_string()), - ..Default::default() + prefix: Some("prefix".to_string()), + }), }); + assert_eq!( + config, + Config { + device_type: Type::Tun, + device_name: "vpncloud%d".to_string(), + device_path: None, + ip: None, + ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), + ifdown: Some("true".to_string()), + listen: "[::]:3210".to_string(), + peers: vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()], + peer_timeout: 600, + keepalive: Some(840), + switch_timeout: 300, + beacon_store: Some("/run/vpncloud.beacon.out".to_string()), + beacon_load: Some("/run/vpncloud.beacon.in".to_string()), + beacon_interval: 7200, + beacon_password: Some("test123".to_string()), + mode: Mode::Normal, + port_forwarding: true, + claims: vec!["10.0.1.0/24".to_string()], + user: Some("nobody".to_string()), + group: Some("nogroup".to_string()), + pid_file: Some("/run/vpncloud.run".to_string()), + stats_file: Some("/var/log/vpncloud.stats".to_string()), + statsd_server: Some("example.com:1234".to_string()), + statsd_prefix: Some("prefix".to_string()), + ..Default::default() + } + ); config.merge_args(Args { type_: Some(Type::Tap), device: Some("vpncloud0".to_string()), @@ -716,38 +719,41 @@ fn config_merge() { group: Some("root".to_string()), ..Default::default() }); - assert_eq!(config, Config { - device_type: Type::Tap, - device_name: "vpncloud0".to_string(), - device_path: Some("/dev/null".to_string()), - fix_rp_filter: false, - ip: None, - ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()), - ifdown: Some("ifconfig $IFNAME down".to_string()), - crypto: CryptoConfig { password: Some("anothersecret".to_string()), ..CryptoConfig::default() }, - listen: "[::]:3211".parse::().unwrap(), - peers: vec![ - "remote.machine.foo:3210".to_string(), - "remote.machine.bar:3210".to_string(), - "another:3210".to_string() - ], - peer_timeout: 1801, - keepalive: Some(850), - switch_timeout: 301, - beacon_store: Some("/run/vpncloud.beacon.out2".to_string()), - beacon_load: Some("/run/vpncloud.beacon.in2".to_string()), - beacon_interval: 3600, - beacon_password: Some("test1234".to_string()), - mode: Mode::Switch, - port_forwarding: false, - claims: vec!["10.0.1.0/24".to_string()], - auto_claim: true, - user: Some("root".to_string()), - group: Some("root".to_string()), - pid_file: Some("/run/vpncloud-mynet.run".to_string()), - stats_file: Some("/var/log/vpncloud-mynet.stats".to_string()), - statsd_server: Some("example.com:2345".to_string()), - statsd_prefix: Some("prefix2".to_string()), - daemonize: true - }); + assert_eq!( + config, + Config { + device_type: Type::Tap, + device_name: "vpncloud0".to_string(), + device_path: Some("/dev/null".to_string()), + fix_rp_filter: false, + ip: None, + ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()), + ifdown: Some("ifconfig $IFNAME down".to_string()), + crypto: CryptoConfig { password: Some("anothersecret".to_string()), ..CryptoConfig::default() }, + listen: "[::]:3211".to_string(), + peers: vec![ + "remote.machine.foo:3210".to_string(), + "remote.machine.bar:3210".to_string(), + "another:3210".to_string() + ], + peer_timeout: 1801, + keepalive: Some(850), + switch_timeout: 301, + beacon_store: Some("/run/vpncloud.beacon.out2".to_string()), + beacon_load: Some("/run/vpncloud.beacon.in2".to_string()), + beacon_interval: 3600, + beacon_password: Some("test1234".to_string()), + mode: Mode::Switch, + port_forwarding: false, + claims: vec!["10.0.1.0/24".to_string()], + auto_claim: true, + user: Some("root".to_string()), + group: Some("root".to_string()), + pid_file: Some("/run/vpncloud-mynet.run".to_string()), + stats_file: Some("/var/log/vpncloud-mynet.stats".to_string()), + statsd_server: Some("example.com:2345".to_string()), + statsd_prefix: Some("prefix2".to_string()), + daemonize: true + } + ); } diff --git a/src/main.rs b/src/main.rs index 156f17d..7150c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,15 @@ #![cfg_attr(feature = "bench", feature(test))] -#[macro_use] extern crate log; -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde_derive; -#[cfg(test)] extern crate tempfile; -#[cfg(feature = "bench")] extern crate test; +#[cfg(test)] +extern crate tempfile; +#[cfg(feature = "bench")] +extern crate test; #[macro_use] pub mod util; @@ -30,6 +34,7 @@ pub mod port_forwarding; pub mod table; pub mod traffic; pub mod types; +pub mod wsproxy; use structopt::StructOpt; @@ -39,26 +44,26 @@ use std::{ net::{Ipv4Addr, UdpSocket}, os::unix::fs::PermissionsExt, path::Path, - process::Command, + process, str::FromStr, sync::Mutex, - thread + thread, }; use crate::{ cloud::GenericCloud, - config::{Args, Config}, + config::{Args, Command, Config}, crypto::Crypto, device::{Device, TunTapDevice, Type}, + net::Socket, oldconfig::OldConfigFile, payload::Protocol, - port_forwarding::PortForwarding, - util::SystemTimeSource + util::SystemTimeSource, + wsproxy::ProxyConnection, }; - struct DualLogger { - file: Option> + file: Option>, } impl DualLogger { @@ -105,7 +110,7 @@ impl log::Log for DualLogger { } fn run_script(script: &str, ifname: &str) { - let mut cmd = Command::new("sh"); + let mut cmd = process::Command::new("sh"); cmd.arg("-c").arg(&script).env("IFNAME", ifname); debug!("Running script: {:?}", cmd); match cmd.status() { @@ -114,18 +119,18 @@ fn run_script(script: &str, ifname: &str) { error!("Script returned with error: {:?}", status.code()) } } - Err(e) => error!("Failed to execute script {:?}: {}", script, e) + Err(e) => error!("Failed to execute script {:?}: {}", script, e), } } fn parse_ip_netmask(addr: &str) -> Result<(Ipv4Addr, Ipv4Addr), String> { let (ip_str, len_str) = match addr.find('/') { Some(pos) => (&addr[..pos], &addr[pos + 1..]), - None => (addr, "24") + None => (addr, "24"), }; let prefix_len = u8::from_str(len_str).map_err(|_| format!("Invalid prefix length: {}", len_str))?; if prefix_len > 32 { - return Err(format!("Invalid prefix length: {}", prefix_len)) + return Err(format!("Invalid prefix length: {}", prefix_len)); } let ip = Ipv4Addr::from_str(ip_str).map_err(|_| format!("Invalid ip address: {}", ip_str))?; let netmask = Ipv4Addr::from(u32::max_value().checked_shl(32 - prefix_len as u32).unwrap()); @@ -162,11 +167,10 @@ fn setup_device(config: &Config) -> TunTapDevice { device } - #[allow(clippy::cognitive_complexity)] -fn run(config: Config) { +fn run(config: Config, socket: S) { let device = setup_device(&config); - let port_forwarding = if config.port_forwarding { PortForwarding::new(config.listen.port()) } else { None }; + let port_forwarding = if config.port_forwarding { socket.create_port_forwarding() } else { None }; let stats_file = match config.stats_file { None => None, Some(ref name) => { @@ -183,7 +187,7 @@ fn run(config: Config) { } }; let mut cloud = - GenericCloud::::new(&config, device, port_forwarding, stats_file); + GenericCloud::::new(&config, socket, device, port_forwarding, stats_file); for addr in config.peers { try_fail!(cloud.connect(&addr as &str), "Failed to send message to {}: {}", &addr); cloud.add_reconnect_peer(addr); @@ -224,15 +228,7 @@ fn main() { let args: Args = Args::from_args(); if args.version { println!("VpnCloud v{}", env!("CARGO_PKG_VERSION")); - return - } - if args.genkey { - let (privkey, pubkey) = Crypto::generate_keypair(args.password.as_deref()); - println!("Private key: {}\nPublic key: {}\n", privkey, pubkey); - println!( - "Attention: Keep the private key secret and use only the public key on other nodes to establish trust." - ); - return + return; } let logger = try_fail!(DualLogger::new(args.log_file.as_ref()), "Failed to open logfile: {}"); log::set_boxed_logger(Box::new(logger)).unwrap(); @@ -244,24 +240,43 @@ fn main() { } else { log::LevelFilter::Info }); - if args.migrate_config { - let file = args.config.unwrap(); - info!("Trying to convert from old config format"); - let f = try_fail!(File::open(&file), "Failed to open config file: {:?}"); - let config_file_old: OldConfigFile = - try_fail!(serde_yaml::from_reader(f), "Config file not valid for version 1: {:?}"); - let new_config = config_file_old.convert(); - info!("Successfully converted from old format"); - info!("Renaming original file to {}.orig", file); - try_fail!(fs::rename(&file, format!("{}.orig", file)), "Failed to rename original file: {:?}"); - info!("Writing new config back into {}", file); - let f = try_fail!(File::create(&file), "Failed to open config file: {:?}"); - try_fail!( - fs::set_permissions(&file, fs::Permissions::from_mode(0o600)), - "Failed to set permissions on file: {:?}" - ); - try_fail!(serde_yaml::to_writer(f, &new_config), "Failed to write converted config: {:?}"); - return + if let Some(cmd) = args.cmd { + match cmd { + Command::GenKey => { + let (privkey, pubkey) = Crypto::generate_keypair(args.password.as_deref()); + println!("Private key: {}\nPublic key: {}\n", privkey, pubkey); + println!( + "Attention: Keep the private key secret and use only the public key on other nodes to establish trust." + ); + } + Command::MigrateConfig { config_file } => { + info!("Trying to convert from old config format"); + let f = try_fail!(File::open(&config_file), "Failed to open config file: {:?}"); + let config_file_old: OldConfigFile = + try_fail!(serde_yaml::from_reader(f), "Config file not valid for version 1: {:?}"); + let new_config = config_file_old.convert(); + info!("Successfully converted from old format"); + info!("Renaming original file to {}.orig", config_file); + try_fail!( + fs::rename(&config_file, format!("{}.orig", config_file)), + "Failed to rename original file: {:?}" + ); + info!("Writing new config back into {}", config_file); + let f = try_fail!(File::create(&config_file), "Failed to open config file: {:?}"); + try_fail!( + fs::set_permissions(&config_file, fs::Permissions::from_mode(0o600)), + "Failed to set permissions on file: {:?}" + ); + try_fail!(serde_yaml::to_writer(f, &new_config), "Failed to write converted config: {:?}"); + } + Command::WsProxy => { + wsproxy::run_proxy(); + } + Command::WsClient { url } => { + wsproxy::run_client(url); + } + } + return; } let mut config = Config::default(); if let Some(ref file) = args.config { @@ -284,8 +299,21 @@ fn main() { } config.merge_args(args); debug!("Config: {:?}", config); - match config.device_type { - Type::Tap => run::(config), - Type::Tun => run::(config) + if config.crypto.password.is_none() && config.crypto.private_key.is_none() { + error!("Either password or private key must be set in config or given as parameter"); + return; + } + if config.listen.starts_with("ws") { + let socket = try_fail!(ProxyConnection::listen(&config.listen), "Failed to open socket {}: {}", config.listen); + match config.device_type { + Type::Tap => run::(config, socket), + Type::Tun => run::(config, socket), + } + } else { + let socket = try_fail!(UdpSocket::listen(&config.listen), "Failed to open socket {}: {}", config.listen); + match config.device_type { + Type::Tap => run::(config, socket), + Type::Tun => run::(config, socket), + } } } diff --git a/src/net.rs b/src/net.rs index 296c6e9..00b4611 100644 --- a/src/net.rs +++ b/src/net.rs @@ -5,12 +5,13 @@ use std::{ collections::{HashMap, VecDeque}, io::{self, ErrorKind}, - net::{IpAddr, SocketAddr, UdpSocket}, + net::{IpAddr, SocketAddr, UdpSocket, Ipv6Addr}, os::unix::io::{AsRawFd, RawFd}, sync::atomic::{AtomicBool, Ordering} }; use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource}; +use crate::port_forwarding::PortForwarding; pub fn mapped_addr(addr: SocketAddr) -> SocketAddr { match addr { @@ -21,14 +22,28 @@ pub fn mapped_addr(addr: SocketAddr) -> SocketAddr { pub trait Socket: AsRawFd + Sized { - fn listen(addr: SocketAddr) -> Result; + fn listen(addr: &str) -> Result; fn receive(&mut self, buffer: &mut MsgBuffer) -> Result; fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result; fn address(&self) -> Result; + fn create_port_forwarding(&self) -> Option; +} + +fn parse_listen(addr: &str) -> SocketAddr { + if let Some(addr) = addr.strip_prefix("*:") { + let port = try_fail!(addr.parse::(), "Invalid port: {}"); + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port) + } else if addr.contains(':') { + try_fail!(addr.parse::(), "Invalid address: {}: {}", addr) + } else { + let port = try_fail!(addr.parse::(), "Invalid port: {}"); + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port) + } } impl Socket for UdpSocket { - fn listen(addr: SocketAddr) -> Result { + fn listen(addr: &str) -> Result { + let addr = parse_listen(addr); UdpSocket::bind(addr) } @@ -46,6 +61,10 @@ impl Socket for UdpSocket { fn address(&self) -> Result { self.local_addr() } + + fn create_port_forwarding(&self) -> Option { + PortForwarding::new(self.address().unwrap().port()) + } } thread_local! { @@ -106,8 +125,8 @@ impl AsRawFd for MockSocket { } impl Socket for MockSocket { - fn listen(addr: SocketAddr) -> Result { - Ok(Self::new(addr)) + fn listen(addr: &str) -> Result { + Ok(Self::new(parse_listen(addr))) } fn receive(&mut self, buffer: &mut MsgBuffer) -> Result { @@ -132,6 +151,10 @@ impl Socket for MockSocket { fn address(&self) -> Result { Ok(self.address) } + + fn create_port_forwarding(&self) -> Option { + None + } } #[cfg(feature = "bench")] diff --git a/src/wsproxy.rs b/src/wsproxy.rs new file mode 100644 index 0000000..c387640 --- /dev/null +++ b/src/wsproxy.rs @@ -0,0 +1,102 @@ +use super::{ + net::{mapped_addr, Socket}, + port_forwarding::PortForwarding, + util::MsgBuffer +}; +use std::{ + io::{self, Write, Read, Cursor}, + net::{SocketAddr, TcpListener, SocketAddrV6, Ipv6Addr}, + os::unix::io::{AsRawFd, RawFd}, + thread::spawn, +}; +use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; +use tungstenite::{client::AutoStream, connect, protocol::WebSocket, server::accept, stream::Stream, Message}; +use url::Url; + +pub fn run_proxy() { + let server = TcpListener::bind("127.0.0.1:9001").unwrap(); + for stream in server.incoming() { + info!("connect"); + spawn(move || { + let mut websocket = accept(stream.unwrap()).unwrap(); + loop { + let msg = websocket.read_message().unwrap(); + // We do not want to send back ping/pong messages. + if msg.is_binary() || msg.is_text() { + info!("msg"); + websocket.write_message(msg).unwrap(); + } + } + }); + } +} + +pub struct ProxyConnection { + url: String, + addr: SocketAddr, + socket: WebSocket +} + +impl AsRawFd for ProxyConnection { + fn as_raw_fd(&self) -> RawFd { + match self.socket.get_ref() { + Stream::Plain(sock) => sock.as_raw_fd(), + Stream::Tls(sock) => sock.get_ref().as_raw_fd() + } + } +} + +impl Socket for ProxyConnection { + fn listen(url: &str) -> Result { + let (mut socket, _) = connect(Url::parse(url).unwrap()).unwrap(); + let addr = "0.0.0.0:0".parse::().unwrap(); + Ok(ProxyConnection { url: addr.to_string(), addr, socket }) + } + + fn receive(&mut self, buffer: &mut MsgBuffer) -> Result { + buffer.clear(); + match self.socket.read_message().unwrap() { + Message::Binary(data) => { + let mut cursor = Cursor::new(&data); + let mut ip = [0u8; 16]; + cursor.read_exact(&mut ip)?; + let port = cursor.read_u16::()?; + let addr = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0)); + buffer.clone_from(&data[18..]); + Ok(addr) + }, + _ => unimplemented!() + } + } + + fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result { + let mut msg = Vec::with_capacity(data.len() + 18); + let addr = mapped_addr(addr); + match mapped_addr(addr) { + SocketAddr::V6(addr) => { + msg.write_all(&addr.ip().octets())?; + msg.write_u16::(addr.port())?; + }, + _ => unreachable!() + } + msg.write_all(data)?; + self.socket.write_message(Message::Binary(msg)).unwrap(); + Ok(data.len()) + } + + fn address(&self) -> Result { + Ok(self.addr) + } + + fn create_port_forwarding(&self) -> Option { + None + } +} + +pub fn run_client(url: String) { + let (mut socket, _) = connect(Url::parse(&url).unwrap()).unwrap(); + socket.write_message(Message::Text("test".to_string())).unwrap(); + let msg = socket.read_message().unwrap(); + info!("msg: {}", msg); + socket.close(None).unwrap(); +}