prometheus_wireguard_exporter/src/main.rs

232 lines
8.1 KiB
Rust
Raw Normal View History

//extern crate serde_json;
2019-06-09 20:09:06 +02:00
use clap::{crate_authors, crate_name, crate_version, Arg};
2019-12-01 19:42:47 +01:00
use hyper::{Body, Request};
use log::{debug, info, trace};
2019-04-23 23:06:35 +02:00
use std::env;
mod options;
use options::Options;
mod wireguard;
use std::convert::TryFrom;
use std::process::Command;
mod friendly_description;
pub use friendly_description::*;
2019-04-23 23:06:35 +02:00
use wireguard::WireGuard;
2019-06-09 20:09:06 +02:00
mod exporter_error;
2019-05-15 16:55:07 +02:00
mod wireguard_config;
2019-06-09 20:09:06 +02:00
use prometheus_exporter_base::render_prometheus;
2020-03-24 09:36:13 +01:00
use std::net::IpAddr;
2019-09-27 11:01:27 +02:00
use std::sync::Arc;
use wireguard_config::peer_entry_hashmap_try_from;
2019-04-23 23:06:35 +02:00
2019-12-01 19:42:47 +01:00
async fn perform_request(
2019-04-23 23:06:35 +02:00
_req: Request<Body>,
2019-12-01 19:42:47 +01:00
options: Arc<Options>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let interfaces_to_handle = match &options.interfaces {
Some(interfaces_str) => interfaces_str.clone(),
None => vec!["all".to_owned()],
};
2019-05-17 19:32:35 +02:00
let peer_entry_contents = options
.extract_names_config_files
.as_ref()
.map(|files| {
files // if we have values
.iter() // for each value
.map(|file| std::fs::read_to_string(file as &str)) // read the contents into a String
.collect::<Result<Vec<String>, std::io::Error>>() // And transform it into a vec (stopping in case of errors)
})
.transpose()? // bail out if there was an error
.map(|strings| strings.join("\n")); // now join the strings in a new string
2019-12-01 19:42:47 +01:00
let peer_entry_hashmap = peer_entry_contents
.as_ref()
.map(|contents| peer_entry_hashmap_try_from(contents))
.transpose()?;
trace!("peer_entry_hashmap == {:#?}", peer_entry_hashmap);
2019-12-01 19:42:47 +01:00
let mut wg_accumulator: Option<WireGuard> = None;
for interface_to_handle in interfaces_to_handle {
2020-10-11 16:40:10 +02:00
let output = if options.prepend_sudo {
Command::new("sudo")
.arg("wg")
.arg("show")
.arg(&interface_to_handle)
.arg("dump")
.output()?
} else {
Command::new("wg")
.arg("show")
.arg(&interface_to_handle)
.arg("dump")
.output()?
};
let output_stdout_str = String::from_utf8(output.stdout)?;
trace!(
"wg show {} dump stdout == {}",
interface_to_handle,
output_stdout_str
);
let output_stderr_str = String::from_utf8(output.stderr)?;
trace!(
"wg show {} dump stderr == {}",
interface_to_handle,
output_stderr_str
);
// the output of wg show is different if we use all or we specify an interface.
// In the first case the first column will be the interface name. In the second case
// the interface name will be omitted. We need to compensate for the skew somehow (one
// column less in the second case). We solve this prepending the interface name in every
// line so the output of the second case will be equal to the first case.
let output_stdout_str = if interface_to_handle != "all" {
debug!("injecting {} to the wg show output", interface_to_handle);
let mut result = String::new();
for s in output_stdout_str.lines() {
result.push_str(&format!("{}\t{}\n", interface_to_handle, s));
}
result
} else {
output_stdout_str
};
if let Some(wg_accumulator) = &mut wg_accumulator {
let wg = WireGuard::try_from(&output_stdout_str as &str)?;
wg_accumulator.merge(&wg);
} else {
wg_accumulator = Some(WireGuard::try_from(&output_stdout_str as &str)?);
};
}
if let Some(wg_accumulator) = wg_accumulator {
Ok(wg_accumulator.render_with_names(
peer_entry_hashmap.as_ref(),
2019-12-01 19:42:47 +01:00
options.separate_allowed_ips,
options.export_remote_ip_and_port,
))
} else {
panic!();
2019-12-01 19:42:47 +01:00
}
2019-04-23 23:06:35 +02:00
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2019-06-09 20:09:06 +02:00
let matches = clap::App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!("\n"))
.arg(
Arg::with_name("addr")
.short("l")
.long("address")
.env("PROMETHEUS_WIREGUARD_EXPORTER_ADDRESS")
.help("exporter address")
2019-09-27 11:01:27 +02:00
.default_value("0.0.0.0")
.takes_value(true),
)
2019-04-23 23:06:35 +02:00
.arg(
Arg::with_name("port")
.short("p")
.long("port")
.env("PROMETHEUS_WIREGUARD_EXPORTER_PORT")
2019-05-17 19:32:35 +02:00
.help("exporter port")
2019-05-20 11:23:28 +02:00
.default_value("9586")
2019-04-23 23:06:35 +02:00
.takes_value(true),
)
.arg(
Arg::with_name("verbose")
.short("v")
.long("verbose")
.env("PROMETHEUS_WIREGUARD_EXPORTER_VERBOSE_ENABLED")
2019-04-23 23:06:35 +02:00
.help("verbose logging")
.default_value("false")
.takes_value(true),
2019-04-23 23:06:35 +02:00
)
2020-10-11 16:48:08 +02:00
.arg(
Arg::with_name("prepend_sudo")
.short("a")
.long("prepend_sudo")
.env("PROMETHEUS_WIREGUARD_EXPORTER_PREPEND_SUDO_ENABLED")
2020-10-11 16:48:08 +02:00
.help("Prepend sudo to the wg show commands")
.default_value("false")
.takes_value(true),
2020-10-11 16:48:08 +02:00
)
2019-05-17 19:32:35 +02:00
.arg(
2019-07-11 15:31:25 +02:00
Arg::with_name("separate_allowed_ips")
.short("s")
.long("separate_allowed_ips")
.env("PROMETHEUS_WIREGUARD_EXPORTER_SEPARATE_ALLOWED_IPS_ENABLED")
2019-07-11 15:31:25 +02:00
.help("separate allowed ips and ports")
.default_value("false")
.takes_value(true),
2019-07-11 15:31:25 +02:00
)
.arg(
Arg::with_name("export_remote_ip_and_port")
.short("r")
.long("export_remote_ip_and_port")
.env("PROMETHEUS_WIREGUARD_EXPORTER_EXPORT_REMOTE_IP_AND_PORT_ENABLED")
.help("exports peer's remote ip and port as labels (if available)")
.default_value("false")
.takes_value(true),
)
2019-12-01 19:42:47 +01:00
.arg(
Arg::with_name("extract_names_config_files")
2019-05-17 19:32:35 +02:00
.short("n")
.long("extract_names_config_files")
.env("PROMETHEUS_WIREGUARD_EXPORTER_CONFIG_FILE_NAMES")
.help("If set, the exporter will look in the specified WireGuard config file for peer names (must be in [Peer] definition and be a comment). Multiple files are supported.")
.multiple(true)
.use_delimiter(true)
.number_of_values(1)
2019-05-17 19:32:35 +02:00
.takes_value(true))
.arg(
Arg::with_name("interfaces")
.short("i")
.long("interfaces")
.env("PROMETHEUS_WIREGUARD_EXPORTER_INTERFACES")
.help("If set specifies the interface passed to the wg show command. It is relative to the same position config_file. In not specified, all will be passed.")
.multiple(true)
.use_delimiter(true)
.number_of_values(1)
.takes_value(true))
2019-04-23 23:06:35 +02:00
.get_matches();
let options = Options::from_claps(&matches);
if options.verbose {
2019-06-09 20:09:06 +02:00
env::set_var(
"RUST_LOG",
format!("{}=trace,prometheus_exporter_base=trace", crate_name!()),
);
2019-04-23 23:06:35 +02:00
} else {
2019-06-09 20:09:06 +02:00
env::set_var(
"RUST_LOG",
format!("{}=info,prometheus_exporter_base=info", crate_name!()),
);
2019-04-23 23:06:35 +02:00
}
env_logger::init();
2020-05-10 15:40:45 +02:00
info!(
"{} v{} starting...",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
);
2019-04-23 23:06:35 +02:00
info!("using options: {:?}", options);
let bind = matches.value_of("port").unwrap();
let bind = bind.parse::<u16>().expect("port must be a valid number");
2020-03-24 09:36:13 +01:00
let ip = matches.value_of("addr").unwrap().parse::<IpAddr>().unwrap();
let addr = (ip, bind).into();
2019-04-23 23:06:35 +02:00
info!("starting exporter on http://{}/metrics", addr);
2019-04-23 23:06:35 +02:00
2019-12-01 19:42:47 +01:00
render_prometheus(addr, options, |request, options| {
Box::pin(perform_request(request, options))
})
.await;
Ok(())
2019-04-23 23:06:35 +02:00
}