Compare commits

...

11 Commits

Author SHA1 Message Date
Dennis Schwerdel ed260d9a98 Merge branch 'master' of github.com:dswd/vpncloud 2021-02-04 23:43:16 +01:00
Dennis Schwerdel d154f85ecd Changelog 2021-02-04 23:42:51 +01:00
Dennis Schwerdel 65eef143cd Small improvements 2021-02-04 23:38:08 +01:00
Dennis Schwerdel 791ecfb0fe Remove tls support on client 2021-02-04 22:12:20 +01:00
Dennis Schwerdel e9122743e9 Feature gate websockets 2021-02-04 21:42:38 +01:00
Dennis Schwerdel bd0d102358 Add configurable WS port 2021-02-04 21:19:05 +01:00
Dennis Schwerdel a113b3ba22 Remove unused imports 2021-02-03 22:46:53 +01:00
Dennis Schwerdel f0d9ad2ccd Merge branch 'master' into wsproxy 2021-02-03 22:44:31 +01:00
Dennis Schwerdel e3fa631ed9 Working example 2021-02-03 22:03:42 +01:00
Dennis Schwerdel 124f7cbff9 Almost works 2020-12-20 13:28:01 +01:00
Dennis Schwerdel d50490ac51 Client works 2020-12-20 01:40:32 +01:00
12 changed files with 516 additions and 157 deletions

View File

@ -4,8 +4,9 @@ This project follows [semantic versioning](http://semver.org).
### UNRELEASED ### UNRELEASED
- [added] Support for creating shell completions - [added] Support for websocket proxy mode
- [added] Support for hook scripts to handle certain situations - [added] Support for hook scripts to handle certain situations
- [added] Support for creating shell completions
- [removed] Removed dummy device type - [removed] Removed dummy device type
- [changed] Updated dependencies - [changed] Updated dependencies
- [changed] Changed Rust version to 1.49.0 - [changed] Changed Rust version to 1.49.0

115
Cargo.lock generated
View File

@ -44,12 +44,27 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 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]] [[package]]
name = "boxfnonce" name = "boxfnonce"
version = "0.1.1" version = "0.1.1"
@ -70,9 +85,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.5.0" version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -134,6 +149,12 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
[[package]]
name = "cpuid-bool"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.3.4" version = "0.3.4"
@ -248,6 +269,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "discard" name = "discard"
version = "1.0.4" version = "1.0.4"
@ -282,6 +312,16 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.2" version = "0.2.2"
@ -328,6 +368,12 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "httparse"
version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.0" version = "0.2.0"
@ -352,6 +398,15 @@ dependencies = [
"xmltree", "xmltree",
] ]
[[package]]
name = "input_buffer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
dependencies = [
"bytes",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.9.0" version = "0.9.0"
@ -489,6 +544,12 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -807,6 +868,19 @@ dependencies = [
"yaml-rust", "yaml-rust",
] ]
[[package]]
name = "sha-1"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cpuid-bool",
"digest",
"opaque-debug",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.6.0" version = "0.6.0"
@ -1040,6 +1114,31 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tungstenite"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24"
dependencies = [
"base64",
"byteorder",
"bytes",
"http",
"httparse",
"input_buffer",
"log",
"rand",
"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]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.4" version = "0.3.4"
@ -1094,6 +1193,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "utf-8"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
@ -1134,6 +1239,8 @@ dependencies = [
"tempfile", "tempfile",
"thiserror", "thiserror",
"time", "time",
"tungstenite",
"url",
"yaml-rust", "yaml-rust",
] ]
@ -1150,9 +1257,9 @@ dependencies = [
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.1+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"

View File

@ -29,15 +29,17 @@ privdrop = "0.5"
byteorder = "1.4" byteorder = "1.4"
thiserror = "1.0" thiserror = "1.0"
smallvec = "1.6" smallvec = "1.6"
tungstenite = { version = "0.12", optional = true, default-features = false }
url = { version = "2.2", optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
criterion = "0.3" criterion = "0.3"
[features] [features]
default = ["nat"] default = ["nat", "websocket"]
bench = []
nat = ["igd"] nat = ["igd"]
websocket = ["tungstenite", "url"]
[[bench]] [[bench]]
name = "bench" name = "bench"

View File

@ -8,7 +8,7 @@ from datetime import date
# Note: this script will run for ~8 minutes and incur costs of about $ 0.02 # Note: this script will run for ~8 minutes and incur costs of about $ 0.02
FILE = "../target/release/vpncloud" FILE = "../target/release/vpncloud"
VERSION = "2.0.0-alpha1" VERSION = "2.0.1"
REGION = "eu-central-1" REGION = "eu-central-1"
env = EC2Environment( env = EC2Environment(

View File

@ -5,7 +5,7 @@ import atexit, argparse, os
REGION = "eu-central-1" REGION = "eu-central-1"
VERSION = "2.0.0" VERSION = "2.0.1"
parser = argparse.ArgumentParser(description='Create a test setup') parser = argparse.ArgumentParser(description='Create a test setup')
@ -25,15 +25,22 @@ if args.keyname:
with open(args.keyfile, 'r') as fp: with open(args.keyfile, 'r') as fp:
privatekey = fp.read() privatekey = fp.read()
opts = {}
if os.path.exists(args.version):
opts["vpncloud_file"] = args.version
opts["vpncloud_version"] = None
else:
opts["vpncloud_version"] = args.version
setup = EC2Environment( setup = EC2Environment(
region = REGION, region = REGION,
node_count = args.count, node_count = args.count,
instance_type = args.instancetype, instance_type = args.instancetype,
vpncloud_version = args.version,
cluster_nodes = args.cluster, cluster_nodes = args.cluster,
subnet = args.subnet or CREATE, subnet = args.subnet or CREATE,
keyname = args.keyname or CREATE, keyname = args.keyname or CREATE,
privatekey = privatekey privatekey = privatekey,
**opts
) )
if not args.keyname: if not args.keyname:

View File

@ -99,11 +99,7 @@ pub struct GenericCloud<D: Device, P: Protocol, S: Socket, TS: TimeSource> {
impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS> { impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new(config: &Config, device: D, port_forwarding: Option<PortForwarding>, stats_file: Option<File>) -> Self { pub fn new(config: &Config, socket: S, device: D, port_forwarding: Option<PortForwarding>, stats_file: Option<File>) -> Self {
let socket = match S::listen(config.listen) {
Ok(socket) => socket,
Err(err) => fail!("Failed to open socket {}: {}", config.listen, err)
};
let (learning, broadcast) = match config.mode { let (learning, broadcast) = match config.mode {
Mode::Normal => { Mode::Normal => {
match config.device_type { match config.device_type {
@ -231,6 +227,7 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
self.own_addresses.push(pfw.get_internal_ip().into()); self.own_addresses.push(pfw.get_internal_ip().into());
self.own_addresses.push(pfw.get_external_ip().into()); self.own_addresses.push(pfw.get_external_ip().into());
} }
debug!("Own addresses: {:?}", self.own_addresses);
// TODO: detect address changes and call event // TODO: detect address changes and call event
Ok(()) Ok(())
} }
@ -917,7 +914,7 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
/// Also, this method will call `housekeep` every second. /// Also, this method will call `housekeep` every second.
pub fn run(&mut self) { pub fn run(&mut self) {
let ctrlc = CtrlC::new(); let ctrlc = CtrlC::new();
let waiter = try_fail!(WaitImpl::new(&self.socket, &self.device, 1000), "Failed to setup poll: {}"); let waiter = try_fail!(WaitImpl::new(self.socket.as_raw_fd(), self.device.as_raw_fd(), 1000), "Failed to setup poll: {}");
let mut buffer = MsgBuffer::new(SPACE_BEFORE); let mut buffer = MsgBuffer::new(SPACE_BEFORE);
let mut poll_error = false; let mut poll_error = false;
self.config.call_hook("vpn_started", vec![("IFNAME", self.device.ifname())], true); self.config.call_hook("vpn_started", vec![("IFNAME", self.device.ifname())], true);

View File

@ -2,15 +2,14 @@
// Copyright (C) 2015-2020 Dennis Schwerdel // Copyright (C) 2015-2020 Dennis Schwerdel
// This software is licensed under GPL-3 or newer (see LICENSE.md) // This software is licensed under GPL-3 or newer (see LICENSE.md)
use super::{device::Type, types::Mode, util::Duration}; use super::{device::Type, types::Mode, util::Duration, util::run_cmd};
pub use crate::crypto::Config as CryptoConfig; pub use crate::crypto::Config as CryptoConfig;
use crate::util::run_cmd;
use std::{ use std::{
cmp::max, cmp::max,
collections::HashMap, collections::HashMap,
ffi::OsStr, ffi::OsStr,
net::{IpAddr, Ipv6Addr, SocketAddr}, process,
process::Command,
thread thread
}; };
use structopt::{clap::Shell, StructOpt}; use structopt::{clap::Shell, StructOpt};
@ -18,17 +17,6 @@ use structopt::{clap::Shell, StructOpt};
pub const DEFAULT_PEER_TIMEOUT: u16 = 300; pub const DEFAULT_PEER_TIMEOUT: u16 = 300;
pub const DEFAULT_PORT: u16 = 3210; 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::<u16>(), "Invalid port: {}");
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
} else if addr.contains(':') {
try_fail!(addr.parse::<SocketAddr>(), "Invalid address: {}: {}", addr)
} else {
let port = try_fail!(addr.parse::<u16>(), "Invalid port: {}");
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
}
}
#[derive(Deserialize, Debug, PartialEq, Clone)] #[derive(Deserialize, Debug, PartialEq, Clone)]
pub struct Config { pub struct Config {
@ -43,7 +31,7 @@ pub struct Config {
pub crypto: CryptoConfig, pub crypto: CryptoConfig,
pub listen: SocketAddr, pub listen: String,
pub peers: Vec<String>, pub peers: Vec<String>,
pub peer_timeout: Duration, pub peer_timeout: Duration,
pub keepalive: Option<Duration>, pub keepalive: Option<Duration>,
@ -78,7 +66,7 @@ impl Default for Config {
ifup: None, ifup: None,
ifdown: None, ifdown: None,
crypto: CryptoConfig::default(), crypto: CryptoConfig::default(),
listen: "[::]:3210".parse::<SocketAddr>().unwrap(), listen: "3210".to_string(),
peers: vec![], peers: vec![],
peer_timeout: DEFAULT_PEER_TIMEOUT as Duration, peer_timeout: DEFAULT_PEER_TIMEOUT as Duration,
keepalive: None, keepalive: None,
@ -131,7 +119,7 @@ impl Config {
self.ifdown = Some(val); self.ifdown = Some(val);
} }
if let Some(val) = file.listen { if let Some(val) = file.listen {
self.listen = parse_listen(&val); self.listen = val;
} }
if let Some(mut val) = file.peers { if let Some(mut val) = file.peers {
self.peers.append(&mut val); self.peers.append(&mut val);
@ -235,7 +223,7 @@ impl Config {
self.ifdown = Some(val); self.ifdown = Some(val);
} }
if let Some(val) = args.listen { if let Some(val) = args.listen {
self.listen = parse_listen(&val); self.listen = val;
} }
self.peers.append(&mut args.peers); self.peers.append(&mut args.peers);
if let Some(val) = args.peer_timeout { if let Some(val) = args.peer_timeout {
@ -318,7 +306,7 @@ impl Config {
pub fn get_keepalive(&self) -> Duration { pub fn get_keepalive(&self) -> Duration {
match self.keepalive { match self.keepalive {
Some(dur) => dur, Some(dur) => dur,
None => max(self.peer_timeout / 2 - 60, 1) None => max(self.peer_timeout / 2 - 60, 1),
} }
} }
@ -336,7 +324,7 @@ impl Config {
return return
} }
let script = script.unwrap(); let script = script.unwrap();
let mut cmd = Command::new("sh"); let mut cmd = process::Command::new("sh");
cmd.arg("-c").arg(script).envs(envs).env("EVENT", event); cmd.arg("-c").arg(script).envs(envs).env("EVENT", event);
debug!("Running event script: {:?}", cmd); debug!("Running event script: {:?}", cmd);
if detach { if detach {
@ -370,7 +358,7 @@ pub struct Args {
pub mode: Option<Mode>, pub mode: Option<Mode>,
/// The shared password to encrypt all traffic /// The shared password to encrypt all traffic
#[structopt(short, long, required_unless_one = &["private-key", "config", "genkey", "version", "completion"], env)] #[structopt(short, long, env)]
pub password: Option<String>, pub password: Option<String>,
/// The private key to use /// The private key to use
@ -461,10 +449,6 @@ pub struct Args {
#[structopt(long)] #[structopt(long)]
pub version: bool, pub version: bool,
/// Generate and print a key-pair and exit
#[structopt(long, conflicts_with = "private_key")]
pub genkey: bool,
/// Disable automatic port forwarding /// Disable automatic port forwarding
#[structopt(long)] #[structopt(long)]
pub no_port_forwarding: bool, pub no_port_forwarding: bool,
@ -501,17 +485,43 @@ pub struct Args {
#[structopt(long)] #[structopt(long)]
pub log_file: Option<String>, pub log_file: Option<String>,
/// Migrate an old config file
#[structopt(long, alias = "migrate", requires = "config")]
pub migrate_config: bool,
/// Generate shell completions
#[structopt(long)]
pub completion: Option<Shell>,
/// Call script on event /// Call script on event
#[structopt(long)] #[structopt(long)]
pub hook: Vec<String> pub hook: Vec<String>,
#[structopt(subcommand)]
pub cmd: Option<Command>,
}
#[derive(StructOpt, Debug)]
pub enum Command {
/// Generate and print a key-pair and exit
#[structopt(name = "genkey", alias = "gen-key")]
GenKey,
/// Run a websocket proxy
#[cfg(feature = "websocket")]
#[structopt(alias = "wsproxy")]
WsProxy {
/// Websocket listen address IP:PORT
#[structopt(long, short, default_value="3210")]
listen: String
},
/// Migrate an old config file
#[structopt(alias = "migrate")]
MigrateConfig {
/// Config file
#[structopt(long)]
config_file: String,
},
/// Generate shell completions
Completion {
/// Shell to create completions for
#[structopt(long)]
shell: Shell
}
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
@ -521,7 +531,7 @@ pub struct ConfigFileDevice {
pub type_: Option<Type>, pub type_: Option<Type>,
pub name: Option<String>, pub name: Option<String>,
pub path: Option<String>, pub path: Option<String>,
pub fix_rp_filter: Option<bool> pub fix_rp_filter: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
@ -530,14 +540,14 @@ pub struct ConfigFileBeacon {
pub store: Option<String>, pub store: Option<String>,
pub load: Option<String>, pub load: Option<String>,
pub interval: Option<Duration>, pub interval: Option<Duration>,
pub password: Option<String> pub password: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)] #[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
pub struct ConfigFileStatsd { pub struct ConfigFileStatsd {
pub server: Option<String>, pub server: Option<String>,
pub prefix: Option<String> pub prefix: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
@ -603,43 +613,46 @@ statsd:
server: example.com:1234 server: example.com:1234
prefix: prefix prefix: prefix
"; ";
assert_eq!(serde_yaml::from_str::<ConfigFile>(config_file).unwrap(), ConfigFile { assert_eq!(
device: Some(ConfigFileDevice { serde_yaml::from_str::<ConfigFile>(config_file).unwrap(),
type_: Some(Type::Tun), ConfigFile {
name: Some("vpncloud%d".to_string()), device: Some(ConfigFileDevice {
path: Some("/dev/net/tun".to_string()), type_: Some(Type::Tun),
fix_rp_filter: None name: Some("vpncloud%d".to_string()),
}), path: Some("/dev/net/tun".to_string()),
ip: Some("10.0.1.1/16".to_string()), fix_rp_filter: None
ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), }),
ifdown: Some("true".to_string()), ip: Some("10.0.1.1/16".to_string()),
crypto: CryptoConfig::default(), ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()),
listen: None, ifdown: Some("true".to_string()),
peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]), crypto: CryptoConfig::default(),
peer_timeout: Some(600), listen: None,
keepalive: Some(840), peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]),
beacon: Some(ConfigFileBeacon { peer_timeout: Some(600),
store: Some("/run/vpncloud.beacon.out".to_string()), keepalive: Some(840),
load: Some("/run/vpncloud.beacon.in".to_string()), beacon: Some(ConfigFileBeacon {
interval: Some(3600), store: Some("/run/vpncloud.beacon.out".to_string()),
password: Some("test123".to_string()) load: Some("/run/vpncloud.beacon.in".to_string()),
}), interval: Some(3600),
mode: Some(Mode::Normal), password: Some("test123".to_string())
switch_timeout: Some(300), }),
claims: Some(vec!["10.0.1.0/24".to_string()]), mode: Some(Mode::Normal),
auto_claim: None, switch_timeout: Some(300),
port_forwarding: Some(true), claims: Some(vec!["10.0.1.0/24".to_string()]),
user: Some("nobody".to_string()), auto_claim: None,
group: Some("nogroup".to_string()), port_forwarding: Some(true),
pid_file: Some("/run/vpncloud.run".to_string()), user: Some("nobody".to_string()),
stats_file: Some("/var/log/vpncloud.stats".to_string()), group: Some("nogroup".to_string()),
statsd: Some(ConfigFileStatsd { pid_file: Some("/run/vpncloud.run".to_string()),
server: Some("example.com:1234".to_string()), stats_file: Some("/var/log/vpncloud.stats".to_string()),
prefix: Some("prefix".to_string()) statsd: Some(ConfigFileStatsd {
}), server: Some("example.com:1234".to_string()),
hook: None, prefix: Some("prefix".to_string())
hooks: HashMap::new() }),
}) hook: None,
hooks: HashMap::new()
}
)
} }
#[test] #[test]
@ -653,7 +666,7 @@ fn default_config_as_default() {
ifup: None, ifup: None,
ifdown: None, ifdown: None,
crypto: CryptoConfig::default(), crypto: CryptoConfig::default(),
listen: "[::]:3210".parse::<SocketAddr>().unwrap(), listen: "[::]:3210".to_string(),
peers: vec![], peers: vec![],
peer_timeout: 0, peer_timeout: 0,
keepalive: None, keepalive: None,
@ -690,7 +703,7 @@ fn config_merge() {
type_: Some(Type::Tun), type_: Some(Type::Tun),
name: Some("vpncloud%d".to_string()), name: Some("vpncloud%d".to_string()),
path: None, path: None,
fix_rp_filter: None fix_rp_filter: None,
}), }),
ip: None, ip: None,
ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()),
@ -704,7 +717,7 @@ fn config_merge() {
store: Some("/run/vpncloud.beacon.out".to_string()), store: Some("/run/vpncloud.beacon.out".to_string()),
load: Some("/run/vpncloud.beacon.in".to_string()), load: Some("/run/vpncloud.beacon.in".to_string()),
interval: Some(7200), interval: Some(7200),
password: Some("test123".to_string()) password: Some("test123".to_string()),
}), }),
mode: Some(Mode::Normal), mode: Some(Mode::Normal),
switch_timeout: Some(300), switch_timeout: Some(300),
@ -717,7 +730,7 @@ fn config_merge() {
stats_file: Some("/var/log/vpncloud.stats".to_string()), stats_file: Some("/var/log/vpncloud.stats".to_string()),
statsd: Some(ConfigFileStatsd { statsd: Some(ConfigFileStatsd {
server: Some("example.com:1234".to_string()), server: Some("example.com:1234".to_string()),
prefix: Some("prefix".to_string()) prefix: Some("prefix".to_string()),
}), }),
hook: None, hook: None,
hooks: HashMap::new() hooks: HashMap::new()
@ -729,7 +742,7 @@ fn config_merge() {
ip: None, ip: None,
ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()), ifup: Some("ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up".to_string()),
ifdown: Some("true".to_string()), ifdown: Some("true".to_string()),
listen: "[::]:3210".parse::<SocketAddr>().unwrap(), listen: "3210".to_string(),
peers: vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()], peers: vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()],
peer_timeout: 600, peer_timeout: 600,
keepalive: Some(840), keepalive: Some(840),
@ -756,7 +769,7 @@ fn config_merge() {
ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()), ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()),
ifdown: Some("ifconfig $IFNAME down".to_string()), ifdown: Some("ifconfig $IFNAME down".to_string()),
password: Some("anothersecret".to_string()), password: Some("anothersecret".to_string()),
listen: Some("3211".to_string()), listen: Some("[::]:3211".to_string()),
peer_timeout: Some(1801), peer_timeout: Some(1801),
keepalive: Some(850), keepalive: Some(850),
switch_timeout: Some(301), switch_timeout: Some(301),
@ -786,7 +799,7 @@ fn config_merge() {
ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()), ifup: Some("ifconfig $IFNAME 10.0.1.2/16 mtu 1400 up".to_string()),
ifdown: Some("ifconfig $IFNAME down".to_string()), ifdown: Some("ifconfig $IFNAME down".to_string()),
crypto: CryptoConfig { password: Some("anothersecret".to_string()), ..CryptoConfig::default() }, crypto: CryptoConfig { password: Some("anothersecret".to_string()), ..CryptoConfig::default() },
listen: "[::]:3211".parse::<SocketAddr>().unwrap(), listen: "[::]:3211".to_string(),
peers: vec![ peers: vec![
"remote.machine.foo:3210".to_string(), "remote.machine.foo:3210".to_string(),
"remote.machine.bar:3210".to_string(), "remote.machine.bar:3210".to_string(),

View File

@ -27,6 +27,7 @@ pub mod port_forwarding;
pub mod table; pub mod table;
pub mod traffic; pub mod traffic;
pub mod types; pub mod types;
#[cfg(feature = "websocket")] pub mod wsproxy;
use structopt::StructOpt; use structopt::StructOpt;
@ -36,7 +37,7 @@ use std::{
net::{Ipv4Addr, UdpSocket}, net::{Ipv4Addr, UdpSocket},
os::unix::fs::PermissionsExt, os::unix::fs::PermissionsExt,
path::Path, path::Path,
process::Command, process,
str::FromStr, str::FromStr,
sync::Mutex, sync::Mutex,
thread thread
@ -44,15 +45,17 @@ use std::{
use crate::{ use crate::{
cloud::GenericCloud, cloud::GenericCloud,
config::{Args, Config}, config::{Args, Command, Config},
crypto::Crypto, crypto::Crypto,
device::{Device, TunTapDevice, Type}, device::{Device, TunTapDevice, Type},
net::Socket,
oldconfig::OldConfigFile, oldconfig::OldConfigFile,
payload::Protocol, payload::Protocol,
port_forwarding::PortForwarding, util::SystemTimeSource,
util::SystemTimeSource
}; };
#[cfg(feature = "websocket")]
use crate::wsproxy::ProxyConnection;
struct DualLogger { struct DualLogger {
file: Option<Mutex<File>> file: Option<Mutex<File>>
@ -102,7 +105,7 @@ impl log::Log for DualLogger {
} }
fn run_script(script: &str, ifname: &str) { 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); cmd.arg("-c").arg(&script).env("IFNAME", ifname);
debug!("Running script: {:?}", cmd); debug!("Running script: {:?}", cmd);
match cmd.status() { match cmd.status() {
@ -161,11 +164,10 @@ fn setup_device(config: &Config) -> TunTapDevice {
device device
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn run<P: Protocol>(config: Config) { fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
let device = setup_device(&config); 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 { let stats_file = match config.stats_file {
None => None, None => None,
Some(ref name) => { Some(ref name) => {
@ -182,7 +184,7 @@ fn run<P: Protocol>(config: Config) {
} }
}; };
let mut cloud = let mut cloud =
GenericCloud::<TunTapDevice, P, UdpSocket, SystemTimeSource>::new(&config, device, port_forwarding, stats_file); GenericCloud::<TunTapDevice, P, S, SystemTimeSource>::new(&config, socket, device, port_forwarding, stats_file);
for addr in config.peers { for addr in config.peers {
try_fail!(cloud.connect(&addr as &str), "Failed to send message to {}: {}", &addr); try_fail!(cloud.connect(&addr as &str), "Failed to send message to {}: {}", &addr);
cloud.add_reconnect_peer(addr); cloud.add_reconnect_peer(addr);
@ -225,18 +227,6 @@ fn main() {
println!("VpnCloud v{}", env!("CARGO_PKG_VERSION")); println!("VpnCloud v{}", env!("CARGO_PKG_VERSION"));
return 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
}
if let Some(shell) = args.completion {
Args::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
return
}
let logger = try_fail!(DualLogger::new(args.log_file.as_ref()), "Failed to open logfile: {}"); let logger = try_fail!(DualLogger::new(args.log_file.as_ref()), "Failed to open logfile: {}");
log::set_boxed_logger(Box::new(logger)).unwrap(); log::set_boxed_logger(Box::new(logger)).unwrap();
assert!(!args.verbose || !args.quiet); assert!(!args.verbose || !args.quiet);
@ -247,23 +237,44 @@ fn main() {
} else { } else {
log::LevelFilter::Info log::LevelFilter::Info
}); });
if args.migrate_config { if let Some(cmd) = args.cmd {
let file = args.config.unwrap(); match cmd {
info!("Trying to convert from old config format"); Command::GenKey => {
let f = try_fail!(File::open(&file), "Failed to open config file: {:?}"); let (privkey, pubkey) = Crypto::generate_keypair(args.password.as_deref());
let config_file_old: OldConfigFile = println!("Private key: {}\nPublic key: {}\n", privkey, pubkey);
try_fail!(serde_yaml::from_reader(f), "Config file not valid for version 1: {:?}"); println!(
let new_config = config_file_old.convert(); "Attention: Keep the private key secret and use only the public key on other nodes to establish trust."
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: {:?}"); Command::MigrateConfig { config_file } => {
info!("Writing new config back into {}", file); info!("Trying to convert from old config format");
let f = try_fail!(File::create(&file), "Failed to open config file: {:?}"); let f = try_fail!(File::open(&config_file), "Failed to open config file: {:?}");
try_fail!( let config_file_old: OldConfigFile =
fs::set_permissions(&file, fs::Permissions::from_mode(0o600)), try_fail!(serde_yaml::from_reader(f), "Config file not valid for version 1: {:?}");
"Failed to set permissions on file: {:?}" let new_config = config_file_old.convert();
); info!("Successfully converted from old format");
try_fail!(serde_yaml::to_writer(f, &new_config), "Failed to write converted config: {:?}"); 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::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: {:?}");
}
}
return return
} }
let mut config = Config::default(); let mut config = Config::default();
@ -287,8 +298,22 @@ fn main() {
} }
config.merge_args(args); config.merge_args(args);
debug!("Config: {:?}", config); debug!("Config: {:?}", 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
}
#[cfg(feature = "websocket")]
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::<payload::Frame, _>(config, socket),
Type::Tun => run::<payload::Packet, _>(config, socket)
}
return
}
let socket = try_fail!(UdpSocket::listen(&config.listen), "Failed to open socket {}: {}", config.listen);
match config.device_type { match config.device_type {
Type::Tap => run::<payload::Frame>(config), Type::Tap => run::<payload::Frame, _>(config, socket),
Type::Tun => run::<payload::Packet>(config) Type::Tun => run::<payload::Packet, _>(config, socket)
} }
} }

View File

@ -5,12 +5,13 @@
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
io::{self, ErrorKind}, io::{self, ErrorKind},
net::{IpAddr, SocketAddr, UdpSocket}, net::{IpAddr, SocketAddr, UdpSocket, Ipv6Addr},
os::unix::io::{AsRawFd, RawFd}, os::unix::io::{AsRawFd, RawFd},
sync::atomic::{AtomicBool, Ordering} sync::atomic::{AtomicBool, Ordering}
}; };
use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource}; use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource};
use crate::port_forwarding::PortForwarding;
pub fn mapped_addr(addr: SocketAddr) -> SocketAddr { pub fn mapped_addr(addr: SocketAddr) -> SocketAddr {
// HOT PATH // HOT PATH
@ -20,16 +21,35 @@ pub fn mapped_addr(addr: SocketAddr) -> SocketAddr {
} }
} }
pub fn get_ip() -> IpAddr {
let s = UdpSocket::bind("[::]:0").unwrap();
s.connect("8.8.8.8:0").unwrap();
s.local_addr().unwrap().ip()
}
pub trait Socket: AsRawFd + Sized { pub trait Socket: AsRawFd + Sized {
fn listen(addr: SocketAddr) -> Result<Self, io::Error>; fn listen(addr: &str) -> Result<Self, io::Error>;
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, 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>; fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error>;
fn address(&self) -> Result<SocketAddr, io::Error>; fn address(&self) -> Result<SocketAddr, io::Error>;
fn create_port_forwarding(&self) -> Option<PortForwarding>;
}
pub fn parse_listen(addr: &str) -> SocketAddr {
if let Some(addr) = addr.strip_prefix("*:") {
let port = try_fail!(addr.parse::<u16>(), "Invalid port: {}");
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
} else if addr.contains(':') {
try_fail!(addr.parse::<SocketAddr>(), "Invalid address: {}: {}", addr)
} else {
let port = try_fail!(addr.parse::<u16>(), "Invalid port: {}");
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
}
} }
impl Socket for UdpSocket { impl Socket for UdpSocket {
fn listen(addr: SocketAddr) -> Result<Self, io::Error> { fn listen(addr: &str) -> Result<Self, io::Error> {
let addr = parse_listen(addr);
UdpSocket::bind(addr) UdpSocket::bind(addr)
} }
@ -45,7 +65,13 @@ impl Socket for UdpSocket {
} }
fn address(&self) -> Result<SocketAddr, io::Error> { fn address(&self) -> Result<SocketAddr, io::Error> {
self.local_addr() let mut addr = self.local_addr()?;
addr.set_ip(get_ip());
Ok(addr)
}
fn create_port_forwarding(&self) -> Option<PortForwarding> {
PortForwarding::new(self.address().unwrap().port())
} }
} }
@ -107,8 +133,8 @@ impl AsRawFd for MockSocket {
} }
impl Socket for MockSocket { impl Socket for MockSocket {
fn listen(addr: SocketAddr) -> Result<Self, io::Error> { fn listen(addr: &str) -> Result<Self, io::Error> {
Ok(Self::new(addr)) Ok(Self::new(parse_listen(addr)))
} }
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> { fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
@ -133,4 +159,23 @@ impl Socket for MockSocket {
fn address(&self) -> Result<SocketAddr, io::Error> { fn address(&self) -> Result<SocketAddr, io::Error> {
Ok(self.address) Ok(self.address)
} }
fn create_port_forwarding(&self) -> Option<PortForwarding> {
None
}
}
#[cfg(feature = "bench")]
mod bench {
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use test::Bencher;
#[bench]
fn udp_send(b: &mut Bencher) {
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let data = [0; 1400];
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1);
b.iter(|| sock.send_to(&data, &addr).unwrap());
b.bytes = 1400;
}
} }

View File

@ -2,11 +2,9 @@
// Copyright (C) 2015-2020 Dennis Schwerdel // Copyright (C) 2015-2020 Dennis Schwerdel
// This software is licensed under GPL-3 or newer (see LICENSE.md) // This software is licensed under GPL-3 or newer (see LICENSE.md)
use crate::device::Device;
use std::{io, os::unix::io::RawFd}; use std::{io, os::unix::io::RawFd};
use super::WaitResult; use super::WaitResult;
use crate::net::Socket;
pub struct EpollWait { pub struct EpollWait {
poll_fd: RawFd, poll_fd: RawFd,
@ -17,21 +15,21 @@ pub struct EpollWait {
} }
impl EpollWait { impl EpollWait {
pub fn new<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> { pub fn new(socket: RawFd, device: RawFd, timeout: u32) -> io::Result<Self> {
Self::create(socket, device, timeout, libc::EPOLLIN as u32) Self::create(socket, device, timeout, libc::EPOLLIN as u32)
} }
pub fn testing<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> { pub fn testing(socket: RawFd, device: RawFd, timeout: u32) -> io::Result<Self> {
Self::create(socket, device, timeout, (libc::EPOLLIN | libc::EPOLLOUT) as u32) Self::create(socket, device, timeout, (libc::EPOLLIN | libc::EPOLLOUT) as u32)
} }
fn create<S: Socket>(socket: &S, device: &dyn Device, timeout: u32, flags: u32) -> io::Result<Self> { fn create(socket: RawFd, device: RawFd, timeout: u32, flags: u32) -> io::Result<Self> {
let mut event = libc::epoll_event { u64: 0, events: 0 }; let mut event = libc::epoll_event { u64: 0, events: 0 };
let poll_fd = unsafe { libc::epoll_create(3) }; let poll_fd = unsafe { libc::epoll_create(3) };
if poll_fd == -1 { if poll_fd == -1 {
return Err(io::Error::last_os_error()) return Err(io::Error::last_os_error())
} }
for fd in &[socket.as_raw_fd(), device.as_raw_fd()] { for fd in &[socket, device] {
event.u64 = *fd as u64; event.u64 = *fd as u64;
event.events = flags; event.events = flags;
let res = unsafe { libc::epoll_ctl(poll_fd, libc::EPOLL_CTL_ADD, *fd, &mut event) }; let res = unsafe { libc::epoll_ctl(poll_fd, libc::EPOLL_CTL_ADD, *fd, &mut event) };
@ -39,7 +37,7 @@ impl EpollWait {
return Err(io::Error::last_os_error()) return Err(io::Error::last_os_error())
} }
} }
Ok(Self { poll_fd, event, socket: socket.as_raw_fd(), device: device.as_raw_fd(), timeout }) Ok(Self { poll_fd, event, socket, device, timeout })
} }
} }

View File

@ -9,7 +9,7 @@ mod peers;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
io::Write, io::Write,
net::{IpAddr, Ipv6Addr, SocketAddr}, net::SocketAddr,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Once Once
@ -89,16 +89,17 @@ impl<P: Protocol> Simulator<P> {
pub fn add_node(&mut self, nat: bool, config: &Config) -> SocketAddr { pub fn add_node(&mut self, nat: bool, config: &Config) -> SocketAddr {
let mut config = config.clone(); let mut config = config.clone();
MockSocket::set_nat(nat); MockSocket::set_nat(nat);
config.listen = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.next_port); config.listen = format!("[::]:{}", self.next_port);
let addr = config.listen.parse::<SocketAddr>().unwrap();
if config.crypto.password.is_none() && config.crypto.private_key.is_none() { if config.crypto.password.is_none() && config.crypto.private_key.is_none() {
config.crypto.password = Some("test123".to_string()) config.crypto.password = Some("test123".to_string())
} }
DebugLogger::set_node(self.next_port as usize); DebugLogger::set_node(self.next_port as usize);
self.next_port += 1; self.next_port += 1;
let node = TestNode::new(&config, MockDevice::new(), None, None); let node = TestNode::new(&config, MockSocket::new(addr), MockDevice::new(), None, None);
DebugLogger::set_node(0); DebugLogger::set_node(0);
self.nodes.insert(config.listen, node); self.nodes.insert(addr, node);
config.listen addr
} }
#[allow(dead_code)] #[allow(dead_code)]

163
src/wsproxy.rs Normal file
View File

@ -0,0 +1,163 @@
use super::{
net::{get_ip, mapped_addr, parse_listen, Socket},
poll::{WaitImpl, WaitResult},
port_forwarding::PortForwarding,
util::MsgBuffer
};
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
use std::{
io::{self, Cursor, Read, Write},
net::{Ipv6Addr, SocketAddr, SocketAddrV6, TcpListener, TcpStream, UdpSocket},
os::unix::io::{AsRawFd, RawFd},
thread::spawn
};
use tungstenite::{client::AutoStream, connect, protocol::WebSocket, server::accept, Message};
use url::Url;
macro_rules! io_error {
($val:expr, $format:expr) => ( {
$val.map_err(|err| io::Error::new(io::ErrorKind::Other, format!($format, err)))
} );
($val:expr, $format:expr, $( $arg:expr ),+) => ( {
$val.map_err(|err| io::Error::new(io::ErrorKind::Other, format!($format, $( $arg ),+, err)))
} );
}
fn write_addr<W: Write>(addr: SocketAddr, mut out: W) -> Result<(), io::Error> {
let addr = mapped_addr(addr);
match mapped_addr(addr) {
SocketAddr::V6(addr) => {
out.write_all(&addr.ip().octets())?;
out.write_u16::<NetworkEndian>(addr.port())?;
}
_ => unreachable!()
}
Ok(())
}
fn read_addr<R: Read>(mut r: R) -> Result<SocketAddr, io::Error> {
let mut ip = [0u8; 16];
r.read_exact(&mut ip)?;
let port = r.read_u16::<NetworkEndian>()?;
let addr = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0));
Ok(addr)
}
fn serve_proxy_connection(stream: TcpStream) -> Result<(), io::Error> {
let peer = stream.peer_addr()?;
info!("WS client {} connected", peer);
stream.set_nodelay(true)?;
let mut websocket = io_error!(accept(stream), "Failed to initialize websocket with {}: {}", peer)?;
let udpsocket = UdpSocket::bind("[::]:0")?;
let mut msg = Vec::with_capacity(18);
let mut addr = udpsocket.local_addr()?;
info!("Listening on {} for peer {}", addr, peer);
addr.set_ip(get_ip());
write_addr(addr, &mut msg)?;
io_error!(websocket.write_message(Message::Binary(msg)), "Failed to write to ws connection: {}")?;
let websocketfd = websocket.get_ref().as_raw_fd();
let poll = WaitImpl::new(websocketfd, udpsocket.as_raw_fd(), 60 * 1000)?;
let mut buffer = [0; 65535];
for evt in poll {
match evt {
WaitResult::Socket => {
let msg = io_error!(websocket.read_message(), "Failed to read message on websocket {}: {}", peer)?;
match msg {
Message::Binary(data) => {
let dst = read_addr(Cursor::new(&data))?;
udpsocket.send_to(&data[18..], dst)?;
}
Message::Close(_) => return Ok(()),
_ => {}
}
}
WaitResult::Device => {
let (size, addr) = udpsocket.recv_from(&mut buffer)?;
let mut data = Vec::with_capacity(18 + size);
write_addr(addr, &mut data)?;
data.write_all(&buffer[..size])?;
io_error!(websocket.write_message(Message::Binary(data)), "Failed to write to {}: {}", peer)?;
}
WaitResult::Timeout => {
io_error!(websocket.write_message(Message::Ping(vec![])), "Failed to send ping: {}")?;
}
WaitResult::Error(err) => return Err(err)
}
}
Ok(())
}
pub fn run_proxy(listen: &str) -> Result<(), io::Error> {
let addr = parse_listen(listen);
let server = TcpListener::bind(addr)?;
info!("Listening on ws://{}", server.local_addr()?);
for stream in server.incoming() {
let stream = stream?;
let peer = stream.peer_addr()?;
spawn(move || {
if let Err(err) = serve_proxy_connection(stream) {
error!("Error on connection {}: {}", peer, err);
}
});
}
Ok(())
}
pub struct ProxyConnection {
addr: SocketAddr,
socket: WebSocket<AutoStream>
}
impl ProxyConnection {
fn read_message(&mut self) -> Result<Vec<u8>, io::Error> {
loop {
if let Message::Binary(data) = io_error!(self.socket.read_message(), "Failed to read from ws proxy: {}")? {
return Ok(data)
}
}
}
}
impl AsRawFd for ProxyConnection {
fn as_raw_fd(&self) -> RawFd {
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 addr_data = con.read_message()?;
con.addr = read_addr(Cursor::new(&addr_data))?;
Ok(con)
}
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
buffer.clear();
let data = self.read_message()?;
let addr = read_addr(Cursor::new(&data))?;
buffer.clone_from(&data[18..]);
Ok(addr)
}
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
let mut msg = Vec::with_capacity(data.len() + 18);
write_addr(addr, &mut msg)?;
msg.write_all(data)?;
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> {
Ok(self.addr)
}
fn create_port_forwarding(&self) -> Option<PortForwarding> {
None
}
}