mirror of https://github.com/dswd/vpncloud.git
Add listen option, switched to dual-stack
This commit is contained in:
parent
fba72882a6
commit
390f217a23
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
This project follows [semantic versioning](http://semver.org).
|
This project follows [semantic versioning](http://semver.org).
|
||||||
|
|
||||||
|
### UNRELEASED
|
||||||
|
|
||||||
|
- [added] Added option to listen on specified IP
|
||||||
|
- [changed] No longer using two sockets for ipv4 and ipv6
|
||||||
|
|
||||||
### v1.3.0 (2020-01-25)
|
### v1.3.0 (2020-01-25)
|
||||||
|
|
||||||
- [added] Building for aarch64 aka arm64 (thanks to Ivan)
|
- [added] Building for aarch64 aka arm64 (thanks to Ivan)
|
||||||
|
|
|
@ -130,10 +130,9 @@ fn now(b: &mut Bencher) {
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn epoll_wait(b: &mut Bencher) {
|
fn epoll_wait(b: &mut Bencher) {
|
||||||
let socketv4 = UdpSocket::bind("0.0.0.0:0").unwrap();
|
let socket = UdpSocket::bind("[::]:0").unwrap();
|
||||||
let socketv6 = UdpSocket::bind("[::]:0").unwrap();
|
|
||||||
let device = TunTapDevice::dummy("dummy", "/dev/zero", Type::Dummy).unwrap();
|
let device = TunTapDevice::dummy("dummy", "/dev/zero", Type::Dummy).unwrap();
|
||||||
let mut waiter = WaitImpl::testing(&socketv4, &socketv6, &device, 1000).unwrap();
|
let mut waiter = WaitImpl::testing(&socket, &device, 1000).unwrap();
|
||||||
b.iter(|| assert!(waiter.next().is_some()));
|
b.iter(|| assert!(waiter.next().is_some()));
|
||||||
b.bytes = 1400;
|
b.bytes = 1400;
|
||||||
}
|
}
|
||||||
|
|
67
src/cloud.rs
67
src/cloud.rs
|
@ -222,8 +222,7 @@ pub struct GenericCloud<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSou
|
||||||
reconnect_peers: Vec<ReconnectEntry>,
|
reconnect_peers: Vec<ReconnectEntry>,
|
||||||
own_addresses: Vec<SocketAddr>,
|
own_addresses: Vec<SocketAddr>,
|
||||||
table: T,
|
table: T,
|
||||||
socket4: S,
|
socket: S,
|
||||||
socket6: S,
|
|
||||||
device: D,
|
device: D,
|
||||||
crypto: Crypto,
|
crypto: Crypto,
|
||||||
next_peerlist: Time,
|
next_peerlist: Time,
|
||||||
|
@ -248,13 +247,9 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
port_forwarding: Option<PortForwarding>, stats_file: Option<File>
|
port_forwarding: Option<PortForwarding>, stats_file: Option<File>
|
||||||
) -> Self
|
) -> Self
|
||||||
{
|
{
|
||||||
let socket4 = match S::listen_v4("0.0.0.0", config.port) {
|
let socket = match S::listen(config.listen) {
|
||||||
Ok(socket) => socket,
|
Ok(socket) => socket,
|
||||||
Err(err) => fail!("Failed to open ipv4 address 0.0.0.0:{}: {}", config.port, err)
|
Err(err) => fail!("Failed to open socket {}: {}", config.listen, err)
|
||||||
};
|
|
||||||
let socket6 = match S::listen_v6("::", config.port) {
|
|
||||||
Ok(socket) => socket,
|
|
||||||
Err(err) => fail!("Failed to open ipv6 address ::{}: {}", config.port, err)
|
|
||||||
};
|
};
|
||||||
let now = TS::now();
|
let now = TS::now();
|
||||||
let update_freq = config.get_keepalive() as u16;
|
let update_freq = config.get_keepalive() as u16;
|
||||||
|
@ -269,8 +264,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
own_addresses: Vec::new(),
|
own_addresses: Vec::new(),
|
||||||
peer_timeout_publish: config.peer_timeout as u16,
|
peer_timeout_publish: config.peer_timeout as u16,
|
||||||
table,
|
table,
|
||||||
socket4,
|
socket,
|
||||||
socket6,
|
|
||||||
device,
|
device,
|
||||||
next_peerlist: now,
|
next_peerlist: now,
|
||||||
update_freq,
|
update_freq,
|
||||||
|
@ -309,11 +303,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto);
|
let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto);
|
||||||
for addr in self.peers.peers.keys() {
|
for addr in self.peers.peers.keys() {
|
||||||
self.traffic.count_out_traffic(*addr, msg_data.len());
|
self.traffic.count_out_traffic(*addr, msg_data.len());
|
||||||
let socket = match *addr {
|
match self.socket.send(msg_data, *addr) {
|
||||||
SocketAddr::V4(_) => &mut self.socket4,
|
|
||||||
SocketAddr::V6(_) => &mut self.socket6
|
|
||||||
};
|
|
||||||
match socket.send(msg_data, *addr) {
|
|
||||||
Ok(written) if written == msg_data.len() => Ok(()),
|
Ok(written) if written == msg_data.len() => Ok(()),
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Err(Error::Socket("Sent out truncated packet", io::Error::new(io::ErrorKind::Other, "truncated")))
|
Err(Error::Socket("Sent out truncated packet", io::Error::new(io::ErrorKind::Other, "truncated")))
|
||||||
|
@ -335,11 +325,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
// Encrypt and encode
|
// Encrypt and encode
|
||||||
let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto);
|
let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto);
|
||||||
self.traffic.count_out_traffic(addr, msg_data.len());
|
self.traffic.count_out_traffic(addr, msg_data.len());
|
||||||
let socket = match addr {
|
match self.socket.send(msg_data, addr) {
|
||||||
SocketAddr::V4(_) => &mut self.socket4,
|
|
||||||
SocketAddr::V6(_) => &mut self.socket6
|
|
||||||
};
|
|
||||||
match socket.send(msg_data, addr) {
|
|
||||||
Ok(written) if written == msg_data.len() => Ok(()),
|
Ok(written) if written == msg_data.len() => Ok(()),
|
||||||
Ok(_) => Err(Error::Socket("Sent out truncated packet", io::Error::new(io::ErrorKind::Other, "truncated"))),
|
Ok(_) => Err(Error::Socket("Sent out truncated packet", io::Error::new(io::ErrorKind::Other, "truncated"))),
|
||||||
Err(e) => Err(Error::Socket("IOError when sending", e))
|
Err(e) => Err(Error::Socket("IOError when sending", e))
|
||||||
|
@ -354,8 +340,8 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an IOError if the underlying system call fails
|
/// Returns an IOError if the underlying system call fails
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn address(&self) -> io::Result<(SocketAddr, SocketAddr)> {
|
pub fn address(&self) -> io::Result<SocketAddr> {
|
||||||
Ok((self.socket4.address()?, self.socket6.address()?))
|
Ok(self.socket.address()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of peers
|
/// Returns the number of peers
|
||||||
|
@ -747,10 +733,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
fn initialize(&mut self) {
|
fn initialize(&mut self) {
|
||||||
match self.address() {
|
match self.address() {
|
||||||
Err(err) => error!("Failed to obtain local addresses: {}", err),
|
Err(err) => error!("Failed to obtain local addresses: {}", err),
|
||||||
Ok((v4, v6)) => {
|
Ok(addr) => self.own_addresses.push(addr)
|
||||||
self.own_addresses.push(v4);
|
|
||||||
self.own_addresses.push(v6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,13 +747,8 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_socket_v4_event(&mut self, buffer: &mut [u8]) {
|
fn handle_socket_event(&mut self, buffer: &mut [u8]) {
|
||||||
let (size, src) = try_fail!(self.socket4.receive(buffer), "Failed to read from ipv4 network socket: {}");
|
let (size, src) = try_fail!(self.socket.receive(buffer), "Failed to read from network socket: {}");
|
||||||
self.handle_socket_data(src, &mut buffer[..size])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_socket_v6_event(&mut self, buffer: &mut [u8]) {
|
|
||||||
let (size, src) = try_fail!(self.socket6.receive(buffer), "Failed to read from ipv6 network socket: {}");
|
|
||||||
self.handle_socket_data(src, &mut buffer[..size])
|
self.handle_socket_data(src, &mut buffer[..size])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -792,8 +770,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
/// 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 =
|
let waiter = try_fail!(WaitImpl::new(&self.socket, &self.device, 1000), "Failed to setup poll: {}");
|
||||||
try_fail!(WaitImpl::new(&self.socket4, &self.socket6, &self.device, 1000), "Failed to setup poll: {}");
|
|
||||||
let mut buffer = [0; 64 * 1024];
|
let mut buffer = [0; 64 * 1024];
|
||||||
let mut poll_error = false;
|
let mut poll_error = false;
|
||||||
for evt in waiter {
|
for evt in waiter {
|
||||||
|
@ -806,8 +783,7 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
poll_error = true;
|
poll_error = true;
|
||||||
}
|
}
|
||||||
WaitResult::Timeout => {}
|
WaitResult::Timeout => {}
|
||||||
WaitResult::SocketV4 => self.handle_socket_v4_event(&mut buffer),
|
WaitResult::Socket => self.handle_socket_event(&mut buffer),
|
||||||
WaitResult::SocketV6 => self.handle_socket_v6_event(&mut buffer),
|
|
||||||
WaitResult::Device => self.handle_device_event(&mut buffer)
|
WaitResult::Device => self.handle_device_event(&mut buffer)
|
||||||
}
|
}
|
||||||
if self.next_housekeep < TS::now() {
|
if self.next_housekeep < TS::now() {
|
||||||
|
@ -842,26 +818,17 @@ impl<D: Device, P: Protocol, T: Table, S: Socket, TS: TimeSource> GenericCloud<D
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
impl<P: Protocol, T: Table> GenericCloud<MockDevice, P, T, MockSocket, MockTimeSource> {
|
impl<P: Protocol, T: Table> GenericCloud<MockDevice, P, T, MockSocket, MockTimeSource> {
|
||||||
pub fn socket4(&mut self) -> &mut MockSocket {
|
pub fn socket(&mut self) -> &mut MockSocket {
|
||||||
&mut self.socket4
|
&mut self.socket
|
||||||
}
|
|
||||||
|
|
||||||
pub fn socket6(&mut self) -> &mut MockSocket {
|
|
||||||
&mut self.socket6
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device(&mut self) -> &mut MockDevice {
|
pub fn device(&mut self) -> &mut MockDevice {
|
||||||
&mut self.device
|
&mut self.device
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_socket_v4_event(&mut self) {
|
pub fn trigger_socket_event(&mut self) {
|
||||||
let mut buffer = [0; 64 * 1024];
|
let mut buffer = [0; 64 * 1024];
|
||||||
self.handle_socket_v4_event(&mut buffer);
|
self.handle_socket_event(&mut buffer);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_socket_v6_event(&mut self) {
|
|
||||||
let mut buffer = [0; 64 * 1024];
|
|
||||||
self.handle_socket_v6_event(&mut buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_device_event(&mut self) {
|
pub fn trigger_device_event(&mut self) {
|
||||||
|
|
|
@ -12,13 +12,28 @@ use super::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use siphasher::sip::SipHasher24;
|
use siphasher::sip::SipHasher24;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::{
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
net::{IpAddr, Ipv6Addr, SocketAddr}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const HASH_PREFIX: &str = "hash:";
|
const HASH_PREFIX: &str = "hash:";
|
||||||
pub const DEFAULT_PEER_TIMEOUT: u16 = 600;
|
pub const DEFAULT_PEER_TIMEOUT: u16 = 600;
|
||||||
|
|
||||||
|
|
||||||
|
fn parse_listen(addr: &str) -> SocketAddr {
|
||||||
|
if addr.starts_with("*:") {
|
||||||
|
let port = try_fail!(addr[2..].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 {
|
||||||
pub device_type: Type,
|
pub device_type: Type,
|
||||||
|
@ -29,7 +44,7 @@ pub struct Config {
|
||||||
pub crypto: CryptoMethod,
|
pub crypto: CryptoMethod,
|
||||||
pub shared_key: Option<String>,
|
pub shared_key: Option<String>,
|
||||||
pub magic: Option<String>,
|
pub magic: Option<String>,
|
||||||
pub port: u16,
|
pub listen: SocketAddr,
|
||||||
pub peers: Vec<String>,
|
pub peers: Vec<String>,
|
||||||
pub peer_timeout: Duration,
|
pub peer_timeout: Duration,
|
||||||
pub keepalive: Option<Duration>,
|
pub keepalive: Option<Duration>,
|
||||||
|
@ -58,7 +73,7 @@ impl Default for Config {
|
||||||
crypto: CryptoMethod::ChaCha20,
|
crypto: CryptoMethod::ChaCha20,
|
||||||
shared_key: None,
|
shared_key: None,
|
||||||
magic: None,
|
magic: None,
|
||||||
port: 3210,
|
listen: "[::]:3210".parse::<SocketAddr>().unwrap(),
|
||||||
peers: vec![],
|
peers: vec![],
|
||||||
peer_timeout: DEFAULT_PEER_TIMEOUT as Duration,
|
peer_timeout: DEFAULT_PEER_TIMEOUT as Duration,
|
||||||
keepalive: None,
|
keepalive: None,
|
||||||
|
@ -79,6 +94,7 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
pub fn merge_file(&mut self, file: ConfigFile) {
|
pub fn merge_file(&mut self, file: ConfigFile) {
|
||||||
if let Some(val) = file.device_type {
|
if let Some(val) = file.device_type {
|
||||||
self.device_type = val;
|
self.device_type = val;
|
||||||
|
@ -105,7 +121,11 @@ impl Config {
|
||||||
self.magic = Some(val);
|
self.magic = Some(val);
|
||||||
}
|
}
|
||||||
if let Some(val) = file.port {
|
if let Some(val) = file.port {
|
||||||
self.port = val;
|
self.listen = parse_listen(&format!("{}", &val));
|
||||||
|
warn!("The config option 'port' is deprecated, use 'listen' instead.");
|
||||||
|
}
|
||||||
|
if let Some(val) = file.listen {
|
||||||
|
self.listen = parse_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);
|
||||||
|
@ -181,7 +201,7 @@ impl Config {
|
||||||
self.magic = Some(val);
|
self.magic = Some(val);
|
||||||
}
|
}
|
||||||
if let Some(val) = args.flag_listen {
|
if let Some(val) = args.flag_listen {
|
||||||
self.port = val;
|
self.listen = parse_listen(&val);
|
||||||
}
|
}
|
||||||
self.peers.append(&mut args.flag_connect);
|
self.peers.append(&mut args.flag_connect);
|
||||||
if let Some(val) = args.flag_peer_timeout {
|
if let Some(val) = args.flag_peer_timeout {
|
||||||
|
@ -265,6 +285,7 @@ pub struct ConfigFile {
|
||||||
pub shared_key: Option<String>,
|
pub shared_key: Option<String>,
|
||||||
pub magic: Option<String>,
|
pub magic: Option<String>,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
|
pub listen: Option<String>,
|
||||||
pub peers: Option<Vec<String>>,
|
pub peers: Option<Vec<String>>,
|
||||||
pub peer_timeout: Option<Duration>,
|
pub peer_timeout: Option<Duration>,
|
||||||
pub keepalive: Option<Duration>,
|
pub keepalive: Option<Duration>,
|
||||||
|
@ -322,6 +343,7 @@ stats_file: /var/log/vpncloud.stats
|
||||||
shared_key: Some("mysecret".to_string()),
|
shared_key: Some("mysecret".to_string()),
|
||||||
magic: Some("0123ABCD".to_string()),
|
magic: Some("0123ABCD".to_string()),
|
||||||
port: Some(3210),
|
port: Some(3210),
|
||||||
|
listen: None,
|
||||||
peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]),
|
peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]),
|
||||||
peer_timeout: Some(600),
|
peer_timeout: Some(600),
|
||||||
keepalive: Some(840),
|
keepalive: Some(840),
|
||||||
|
@ -352,6 +374,7 @@ fn config_merge() {
|
||||||
shared_key: Some("mysecret".to_string()),
|
shared_key: Some("mysecret".to_string()),
|
||||||
magic: Some("0123ABCD".to_string()),
|
magic: Some("0123ABCD".to_string()),
|
||||||
port: Some(3210),
|
port: Some(3210),
|
||||||
|
listen: None,
|
||||||
peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]),
|
peers: Some(vec!["remote.machine.foo:3210".to_string(), "remote.machine.bar:3210".to_string()]),
|
||||||
peer_timeout: Some(600),
|
peer_timeout: Some(600),
|
||||||
keepalive: Some(840),
|
keepalive: Some(840),
|
||||||
|
@ -376,7 +399,7 @@ fn config_merge() {
|
||||||
magic: Some("0123ABCD".to_string()),
|
magic: Some("0123ABCD".to_string()),
|
||||||
crypto: CryptoMethod::AES256,
|
crypto: CryptoMethod::AES256,
|
||||||
shared_key: Some("mysecret".to_string()),
|
shared_key: Some("mysecret".to_string()),
|
||||||
port: 3210,
|
listen: "[::]:3210".parse::<SocketAddr>().unwrap(),
|
||||||
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),
|
||||||
|
@ -402,7 +425,7 @@ fn config_merge() {
|
||||||
flag_crypto: Some(CryptoMethod::ChaCha20),
|
flag_crypto: Some(CryptoMethod::ChaCha20),
|
||||||
flag_shared_key: Some("anothersecret".to_string()),
|
flag_shared_key: Some("anothersecret".to_string()),
|
||||||
flag_magic: Some("hash:mynet".to_string()),
|
flag_magic: Some("hash:mynet".to_string()),
|
||||||
flag_listen: Some(3211),
|
flag_listen: Some("3211".to_string()),
|
||||||
flag_peer_timeout: Some(1801),
|
flag_peer_timeout: Some(1801),
|
||||||
flag_keepalive: Some(850),
|
flag_keepalive: Some(850),
|
||||||
flag_dst_timeout: Some(301),
|
flag_dst_timeout: Some(301),
|
||||||
|
@ -429,7 +452,7 @@ fn config_merge() {
|
||||||
magic: Some("hash:mynet".to_string()),
|
magic: Some("hash:mynet".to_string()),
|
||||||
crypto: CryptoMethod::ChaCha20,
|
crypto: CryptoMethod::ChaCha20,
|
||||||
shared_key: Some("anothersecret".to_string()),
|
shared_key: Some("anothersecret".to_string()),
|
||||||
port: 3211,
|
listen: "[::]:3211".parse::<SocketAddr>().unwrap(),
|
||||||
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(),
|
||||||
|
|
|
@ -72,7 +72,7 @@ pub struct Args {
|
||||||
flag_crypto: Option<CryptoMethod>,
|
flag_crypto: Option<CryptoMethod>,
|
||||||
flag_subnet: Vec<String>,
|
flag_subnet: Vec<String>,
|
||||||
flag_device: Option<String>,
|
flag_device: Option<String>,
|
||||||
flag_listen: Option<u16>,
|
flag_listen: Option<String>,
|
||||||
flag_network_id: Option<String>,
|
flag_network_id: Option<String>,
|
||||||
flag_magic: Option<String>,
|
flag_magic: Option<String>,
|
||||||
flag_connect: Vec<String>,
|
flag_connect: Vec<String>,
|
||||||
|
@ -270,7 +270,7 @@ fn run<P: Protocol>(config: Config) {
|
||||||
Some(ref key) => Crypto::from_shared_key(config.crypto, key),
|
Some(ref key) => Crypto::from_shared_key(config.crypto, key),
|
||||||
None => Crypto::None
|
None => Crypto::None
|
||||||
};
|
};
|
||||||
let port_forwarding = if config.port_forwarding { PortForwarding::new(config.port) } else { None };
|
let port_forwarding = if config.port_forwarding { PortForwarding::new(config.listen.port()) } 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) => {
|
||||||
|
|
27
src/net.rs
27
src/net.rs
|
@ -1,34 +1,24 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
io::{self, ErrorKind},
|
io::{self, ErrorKind},
|
||||||
net::{SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket},
|
net::{SocketAddr, UdpSocket},
|
||||||
os::unix::io::{AsRawFd, RawFd},
|
os::unix::io::{AsRawFd, RawFd},
|
||||||
sync::atomic::{AtomicBool, Ordering}
|
sync::atomic::{AtomicBool, Ordering}
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::util::{MockTimeSource, Time, TimeSource};
|
use super::util::{MockTimeSource, Time, TimeSource};
|
||||||
|
|
||||||
use net2::UdpBuilder;
|
|
||||||
|
|
||||||
|
|
||||||
pub trait Socket: AsRawFd + Sized {
|
pub trait Socket: AsRawFd + Sized {
|
||||||
fn listen_v4(host: &str, port: u16) -> Result<Self, io::Error>;
|
fn listen(addr: SocketAddr) -> Result<Self, io::Error>;
|
||||||
fn listen_v6(host: &str, port: u16) -> Result<Self, io::Error>;
|
|
||||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error>;
|
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, 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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Socket for UdpSocket {
|
impl Socket for UdpSocket {
|
||||||
fn listen_v4(host: &str, port: u16) -> Result<Self, io::Error> {
|
fn listen(addr: SocketAddr) -> Result<Self, io::Error> {
|
||||||
UdpBuilder::new_v4().expect("Failed to obtain ipv4 socket builder").bind((host, port))
|
UdpSocket::bind(addr)
|
||||||
}
|
|
||||||
fn listen_v6(host: &str, port: u16) -> Result<Self, io::Error> {
|
|
||||||
UdpBuilder::new_v6()
|
|
||||||
.expect("Failed to obtain ipv4 socket builder")
|
|
||||||
.only_v6(true)
|
|
||||||
.expect("Failed to set only_v6")
|
|
||||||
.bind((host, port))
|
|
||||||
}
|
}
|
||||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
||||||
self.recv_from(buffer)
|
self.recv_from(buffer)
|
||||||
|
@ -99,13 +89,8 @@ impl AsRawFd for MockSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Socket for MockSocket {
|
impl Socket for MockSocket {
|
||||||
fn listen_v4(host: &str, port: u16) -> Result<Self, io::Error> {
|
fn listen(addr: SocketAddr) -> Result<Self, io::Error> {
|
||||||
let ip = try_fail!(host.parse(), "Failed to parse IPv4 address: {}");
|
Ok(Self::new(addr))
|
||||||
Ok(Self::new(SocketAddr::V4(SocketAddrV4::new(ip, port))))
|
|
||||||
}
|
|
||||||
fn listen_v6(host: &str, port: u16) -> Result<Self, io::Error> {
|
|
||||||
let ip = try_fail!(host.parse(), "Failed to parse IPv6 address: {}");
|
|
||||||
Ok(Self::new(SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0))))
|
|
||||||
}
|
}
|
||||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
||||||
if let Some((addr, data)) = self.inbound.pop_front() {
|
if let Some((addr, data)) = self.inbound.pop_front() {
|
||||||
|
|
|
@ -13,33 +13,30 @@ use crate::{device::Type, net::Socket};
|
||||||
pub struct EpollWait {
|
pub struct EpollWait {
|
||||||
poll_fd: RawFd,
|
poll_fd: RawFd,
|
||||||
event: libc::epoll_event,
|
event: libc::epoll_event,
|
||||||
socketv4: RawFd,
|
socket: RawFd,
|
||||||
socketv6: RawFd,
|
|
||||||
device: RawFd,
|
device: RawFd,
|
||||||
timeout: u32
|
timeout: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpollWait {
|
impl EpollWait {
|
||||||
pub fn new<S: Socket>(socketv4: &S, socketv6: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
pub fn new<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
||||||
Self::create(socketv4, socketv6, device, timeout, libc::EPOLLIN as u32)
|
Self::create(socket, device, timeout, libc::EPOLLIN as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn testing<S: Socket>(socketv4: &S, socketv6: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
pub fn testing<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
||||||
Self::create(socketv4, socketv6, device, timeout, (libc::EPOLLIN | libc::EPOLLOUT) as u32)
|
Self::create(socket, device, timeout, (libc::EPOLLIN | libc::EPOLLOUT) as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create<S: Socket>(
|
fn create<S: Socket>(socket: &S, device: &dyn Device, timeout: u32, flags: u32) -> io::Result<Self> {
|
||||||
socketv4: &S, socketv6: &S, device: &dyn Device, 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())
|
||||||
}
|
}
|
||||||
let raw_fds = if device.get_type() != Type::Dummy {
|
let raw_fds = if device.get_type() != Type::Dummy {
|
||||||
vec![socketv4.as_raw_fd(), socketv6.as_raw_fd(), device.as_raw_fd()]
|
vec![socket.as_raw_fd(), device.as_raw_fd()]
|
||||||
} else {
|
} else {
|
||||||
vec![socketv4.as_raw_fd(), socketv6.as_raw_fd()]
|
vec![socket.as_raw_fd()]
|
||||||
};
|
};
|
||||||
for fd in raw_fds {
|
for fd in raw_fds {
|
||||||
event.u64 = fd as u64;
|
event.u64 = fd as u64;
|
||||||
|
@ -52,8 +49,7 @@ impl EpollWait {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
poll_fd,
|
poll_fd,
|
||||||
event,
|
event,
|
||||||
socketv4: socketv4.as_raw_fd(),
|
socket: socket.as_raw_fd(),
|
||||||
socketv6: socketv6.as_raw_fd(),
|
|
||||||
device: device.as_raw_fd(),
|
device: device.as_raw_fd(),
|
||||||
timeout
|
timeout
|
||||||
})
|
})
|
||||||
|
@ -74,10 +70,8 @@ impl Iterator for EpollWait {
|
||||||
-1 => WaitResult::Error(io::Error::last_os_error()),
|
-1 => WaitResult::Error(io::Error::last_os_error()),
|
||||||
0 => WaitResult::Timeout,
|
0 => WaitResult::Timeout,
|
||||||
1 => {
|
1 => {
|
||||||
if self.event.u64 == self.socketv4 as u64 {
|
if self.event.u64 == self.socket as u64 {
|
||||||
WaitResult::SocketV4
|
WaitResult::Socket
|
||||||
} else if self.event.u64 == self.socketv6 as u64 {
|
|
||||||
WaitResult::SocketV6
|
|
||||||
} else if self.event.u64 == self.device as u64 {
|
} else if self.event.u64 == self.device as u64 {
|
||||||
WaitResult::Device
|
WaitResult::Device
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,8 +13,7 @@ use std::io;
|
||||||
|
|
||||||
pub enum WaitResult {
|
pub enum WaitResult {
|
||||||
Timeout,
|
Timeout,
|
||||||
SocketV4,
|
Socket,
|
||||||
SocketV6,
|
|
||||||
Device,
|
Device,
|
||||||
Error(io::Error)
|
Error(io::Error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
macro_rules! assert_clean {
|
macro_rules! assert_clean {
|
||||||
($($node: expr),*) => {
|
($($node: expr),*) => {
|
||||||
$(
|
$(
|
||||||
assert_eq!($node.socket4().pop_outbound().map(|(addr, mut msg)| (addr, $node.decode_message(&mut msg).unwrap().without_data())), None);
|
assert_eq!($node.socket().pop_outbound().map(|(addr, mut msg)| (addr, $node.decode_message(&mut msg).unwrap().without_data())), None);
|
||||||
assert_eq!($node.socket6().pop_outbound().map(|(addr, mut msg)| (addr, $node.decode_message(&mut msg).unwrap().without_data())), None);
|
|
||||||
assert_eq!($node.device().pop_outbound(), None);
|
assert_eq!($node.device().pop_outbound(), None);
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
|
@ -10,13 +9,13 @@ macro_rules! assert_clean {
|
||||||
|
|
||||||
macro_rules! assert_message4 {
|
macro_rules! assert_message4 {
|
||||||
($from: expr, $from_addr: expr, $to: expr, $to_addr: expr, $message: expr) => {
|
($from: expr, $from_addr: expr, $to: expr, $to_addr: expr, $message: expr) => {
|
||||||
let (addr, mut data) = msg4_get(&mut $from);
|
let (addr, mut data) = msg_get(&mut $from);
|
||||||
assert_eq!($to_addr, addr);
|
assert_eq!($to_addr, addr);
|
||||||
{
|
{
|
||||||
let message = $from.decode_message(&mut data).unwrap();
|
let message = $from.decode_message(&mut data).unwrap();
|
||||||
assert_eq!($message, message.without_data());
|
assert_eq!($message, message.without_data());
|
||||||
}
|
}
|
||||||
msg4_put(&mut $to, $from_addr, data);
|
msg_put(&mut $to, $from_addr, data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@ mod nat;
|
||||||
mod payload;
|
mod payload;
|
||||||
mod peers;
|
mod peers;
|
||||||
|
|
||||||
pub use std::net::SocketAddr;
|
|
||||||
use std::{
|
use std::{
|
||||||
io::Write,
|
io::Write,
|
||||||
|
net::{IpAddr, Ipv6Addr, SocketAddr},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
Once
|
Once
|
||||||
|
@ -73,13 +73,17 @@ thread_local! {
|
||||||
static NEXT_PORT: AtomicUsize = AtomicUsize::new(1);
|
static NEXT_PORT: AtomicUsize = AtomicUsize::new(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_sock_addr() -> SocketAddr {
|
||||||
|
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), NEXT_PORT.with(|p| p.fetch_add(1, Ordering::Relaxed)) as u16)
|
||||||
|
}
|
||||||
|
|
||||||
fn create_tap_node(nat: bool) -> TapTestNode {
|
fn create_tap_node(nat: bool) -> TapTestNode {
|
||||||
create_tap_node_with_config(nat, Config::default())
|
create_tap_node_with_config(nat, Config::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_tap_node_with_config(nat: bool, mut config: Config) -> TapTestNode {
|
fn create_tap_node_with_config(nat: bool, mut config: Config) -> TapTestNode {
|
||||||
MockSocket::set_nat(nat);
|
MockSocket::set_nat(nat);
|
||||||
config.port = NEXT_PORT.with(|p| p.fetch_add(1, Ordering::Relaxed)) as u16;
|
config.listen = next_sock_addr();
|
||||||
TestNode::new(&config, MockDevice::new(), SwitchTable::new(1800, 10), true, true, vec![], Crypto::None, None, None)
|
TestNode::new(&config, MockDevice::new(), SwitchTable::new(1800, 10), true, true, vec![], Crypto::None, None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +91,7 @@ fn create_tap_node_with_config(nat: bool, mut config: Config) -> TapTestNode {
|
||||||
fn create_tun_node(nat: bool, addresses: Vec<Range>) -> TunTestNode {
|
fn create_tun_node(nat: bool, addresses: Vec<Range>) -> TunTestNode {
|
||||||
MockSocket::set_nat(nat);
|
MockSocket::set_nat(nat);
|
||||||
TestNode::new(
|
TestNode::new(
|
||||||
&Config { port: NEXT_PORT.with(|p| p.fetch_add(1, Ordering::Relaxed)) as u16, ..Config::default() },
|
&Config { listen: next_sock_addr(), ..Config::default() },
|
||||||
MockDevice::new(),
|
MockDevice::new(),
|
||||||
RoutingTable::new(),
|
RoutingTable::new(),
|
||||||
false,
|
false,
|
||||||
|
@ -100,28 +104,15 @@ fn create_tun_node(nat: bool, addresses: Vec<Range>) -> TunTestNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn msg4_get<P: Protocol, T: Table>(node: &mut TestNode<P, T>) -> (SocketAddr, Vec<u8>) {
|
fn msg_get<P: Protocol, T: Table>(node: &mut TestNode<P, T>) -> (SocketAddr, Vec<u8>) {
|
||||||
let msg = node.socket4().pop_outbound();
|
let msg = node.socket().pop_outbound();
|
||||||
assert!(msg.is_some());
|
assert!(msg.is_some());
|
||||||
msg.unwrap()
|
msg.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
fn msg_put<P: Protocol, T: Table>(node: &mut TestNode<P, T>, from: SocketAddr, msg: Vec<u8>) {
|
||||||
fn msg6_get<P: Protocol, T: Table>(node: &mut TestNode<P, T>) -> (SocketAddr, Vec<u8>) {
|
if node.socket().put_inbound(from, msg) {
|
||||||
let msg = node.socket6().pop_outbound();
|
node.trigger_socket_event();
|
||||||
assert!(msg.is_some());
|
|
||||||
msg.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn msg4_put<P: Protocol, T: Table>(node: &mut TestNode<P, T>, from: SocketAddr, msg: Vec<u8>) {
|
|
||||||
if node.socket4().put_inbound(from, msg) {
|
|
||||||
node.trigger_socket_v4_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn msg6_put<P: Protocol, T: Table>(node: &mut TestNode<P, T>, from: SocketAddr, msg: Vec<u8>) {
|
|
||||||
if node.socket6().put_inbound(from, msg) {
|
|
||||||
node.trigger_socket_v6_event();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +127,7 @@ fn simulate<P: Protocol, T: Table>(nodes: &mut [(&mut TestNode<P, T>, SocketAddr
|
||||||
clean = true;
|
clean = true;
|
||||||
let mut msgs = Vec::new();
|
let mut msgs = Vec::new();
|
||||||
for (ref mut node, ref from_addr) in nodes.iter_mut() {
|
for (ref mut node, ref from_addr) in nodes.iter_mut() {
|
||||||
while let Some((to_addr, msg)) = node.socket4().pop_outbound() {
|
while let Some((to_addr, msg)) = node.socket().pop_outbound() {
|
||||||
msgs.push((msg, *from_addr, to_addr));
|
msgs.push((msg, *from_addr, to_addr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,22 +135,7 @@ fn simulate<P: Protocol, T: Table>(nodes: &mut [(&mut TestNode<P, T>, SocketAddr
|
||||||
for (msg, from_addr, to_addr) in msgs {
|
for (msg, from_addr, to_addr) in msgs {
|
||||||
for (ref mut node, ref addr) in nodes.iter_mut() {
|
for (ref mut node, ref addr) in nodes.iter_mut() {
|
||||||
if *addr == to_addr {
|
if *addr == to_addr {
|
||||||
msg4_put(node, from_addr, msg);
|
msg_put(node, from_addr, msg);
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut msgs = Vec::new();
|
|
||||||
for (ref mut node, ref from_addr) in nodes.iter_mut() {
|
|
||||||
while let Some((to_addr, msg)) = node.socket6().pop_outbound() {
|
|
||||||
msgs.push((msg, *from_addr, to_addr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clean &= msgs.is_empty();
|
|
||||||
for (msg, from_addr, to_addr) in msgs {
|
|
||||||
for (ref mut node, ref addr) in nodes.iter_mut() {
|
|
||||||
if *addr == to_addr {
|
|
||||||
msg6_put(node, from_addr, msg);
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ fn connect_via_beacons() {
|
||||||
let beacon_path = "target/.vpncloud_test";
|
let beacon_path = "target/.vpncloud_test";
|
||||||
let mut node1 =
|
let mut node1 =
|
||||||
create_tap_node_with_config(false, Config { beacon_store: Some(beacon_path.to_string()), ..Config::default() });
|
create_tap_node_with_config(false, Config { beacon_store: Some(beacon_path.to_string()), ..Config::default() });
|
||||||
let node1_addr = node1.address().unwrap().0;
|
let node1_addr = node1.address().unwrap();
|
||||||
let mut node2 =
|
let mut node2 =
|
||||||
create_tap_node_with_config(false, Config { beacon_load: Some(beacon_path.to_string()), ..Config::default() });
|
create_tap_node_with_config(false, Config { beacon_load: Some(beacon_path.to_string()), ..Config::default() });
|
||||||
let node2_addr = addr!("2.2.2.2:2222");
|
let node2_addr = addr!("2.2.2.2:2222");
|
||||||
|
@ -162,7 +162,7 @@ fn lost_init1() {
|
||||||
assert_clean!(node1);
|
assert_clean!(node1);
|
||||||
|
|
||||||
// Node 2 -> Node 1: Init 1 | Node 2 -> Node 1: Peers
|
// Node 2 -> Node 1: Init 1 | Node 2 -> Node 1: Peers
|
||||||
assert!(node2.socket4().pop_outbound().is_some());
|
assert!(node2.socket().pop_outbound().is_some());
|
||||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||||
|
|
||||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||||
|
|
12
vpncloud.md
12
vpncloud.md
|
@ -38,9 +38,12 @@ vpncloud(1) -- Peer-to-peer VPN
|
||||||
peers and ignore them otherwise. The **normal** mode is switch for tap
|
peers and ignore them otherwise. The **normal** mode is switch for tap
|
||||||
devices and router for tun devices. [default: `normal`]
|
devices and router for tun devices. [default: `normal`]
|
||||||
|
|
||||||
* `-l <port>`, `--listen <port>`:
|
* `-l <addr>`, `--listen <addr>`:
|
||||||
|
|
||||||
The port number on which to listen for data. [default: `3210`]
|
The address on which to listen for data. This can be simply a port number
|
||||||
|
or a full address in form IP:PORT. If the IP is specified as '*' or only
|
||||||
|
a port number is given, then the socket will listen on all IPs (v4 and v6),
|
||||||
|
otherwise the socket will only listen on the given IP. [default: `3210`]
|
||||||
|
|
||||||
* `-c <addr>`, `--connect <addr>`:
|
* `-c <addr>`, `--connect <addr>`:
|
||||||
|
|
||||||
|
@ -311,7 +314,8 @@ detailed descriptions of the options.
|
||||||
* `crypto`: The encryption method to use. Same as `--crypto`
|
* `crypto`: The encryption method to use. Same as `--crypto`
|
||||||
* `shared_key`: The shared key to encrypt all traffic. Same as `--shared-key`
|
* `shared_key`: The shared key to encrypt all traffic. Same as `--shared-key`
|
||||||
* `magic`: Override the 4-byte magic header of each packet. Same as `--magic`
|
* `magic`: Override the 4-byte magic header of each packet. Same as `--magic`
|
||||||
* `port`: The port number on which to listen for data. Same as `--listen`
|
* `port`: A port number to listen on. This option is DEPRECATED.
|
||||||
|
* `listen`: The address on which to listen for data. Same as `--listen`
|
||||||
* `peers`: A list of addresses to connect to. See `--connect`
|
* `peers`: A list of addresses to connect to. See `--connect`
|
||||||
* `peer_timeout`: Peer timeout in seconds. Same as`--peer-timeout`
|
* `peer_timeout`: Peer timeout in seconds. Same as`--peer-timeout`
|
||||||
* `beacon_store`: Path or command to store beacons. Same as `--beacon-store`
|
* `beacon_store`: Path or command to store beacons. Same as `--beacon-store`
|
||||||
|
@ -334,7 +338,7 @@ device_name: vpncloud%d
|
||||||
ifup: ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up
|
ifup: ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up
|
||||||
crypto: aes256
|
crypto: aes256
|
||||||
shared_key: mysecret
|
shared_key: mysecret
|
||||||
port: 3210
|
listen: 3210
|
||||||
peers:
|
peers:
|
||||||
- remote.machine.foo:3210
|
- remote.machine.foo:3210
|
||||||
- remote.machine.bar:3210
|
- remote.machine.bar:3210
|
||||||
|
|
Loading…
Reference in New Issue