Added statistics file

pull/29/head
Dennis Schwerdel 2019-01-09 17:45:12 +01:00
parent 6a3208949e
commit fe25ecec1d
12 changed files with 294 additions and 32 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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<FnvHasher>;
pub type Hash = BuildHasherDefault<FnvHasher>;
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<SocketAddr>,
}
struct PeerList {
timeout: Duration,
peers: HashMap<SocketAddr, (Time, NodeId, Vec<SocketAddr>), Hash>,
peers: HashMap<SocketAddr, PeerData, Hash>,
nodes: HashMap<NodeId, SocketAddr, Hash>,
addresses: HashSet<SocketAddr, Hash>
}
@ -52,17 +61,17 @@ impl PeerList {
fn timeout(&mut self) -> Vec<SocketAddr> {
let now = now();
let mut del: Vec<SocketAddr> = 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<W: Write>(&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<P: Protocol, T: Table> {
update_freq: Duration,
buffer_out: [u8; 64*1024],
next_housekeep: Time,
next_stats_out: Time,
port_forwarding: Option<PortForwarding>,
traffic: TrafficStats,
stats_file: Option<String>,
_dummy_p: PhantomData<P>,
}
@ -192,7 +217,8 @@ impl<P: Protocol, T: Table> GenericCloud<P, T> {
#[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<Range>,
crypto: Crypto, port_forwarding: Option<PortForwarding>) -> Self {
crypto: Crypto, port_forwarding: Option<PortForwarding>, stats_file: Option<String>
) -> 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<P: Protocol, T: Table> GenericCloud<P, T> {
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<P: Protocol, T: Table> GenericCloud<P, T> {
// 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<P: Protocol, T: Table> GenericCloud<P, T> {
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<P: Protocol, T: Table> GenericCloud<P, T> {
// 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<P: Protocol, T: Table> GenericCloud<P, T> {
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<P: Protocol, T: Table> GenericCloud<P, T> {
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<P: Protocol, T: Table> GenericCloud<P, T> {
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);
}

View File

@ -31,6 +31,7 @@ pub struct Config {
pub port_forwarding: bool,
pub daemonize: bool,
pub pid_file: Option<String>,
pub stats_file: Option<String>,
pub user: Option<String>,
pub group: Option<String>
}
@ -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<Vec<String>>,
pub port_forwarding: Option<bool>,
pub pid_file: Option<String>,
pub stats_file: Option<String>,
pub user: Option<String>,
pub group: Option<String>,
}

View File

@ -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<W: Write>(&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<u8>, addr: SocketAddr) {

View File

@ -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<W: Write>(&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 {

View File

@ -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<String>,
flag_stats_file: Option<String>,
flag_user: Option<String>,
flag_group: Option<String>,
flag_log_file: Option<String>
@ -159,13 +161,13 @@ impl<P: Protocol> AnyCloud<P> {
#[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<Range>,
crypto: Crypto, port_forwarding: Option<PortForwarding>) -> Self {
crypto: Crypto, port_forwarding: Option<PortForwarding>, stats_file: Option<String>) -> Self {
match table {
AnyTable::Switch(t) => AnyCloud::Switch(GenericCloud::<P, SwitchTable>::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::<P, RoutingTable>::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<P: Protocol> (config: Config) {
} else {
None
};
let mut cloud = AnyCloud::<P>::new(magic, device, config.port, table, peer_timeout, learning, broadcasting, ranges, crypto, port_forwarding);
let mut cloud = AnyCloud::<P>::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());
}

134
src/traffic.rs Normal file
View File

@ -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<SocketAddr, TrafficEntry, Hash>,
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<usize>) {
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<Item=(&SocketAddr, &TrafficEntry)> {
self.peers.iter()
}
pub fn get_payload_traffic(&self) -> impl Iterator<Item=(&(Address, Address), &TrafficEntry)> {
self.payload.iter()
}
#[inline]
pub fn write_out<W: Write>(&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(())
}
}

View File

@ -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<u8>, SocketAddr);
fn lookup(&mut self, &Address) -> Option<SocketAddr>;
fn housekeep(&mut self);
fn write_out<W: Write>(&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)
}
}
}
}

View File

@ -28,6 +28,7 @@ Options:
--user <user> Run as other user when daemonizing.
--group <group> Run as other group when daemonizing.
--log-file <file> Print logs also to this file.
--stats-file <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.

View File

@ -136,3 +136,33 @@ pub fn resolve<Addr: ToSocketAddrs+fmt::Debug>(addr: Addr) -> Result<Vec<SocketA
addrs.dedup();
Ok(addrs)
}
pub struct Bytes(pub u64);
impl fmt::Display for Bytes {
fn fmt(&self, formatter: &mut fmt::Formatter) -> 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)
}
}

View File

@ -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 <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)