diff --git a/deb/vpncloud/vpncloud-control b/deb/vpncloud/vpncloud-control index 0970c6d..6c68f36 100755 --- a/deb/vpncloud/vpncloud-control +++ b/deb/vpncloud/vpncloud-control @@ -38,7 +38,7 @@ start() { # 2 if daemon could not be started for net in $NETWORKS; do [ -f "$NETCONFIGS/$net.net" ] || continue - start-stop-daemon --start --pidfile /run/$NAME-$net.pid --name $NAME -- "$DAEMON --daemon --config $NETCONFIGS/$net.net --pid-file /run/$NAME-$net.pid" + start-stop-daemon --start --pidfile /run/$NAME-$net.pid --name $NAME -- "$DAEMON --daemon --config $NETCONFIGS/$net.net --log-file /var/log/$NAME-$net.log --ststs-file /var/log/$NAME-$net.stats --pid-file /run/$NAME-$net.pid" done return 0 } diff --git a/deb/vpncloud/vpncloud@.service b/deb/vpncloud/vpncloud@.service index 73f658f..915ae70 100644 --- a/deb/vpncloud/vpncloud@.service +++ b/deb/vpncloud/vpncloud@.service @@ -4,7 +4,7 @@ Before=systemd-user-sessions.service [Service] Type=forking -ExecStart=/usr/bin/vpncloud --config /etc/vpncloud/%i.net --daemon --log-file /var/log/vpncloud-%i.log --pid-file /run/vpncloud-%i.run +ExecStart=/usr/bin/vpncloud --config /etc/vpncloud/%i.net --daemon --log-file /var/log/vpncloud-%i.log --stats-file /var/log/vpncloud-%i.stats --pid-file /run/vpncloud-%i.run WorkingDirectory=/etc/vpncloud PIDFile=/run/vpncloud-%i.run diff --git a/src/cloud.rs b/src/cloud.rs index 7f4468a..01b7bda 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -5,13 +5,14 @@ use std::net::{SocketAddr, ToSocketAddrs}; use std::collections::{HashMap, HashSet}; use std::net::UdpSocket; -use std::io; +use std::io::{self, Write}; use std::fmt; use std::os::unix::io::AsRawFd; use std::marker::PhantomData; use std::hash::BuildHasherDefault; use std::time::Instant; use std::cmp::min; +use std::fs::File; use fnv::FnvHasher; use signal::{trap::Trap, Signal}; @@ -25,16 +26,24 @@ use super::crypto::Crypto; use super::port_forwarding::PortForwarding; use super::util::{now, Time, Duration, resolve}; use super::poll::{Poll, Flags}; +use super::traffic::TrafficStats; -type Hash = BuildHasherDefault; +pub type Hash = BuildHasherDefault; const MAX_RECONNECT_INTERVAL: u16 = 3600; const RESOLVE_INTERVAL: Time = 300; +pub const STATS_INTERVAL: Time = 60; +struct PeerData { + timeout: Time, + node_id: NodeId, + alt_addrs: Vec, +} + struct PeerList { timeout: Duration, - peers: HashMap), Hash>, + peers: HashMap, nodes: HashMap, addresses: HashSet } @@ -52,17 +61,17 @@ impl PeerList { fn timeout(&mut self) -> Vec { let now = now(); let mut del: Vec = Vec::new(); - for (&addr, &(timeout, _nodeid, ref _alt_addrs)) in &self.peers { - if timeout < now { + for (&addr, ref data) in &self.peers { + if data.timeout < now { del.push(addr); } } for addr in &del { info!("Forgot peer: {}", addr); - if let Some((_timeout, nodeid, alt_addrs)) = self.peers.remove(addr) { - self.nodes.remove(&nodeid); + if let Some(data) = self.peers.remove(addr) { + self.nodes.remove(&data.node_id); self.addresses.remove(addr); - for addr in &alt_addrs { + for addr in &data.alt_addrs { self.addresses.remove(addr); } } @@ -95,23 +104,27 @@ impl PeerList { fn add(&mut self, node_id: NodeId, addr: SocketAddr) { if self.nodes.insert(node_id, addr).is_none() { info!("New peer: {}", addr); - self.peers.insert(addr, (now()+Time::from(self.timeout), node_id, vec![])); + self.peers.insert(addr, PeerData { + timeout: now() + Time::from(self.timeout), + node_id, + alt_addrs: vec![] + }); self.addresses.insert(addr); } } #[inline] fn refresh(&mut self, addr: &SocketAddr) { - if let Some(&mut (ref mut timeout, _node_id, ref _alt_addrs)) = self.peers.get_mut(addr) { - *timeout = now()+Time::from(self.timeout); + if let Some(ref mut data) = self.peers.get_mut(addr) { + data.timeout = now()+Time::from(self.timeout); } } #[inline] fn add_alt_addr(&mut self, node_id: NodeId, addr: SocketAddr) { if let Some(main_addr) = self.nodes.get(&node_id) { - if let Some(&mut (_timeout, _node_id, ref mut alt_addrs)) = self.peers.get_mut(main_addr) { - alt_addrs.push(addr); + if let Some(ref mut data) = self.peers.get_mut(main_addr) { + data.alt_addrs.push(addr); self.addresses.insert(addr); } else { error!("Main address for node is not connected"); @@ -144,15 +157,24 @@ impl PeerList { #[inline] fn remove(&mut self, addr: &SocketAddr) { - if let Some((_timeout, node_id, alt_addrs)) = self.peers.remove(addr) { + if let Some(data) = self.peers.remove(addr) { info!("Removed peer: {}", addr); - self.nodes.remove(&node_id); + self.nodes.remove(&data.node_id); self.addresses.remove(addr); - for addr in alt_addrs { + for addr in data.alt_addrs { self.addresses.remove(&addr); } } } + + #[inline] + fn write_out(&self, out: &mut W) -> Result<(), io::Error> { + try!(writeln!(out, "Peers:")); + for (addr, data) in &self.peers { + try!(writeln!(out, " - {} (ttl: {} s)", addr, data.timeout-now())); + } + Ok(()) + } } #[derive(Clone)] @@ -184,7 +206,10 @@ pub struct GenericCloud { update_freq: Duration, buffer_out: [u8; 64*1024], next_housekeep: Time, + next_stats_out: Time, port_forwarding: Option, + traffic: TrafficStats, + stats_file: Option, _dummy_p: PhantomData

, } @@ -192,7 +217,8 @@ impl GenericCloud { #[allow(unknown_lints,clippy::too_many_arguments)] pub fn new(magic: HeaderMagic, device: Device, listen: u16, table: T, peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec, - crypto: Crypto, port_forwarding: Option) -> Self { + crypto: Crypto, port_forwarding: Option, stats_file: Option + ) -> Self { let socket4 = match UdpBuilder::new_v4().expect("Failed to obtain ipv4 socket builder") .reuse_address(true).expect("Failed to set so_reuseaddr").bind(("0.0.0.0", listen)) { Ok(socket) => socket, @@ -222,7 +248,10 @@ impl GenericCloud { update_freq: peer_timeout/2-60, buffer_out: [0; 64*1024], next_housekeep: now(), + next_stats_out: now() + STATS_INTERVAL, port_forwarding, + traffic: TrafficStats::new(), + stats_file, _dummy_p: PhantomData, } } @@ -244,6 +273,7 @@ impl GenericCloud { // Encrypt and encode once and send several times let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto); for addr in self.peers.peers.keys() { + self.traffic.count_out_traffic(*addr, msg_data.len()); let socket = match *addr { SocketAddr::V4(_) => &self.socket4, SocketAddr::V6(_) => &self.socket6 @@ -267,6 +297,7 @@ impl GenericCloud { debug!("Sending {:?} to {}", msg, addr); // Encrypt and encode let msg_data = encode(msg, &mut self.buffer_out, self.magic, &mut self.crypto); + self.traffic.count_out_traffic(addr, msg_data.len()); let socket = match addr { SocketAddr::V4(_) => &self.socket4, SocketAddr::V6(_) => &self.socket6 @@ -423,6 +454,26 @@ impl GenericCloud { // Schedule next connection attempt entry.next = now + Time::from(entry.timeout); } + if self.next_stats_out < now { + // Write out the statistics + try!(self.write_out_stats().map_err(|err| Error::File("Failed to write stats file", err))); + self.next_stats_out = now + STATS_INTERVAL; + self.traffic.period(Some(60)); + } + Ok(()) + } + + /// Calculates, resets and writes out the statistics to a file + fn write_out_stats(&mut self) -> Result<(), io::Error> { + if self.stats_file.is_none() { return Ok(()) } + debug!("Writing out stats"); + let mut f = try!(File::create(self.stats_file.as_ref().unwrap())); + try!(self.peers.write_out(&mut f)); + try!(writeln!(&mut f)); + try!(self.table.write_out(&mut f)); + try!(writeln!(&mut f)); + try!(self.traffic.write_out(&mut f)); + try!(writeln!(&mut f)); Ok(()) } @@ -445,12 +496,13 @@ impl GenericCloud { pub fn handle_interface_data(&mut self, payload: &mut [u8], start: usize, end: usize) -> Result<(), Error> { let (src, dst) = try!(P::parse(&payload[start..end])); debug!("Read data from interface: src: {}, dst: {}, {} bytes", src, dst, end-start); + self.traffic.count_out_payload(dst, src, end-start); match self.table.lookup(&dst) { Some(addr) => { // Peer found for destination debug!("Found destination for {} => {}", dst, addr); try!(self.send_msg(addr, &mut Message::Data(payload, start, end))); if !self.peers.contains_addr(&addr) { - // If the peer is not actually conected, remove the entry in the table and try + // If the peer is not actually connected, remove the entry in the table and try // to reconnect. warn!("Destination for {} not found in peers: {}", dst, addr); self.table.remove(&dst); @@ -507,8 +559,9 @@ impl GenericCloud { debug!("Received {:?} from {}", msg, peer); match msg { Message::Data(payload, start, end) => { - let (src, _dst) = try!(P::parse(&payload[start..end])); + let (src, dst) = try!(P::parse(&payload[start..end])); debug!("Writing data to device: {} bytes", end-start); + self.traffic.count_in_payload(src, dst, end-start); if let Err(e) = self.device.write(&mut payload[..end], start) { error!("Failed to send via device: {}", e); return Err(e); @@ -605,6 +658,7 @@ impl GenericCloud { fd if fd == socket6_fd => try_fail!(self.socket6.recv_from(&mut buffer), "Failed to read from ipv6 network socket: {}"), _ => unreachable!() }; + self.traffic.count_in_traffic(src, size); if let Err(e) = decode(&mut buffer[..size], self.magic, &mut self.crypto).and_then(|msg| self.handle_net_message(src, msg)) { error!("Error: {}, from: {}", e, src); } diff --git a/src/config.rs b/src/config.rs index ec0e499..8de80f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,7 @@ pub struct Config { pub port_forwarding: bool, pub daemonize: bool, pub pid_file: Option, + pub stats_file: Option, pub user: Option, pub group: Option } @@ -48,6 +49,7 @@ impl Default for Config { port_forwarding: true, daemonize: false, pid_file: None, + stats_file: None, user: None, group: None } @@ -101,6 +103,9 @@ impl Config { if let Some(val) = file.pid_file { self.pid_file = Some(val); } + if let Some(val) = file.stats_file { + self.stats_file = Some(val); + } if let Some(val) = file.user { self.user = Some(val); } @@ -158,6 +163,9 @@ impl Config { if let Some(val) = args.flag_pid_file { self.pid_file = Some(val); } + if let Some(val) = args.flag_stats_file { + self.stats_file = Some(val); + } if let Some(val) = args.flag_user { self.user = Some(val); } @@ -204,6 +212,7 @@ pub struct ConfigFile { pub subnets: Option>, pub port_forwarding: Option, pub pid_file: Option, + pub stats_file: Option, pub user: Option, pub group: Option, } diff --git a/src/ethernet.rs b/src/ethernet.rs index 8939fd9..0ff4343 100644 --- a/src/ethernet.rs +++ b/src/ethernet.rs @@ -6,6 +6,7 @@ use std::net::SocketAddr; use std::collections::HashMap; use std::collections::hash_map::Entry; use std::hash::BuildHasherDefault; +use std::io::{self, Write}; use fnv::FnvHasher; @@ -99,6 +100,16 @@ impl Table for SwitchTable { } } + /// Write out the table + fn write_out(&self, out: &mut W) -> Result<(), io::Error> { + let now = now(); + try!(writeln!(out, "Switch table:")); + for (addr, val) in &self.table { + try!(writeln!(out, " - {} => {} (ttl: {} s)", addr, val.address, val.timeout - now)); + } + Ok(()) + } + /// Learns the given address, inserting it in the hash map #[inline] fn learn(&mut self, key: Address, _prefix_len: Option, addr: SocketAddr) { diff --git a/src/ip.rs b/src/ip.rs index 3109f25..a715f21 100644 --- a/src/ip.rs +++ b/src/ip.rs @@ -5,6 +5,7 @@ use std::net::SocketAddr; use std::collections::{hash_map, HashMap}; use std::hash::BuildHasherDefault; +use std::io::{self, Write}; use fnv::FnvHasher; @@ -53,7 +54,7 @@ impl Protocol for Packet { struct RoutingEntry { address: SocketAddr, - bytes: [u8; 16], + bytes: Address, prefix_len: u8 } @@ -90,7 +91,7 @@ impl Table for RoutingTable { let mut group_bytes = [0; 16]; group_bytes[..group_len].copy_from_slice(&addr.data[..group_len]); // Create an entry - let routing_entry = RoutingEntry{address, bytes: addr.data, prefix_len}; + let routing_entry = RoutingEntry{address, bytes: addr, prefix_len}; // Add the entry to the routing table, creating a new list of the prefix group is empty. match self.0.entry(group_bytes) { hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(routing_entry), @@ -120,7 +121,7 @@ impl Table for RoutingTable { // Calculate the match length of the address and the prefix let mut match_len = 0; for j in 0..addr.len as usize { - let b = addr.data[j] ^ entry.bytes[j]; + let b = addr.data[j] ^ entry.bytes.data[j]; if b == 0 { match_len += 8; } else { @@ -146,6 +147,17 @@ impl Table for RoutingTable { //nothing to do } + /// Write out the table + fn write_out(&self, out: &mut W) -> Result<(), io::Error> { + try!(writeln!(out, "Routing table:")); + for entries in self.0.values() { + for entry in entries { + try!(writeln!(out, " - {}/{} => {}", entry.bytes, entry.prefix_len, entry.address)); + } + } + Ok(()) + } + /// Removes an address from the map and returns whether something has been removed #[inline] fn remove(&mut self, _addr: &Address) -> bool { diff --git a/src/main.rs b/src/main.rs index be04ffc..7cbbf12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ pub mod device; pub mod poll; pub mod config; pub mod port_forwarding; +pub mod traffic; #[cfg(test)] mod tests; #[cfg(feature = "bench")] mod benches; @@ -86,6 +87,7 @@ pub struct Args { flag_no_port_forwarding: bool, flag_daemon: bool, flag_pid_file: Option, + flag_stats_file: Option, flag_user: Option, flag_group: Option, flag_log_file: Option @@ -159,13 +161,13 @@ impl AnyCloud

{ #[allow(unknown_lints,clippy::too_many_arguments)] fn new(magic: HeaderMagic, device: Device, listen: u16, table: AnyTable, peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec, - crypto: Crypto, port_forwarding: Option) -> Self { + crypto: Crypto, port_forwarding: Option, stats_file: Option) -> Self { match table { AnyTable::Switch(t) => AnyCloud::Switch(GenericCloud::::new( - magic, device, listen, t, peer_timeout, learning, broadcast, addresses, crypto, port_forwarding + magic, device, listen, t, peer_timeout, learning, broadcast, addresses, crypto, port_forwarding, stats_file )), AnyTable::Routing(t) => AnyCloud::Routing(GenericCloud::::new( - magic, device, listen, t, peer_timeout, learning, broadcast, addresses, crypto, port_forwarding + magic, device, listen, t, peer_timeout, learning, broadcast, addresses, crypto, port_forwarding, stats_file )) } } @@ -230,7 +232,7 @@ fn run (config: Config) { } else { None }; - let mut cloud = AnyCloud::

::new(magic, device, config.port, table, peer_timeout, learning, broadcasting, ranges, crypto, port_forwarding); + let mut cloud = AnyCloud::

::new(magic, device, config.port, table, peer_timeout, learning, broadcasting, ranges, crypto, port_forwarding, config.stats_file); if let Some(script) = config.ifup { run_script(&script, cloud.ifname()); } diff --git a/src/traffic.rs b/src/traffic.rs new file mode 100644 index 0000000..8d8bffd --- /dev/null +++ b/src/traffic.rs @@ -0,0 +1,134 @@ +use std::net::SocketAddr; +use std::collections::HashMap; +use std::io::{self, Write}; + +use super::types::Address; +use super::cloud::Hash; +use super::util::Bytes; + + +pub struct TrafficEntry { + pub out_bytes_total: u64, + pub out_packets_total: usize, + pub out_bytes: u64, + pub out_packets: usize, + pub in_bytes_total: u64, + pub in_packets_total: usize, + pub in_bytes: u64, + pub in_packets: usize, + pub idle_periods: usize +} + +impl TrafficEntry { + pub fn new() -> Self { + TrafficEntry { + out_bytes_total: 0, + out_packets_total: 0, + out_bytes: 0, + out_packets: 0, + in_bytes_total: 0, + in_packets_total: 0, + in_bytes: 0, + in_packets: 0, + idle_periods: 0 + } + } + + #[inline] + fn count_out(&mut self, bytes: usize) { + self.out_packets += 1; + self.out_bytes += bytes as u64; + } + + #[inline] + fn count_in(&mut self, bytes: usize) { + self.in_packets += 1; + self.in_bytes += bytes as u64; + } + + fn period(&mut self) { + self.out_bytes_total += self.out_bytes; + self.out_packets_total += self.out_packets; + self.in_bytes_total += self.in_bytes; + self.in_packets_total += self.in_packets; + if self.in_packets == 0 && self.out_packets == 0 { + self.idle_periods += 1; + } else { + self.idle_periods = 0; + } + self.out_packets = 0; + self.in_packets = 0; + self.out_bytes = 0; + self.in_bytes = 0; + } +} + +pub struct TrafficStats { + peers: HashMap, + payload: HashMap<(Address, Address), TrafficEntry, Hash> +} + +impl TrafficStats { + pub fn new() -> Self { + Self { peers: Default::default(), payload: Default::default() } + } + + #[inline] + pub fn count_out_traffic(&mut self, peer: SocketAddr, bytes: usize) { + self.peers.entry(peer).or_insert_with(TrafficEntry::new).count_out(bytes); + } + + #[inline] + pub fn count_in_traffic(&mut self, peer: SocketAddr, bytes: usize) { + self.peers.entry(peer).or_insert_with(TrafficEntry::new).count_in(bytes); + } + + #[inline] + pub fn count_out_payload(&mut self, remote: Address, local: Address, bytes: usize) { + self.payload.entry((remote, local)).or_insert_with(TrafficEntry::new).count_out(bytes); + } + + #[inline] + pub fn count_in_payload(&mut self, remote: Address, local: Address, bytes: usize) { + self.payload.entry((remote, local)).or_insert_with(TrafficEntry::new).count_in(bytes); + } + + pub fn period(&mut self, cleanup_idle: Option) { + for entry in self.peers.values_mut() { + entry.period(); + } + for entry in self.payload.values_mut() { + entry.period(); + } + if let Some(periods) = cleanup_idle { + self.peers.retain(|_, entry| entry.idle_periods < periods); + self.payload.retain(|_, entry| entry.idle_periods < periods); + } + } + + pub fn get_peer_traffic(&self) -> impl Iterator { + self.peers.iter() + } + + pub fn get_payload_traffic(&self) -> impl Iterator { + self.payload.iter() + } + + #[inline] + pub fn write_out(&self, out: &mut W) -> Result<(), io::Error> { + try!(writeln!(out, "Peer traffic:")); + let mut peers: Vec<_> = self.get_peer_traffic().collect(); + peers.sort_unstable_by_key(|(_, data)| (data.out_bytes + data.in_bytes)); + for (addr, data) in peers.iter().rev() { + try!(writeln!(out, " - {}: in={}/s, out={}/s", addr, Bytes(data.in_bytes/60), Bytes(data.out_bytes/60))); + } + try!(writeln!(out)); + try!(writeln!(out, "Payload traffic:")); + let mut payload: Vec<_> = self.get_payload_traffic().collect(); + payload.sort_unstable_by_key(|(_, data)| (data.out_bytes + data.in_bytes)); + for ((remote, local), data) in payload.iter().rev() { + try!(writeln!(out, " - {} <-> {}: in={}/s, out={}/s", remote, local, Bytes(data.in_bytes/60), Bytes(data.out_bytes/60))); + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index 9553fca..c1e7ca1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,7 +6,7 @@ use std::net::{SocketAddr, Ipv4Addr, Ipv6Addr}; use std::fmt; use std::str::FromStr; use std::hash::{Hash, Hasher}; -use std::io; +use std::io::{self, Write}; use super::util::{bytes_to_hex, Encoder}; @@ -210,6 +210,7 @@ pub trait Table { fn learn(&mut self, Address, Option, SocketAddr); fn lookup(&mut self, &Address) -> Option; fn housekeep(&mut self); + fn write_out(&self, out: &mut W) -> Result<(), io::Error>; fn remove(&mut self, &Address) -> bool; fn remove_all(&mut self, &SocketAddr); } @@ -225,7 +226,8 @@ pub enum Error { Socket(&'static str, io::Error), Name(String), TunTapDev(&'static str, io::Error), - Crypto(&'static str) + Crypto(&'static str), + File(&'static str, io::Error) } impl fmt::Display for Error { fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> { @@ -236,6 +238,7 @@ impl fmt::Display for Error { Error::Crypto(msg) => write!(formatter, "{}", msg), Error::Name(ref name) => write!(formatter, "failed to resolve name '{}'", name), Error::WrongHeaderMagic(net) => write!(formatter, "wrong header magic: {}", bytes_to_hex(&net)), + Error::File(msg, ref err) => write!(formatter, "{}: {:?}", msg, err) } } -} +} \ No newline at end of file diff --git a/src/usage.txt b/src/usage.txt index 0ed4a1f..a57a07e 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -28,6 +28,7 @@ Options: --user Run as other user when daemonizing. --group Run as other group when daemonizing. --log-file Print logs also to this file. + --stats-file Print statistics to this file. --no-port-forwarding Disable automatic port forward. --daemon Run the process in the background. -v, --verbose Print debug information. diff --git a/src/util.rs b/src/util.rs index e97cc92..c9afa78 100644 --- a/src/util.rs +++ b/src/util.rs @@ -136,3 +136,33 @@ pub fn resolve(addr: Addr) -> Result Result<(), fmt::Error> { + let mut size = self.0 as f32; + if size >= 512.0 { + size /= 1024.0; + } else { + return write!(formatter, "{:.0} B", size); + } + if size >= 512.0 { + size /= 1024.0; + } else { + return write!(formatter, "{:.1} KiB", size); + } + if size >= 512.0 { + size /= 1024.0; + } else { + return write!(formatter, "{:.1} MiB", size); + } + if size >= 512.0 { + size /= 1024.0; + } else { + return write!(formatter, "{:.1} GiB", size); + } + write!(formatter, "{:.1} TiB", size) + } +} diff --git a/vpncloud.md b/vpncloud.md index 05c1b71..a90929c 100644 --- a/vpncloud.md +++ b/vpncloud.md @@ -118,6 +118,11 @@ vpncloud(1) -- Peer-to-peer VPN If set, print logs also to the given file. The file will be created and truncated if is exists. + * `--stats-file `: + + If set, periodically write statistics on peers and current traffic to the + given file. The file will be periodically overwritten with new data. + * `--daemon`: Spawn a background process instead of running the process in the foreground. @@ -280,6 +285,7 @@ detailed descriptions of the options. * `user`: The name of a user to run the background process under. See `--user` * `group`: The name of a group to run the background process under. See `--group` * `pid_file`: The path of the pid file to create. See `--pid-file` +* `stats_file`: The path of the statistics file. See `--stats-file` ### Example @@ -422,5 +428,5 @@ alive. ## COPYRIGHT -Copyright (C) 2015-2016 Dennis Schwerdel +Copyright (C) 2015-2019 Dennis Schwerdel This software is licensed under GPL-3 or newer (see LICENSE.md)