diff --git a/Cargo.lock b/Cargo.lock index 5d1cde7..8b44d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,7 @@ dependencies = [ [[package]] name = "prometheus_wireguard_exporter" -version = "3.3.0" +version = "3.3.1" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 72811c7..a2daca8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus_wireguard_exporter" -version = "3.3.0" +version = "3.3.1" authors = ["Francesco Cogno "] description = "Prometheus WireGuard Exporter" edition = "2018" diff --git a/README.md b/README.md index 126eb2b..d4f5077 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [![Crate](https://img.shields.io/crates/v/prometheus_wireguard_exporter.svg)](https://crates.io/crates/prometheus_wireguard_exporter) [![cratedown](https://img.shields.io/crates/d/prometheus_wireguard_exporter.svg)](https://crates.io/crates/prometheus_wireguard_exporter) [![cratelastdown](https://img.shields.io/crates/dv/prometheus_wireguard_exporter.svg)](https://crates.io/crates/prometheus_wireguard_exporter) -[![release](https://img.shields.io/github/release/MindFlavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/3.3.0) -[![tag](https://img.shields.io/github/tag/mindflavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/3.3.0) +[![release](https://img.shields.io/github/release/MindFlavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/3.3.1) +[![tag](https://img.shields.io/github/tag/mindflavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/3.3.1) [![Build Status](https://travis-ci.org/MindFlavor/prometheus_wireguard_exporter.svg?branch=master)](https://travis-ci.org/MindFlavor/prometheus_wireguard_exporter) -[![commitssince](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/3.3.0.svg)](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/3.3.0.svg) +[![commitssince](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/3.3.1.svg)](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/3.3.1.svg) ## Intro @@ -18,15 +18,15 @@ A Prometheus exporter for [WireGuard](https://www.wireguard.com), written in Rus ## Changelog +* From release [3.3.1](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/3.3.1) the exporter accepts multiple interfaces in the command line options. Just pass the `-i` parameter multiple times. Note the not specifying the interface is equivalent to specifying every one of them (the exporter will pass the `all` parameter to `wg show` command). * **BREAKING** Starting from release [3.3.0](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/3.3.0) the exporter allows you to specify a different interface from the file name. Previously if you specified the file name (the `-n` flag) the program would infer the interface name from the file name. Now the two items are decoupled: you need to specify the file name (with `-n`) and the interface name (with `-i`) separately. Thank you [Vincent Debergue](https://github.com/vdebergue) for helping with this (see issue [#22](https://github.com/MindFlavor/prometheus_wireguard_exporter/issues/22)). Upgrading from [3.2.4](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/3.2.4): Please note that the `-n` flag no longer infer automatically the interface name from the file name. We now have the `-i` parameter for that. In order to keep the previous behaviour (if you use the `-n` flag) please add the `-i` flag to the command line arguments as well. For example, if you had `prometheus_wireguard_exporter -n /etc/wireguard/wg0.conf` you must specify `prometheus_wireguard_exporter -n /etc/wireguard/wg0.conf -i wg0` to keep the same behaviour. -* Starting from release [2.0.2](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/2.0.2) this exporter supports IPv6 addresses too (thanks to [Maximilian Bosch](https://github.com/Ma27)'s PR [#5](https://github.com/MindFlavor/prometheus_wireguard_exporter/pull/5)). * From release [3.0.0](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/3.0.0) the exporter allows two label modes: one is to dump every allowed ip in a single label (called `allowed_ips`) along with their subnets. The second one is to create a pair of labels for each allowed ip/subnet pair (called `allowed_ip_0`/`allowed_subnet_0`, `allowed_ip_1`/`allowed_subnet_1` and so on for every allowed ip). The default if the single label mode but you can enable the second mode by specifying the `-s` switch at startup. Thank you [Toon Schoenmakers](https://github.com/schoentoon) for this solution (see issue [#8](https://github.com/MindFlavor/prometheus_wireguard_exporter/issues/8)). - +* Starting from release [2.0.2](https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/tag/2.0.2) this exporter supports IPv6 addresses too (thanks to [Maximilian Bosch](https://github.com/Ma27)'s PR [#5](https://github.com/MindFlavor/prometheus_wireguard_exporter/pull/5)). ## Prerequisites -* You need [Rust](https://www.rust-lang.org/) to compile this code. Simply follow the instructions on Rust's website to install the toolchain. If you get weird errors while compiling please try and update your Rust version first (I have developed it on `rustc 1.35.0-nightly (8159f389f 2019-04-06)`). -* You need [WireGuard](https://www.wireguard.com) *and* the `wg` CLI in the path. The tool will call `wg show all dump` and of course will fail if the `wg` executable is not found. If you want I can add the option of specifying the `wg` path in the command line, just open an issue for it. +* You need [Rust](https://www.rust-lang.org/) to compile this code. Simply follow the instructions on Rust's website to install the toolchain. If you get weird errors while compiling please try and update your Rust version first (I have developed it on `rustc 1.42.0 (b8cedc004 2020-03-09)`). +* You need [WireGuard](https://www.wireguard.com) *and* the `wg` CLI in the path. The tool will call `wg show |all dump` and of course will fail if the `wg` executable is not found. If you want I can add the option of specifying the `wg` path in the command line, just open an issue for it. ## Compilation @@ -48,15 +48,15 @@ cargo install prometheus_wireguard_exporter Start the binary with `-h` to get the complete syntax. The parameters are: -| Parameter | Mandatory | Valid values | Default | Description | -| -- | -- | -- | -- | -- | -| `-v` | no | | | Enable verbose mode. -| `-l` | no | any valid ip address | 0.0.0.0 | Specify the service address. This is the address your Prometheus instance should point to. -| `-p` | no | any valid port number | 9586 | Specify the service port. This is the port your Prometheus instance should point to. -| `-n` | no | path to the wireguard configuration file | | This flag adds the *friendly_name* attribute to the exported entries. See [Friendly names](#friendly-names) for more details. -| `-s` | no | | off | Enable the allowed ip + subnet split mode for the labels. -| `-r` | no | | off | Exports peer's remote ip and port as labels (if available). -| `-i` | no | your interface name | `all` | Specifies the interface passed to the `wg show dump` parameter. +| Parameter | Mandatory | Valid values | Default | Accepts multiple occurrences? | Description | +| -- | -- | -- | -- | -- | -- | +| `-v` | no | | | No | Enable verbose mode. +| `-l` | no | any valid ip address | 0.0.0.0 | No | Specify the service address. This is the address your Prometheus instance should point to. +| `-p` | no | any valid port number | 9586 | No | Specify the service port. This is the port your Prometheus instance should point to. +| `-n` | no | path to the wireguard configuration file | | No | This flag adds the *friendly_name* attribute to the exported entries. See [Friendly names](#friendly-names) for more details. +| `-s` | no | | off | No | Enable the allowed ip + subnet split mode for the labels. +| `-r` | no | | off | No | Exports peer's remote ip and port as labels (if available). +| `-i` | no | your interface name(s) | `all` | Yes | Specifies the interface(s) passed to the `wg show dump` parameter. Multiple parameters are allowed. Once started, the tool will listen on the specified port (or the default one, 9586, if not specified) and return a Prometheus valid response at the url `/metrics`. So to check if the tool is working properly simply browse the `http://localhost:9586/metrics` (or whichever port you choose). @@ -198,7 +198,7 @@ After=network-online.target User=root Group=root Type=simple -ExecStart=/usr/local/bin/prometheus_wireguard_exporter -n /etc/wireguard/wg0.conf -i wg0 +ExecStart=/usr/local/bin/prometheus_wireguard_exporter -n /etc/wireguard/peers.conf -i wg0 -i wg1 [Install] WantedBy=multi-user.target diff --git a/src/main.rs b/src/main.rs index d5c35aa..8892caa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,81 +20,83 @@ use prometheus_exporter_base::render_prometheus; use std::net::IpAddr; use std::sync::Arc; -fn wg_with_text( - wg_config_str: &str, - wg_output_stdout_str: &str, - options: Arc, -) -> Result { - let pehm = peer_entry_hashmap_try_from(wg_config_str)?; - trace!("pehm == {:?}", pehm); - - let wg = WireGuard::try_from(wg_output_stdout_str)?; - Ok(wg.render_with_names( - Some(&pehm), - options.separate_allowed_ips, - options.export_remote_ip_and_port, - )) -} - async fn perform_request( _req: Request, options: Arc, ) -> Result { - trace!("perform_request"); - debug!("options == {:?}", options); - - let interface_str = match &options.interface { - Some(interface_str) => interface_str, - None => "all", - } - .to_owned(); - - debug!("using inteface_str {}", interface_str); - - let output = Command::new("wg") - .arg("show") - .arg(&interface_str) - .arg("dump") - .output()?; - let output_stdout_str = String::from_utf8(output.stdout)?; - trace!( - "wg show {} dump stdout == {}", - interface_str, - output_stdout_str - ); - let output_stderr_str = String::from_utf8(output.stderr)?; - trace!( - "wg show {} dump stderr == {}", - interface_str, - 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_str != "all" { - debug!("injecting {} to the wg show output", interface_str); - let mut result = String::new(); - for s in output_stdout_str.lines() { - result.push_str(&format!("{}\t{}\n", interface_str, s)); - } - result - } else { - output_stdout_str + let interfaces_to_handle = match &options.interfaces { + Some(interfaces_str) => interfaces_str.clone(), + None => vec!["all".to_owned()], }; - if let Some(extract_names_config_file) = &options.extract_names_config_file { - let wg_config_string = ::std::fs::read_to_string(extract_names_config_file)?; - wg_with_text(&wg_config_string as &str, &output_stdout_str, options) + let peer_entry_contents = + if let Some(extract_names_config_file) = &options.extract_names_config_file { + Some(::std::fs::read_to_string( + &extract_names_config_file as &str, + )?) + } else { + None + }; + + let peer_entry_hashmap = if let Some(peer_entry_contents) = &peer_entry_contents { + Some(peer_entry_hashmap_try_from(peer_entry_contents)?) } else { - let wg = WireGuard::try_from(&output_stdout_str as &str)?; - Ok(wg.render_with_names( - None, + None + }; + + let mut wg_accumulator: Option = None; + + for interface_to_handle in interfaces_to_handle { + let output = 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(), options.separate_allowed_ips, options.export_remote_ip_and_port, )) + } else { + panic!(); } } @@ -136,14 +138,18 @@ async fn main() { .takes_value(false), ) .arg( - Arg::with_name("extract_names_config_file") + Arg::with_name("extract_names_config_files") .short("n") .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(false) + .number_of_values(1) .takes_value(true)) .arg( - Arg::with_name("interface") + Arg::with_name("interfaces") .short("i") - .help("If set specifies the interface passed to the wg show command. In not specified, all will be passed.") + .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) + .number_of_values(1) .takes_value(true)) .get_matches(); diff --git a/src/options.rs b/src/options.rs index fa8ccc8..5e9f4ab 100644 --- a/src/options.rs +++ b/src/options.rs @@ -3,21 +3,30 @@ pub(crate) struct Options { pub verbose: bool, pub separate_allowed_ips: bool, pub extract_names_config_file: Option, - pub interface: Option, + pub interfaces: Option>, pub export_remote_ip_and_port: bool, } impl Options { pub fn from_claps(matches: &clap::ArgMatches<'_>) -> Options { - Options { + let options = Options { verbose: matches.is_present("verbose"), separate_allowed_ips: matches.is_present("separate_allowed_ips"), extract_names_config_file: matches - .value_of("extract_names_config_file") + .value_of("extract_names_config_files") .map(|e| e.to_owned()), - interface: matches.value_of("interface").map(|e| e.to_owned()), + interfaces: matches.values_of("interfaces").map(|e| { + e.into_iter() + .map(|a| { + println!("a ==> {}", a); + a.to_owned() + }) + .collect() + }), export_remote_ip_and_port: matches.is_present("export_remote_ip_and_port"), - } + }; + + options } } diff --git a/src/wireguard.rs b/src/wireguard.rs index e5ab435..0316bee 100644 --- a/src/wireguard.rs +++ b/src/wireguard.rs @@ -126,6 +126,18 @@ impl TryFrom<&str> for WireGuard { } impl WireGuard { + pub fn merge(&mut self, merge_from: &WireGuard) { + for (interface_name, endpoints_to_merge) in merge_from.interfaces.iter() { + if let Some(endpoints) = self.interfaces.get_mut(&interface_name as &str) { + endpoints.extend_from_slice(&endpoints_to_merge); + } else { + let mut new_vec = Vec::new(); + new_vec.extend_from_slice(&endpoints_to_merge); + self.interfaces.insert(interface_name.to_owned(), new_vec); + } + } + } + pub(crate) fn render_with_names( &self, pehm: Option<&PeerEntryHashMap>, @@ -165,7 +177,21 @@ impl WireGuard { let mut s_latest_handshake = Vec::new(); s_latest_handshake.push(pc_latest_handshake.render_header()); - for (interface, endpoints) in self.interfaces.iter() { + // Here we make sure we process the interfaces in the + // lexicographical order. + // This is not stricly necessary but it ensures + // a consistent output between executions (the iter() function + // of HashMap does not guarantee any ordering). + // Prometheus does not care about ordering but humans do so + // we'll sort it beforehand. Being references the cost + // should be negligible anyway. + let mut interfaces_sorted: Vec<(&String, &Vec)> = self + .interfaces + .iter() + .collect::)>>(); + interfaces_sorted.sort_by(|a, b| a.0.partial_cmp(b.0).unwrap()); + + for (interface, endpoints) in interfaces_sorted.into_iter() { for endpoint in endpoints { // only show remote endpoints if let Endpoint::Remote(ep) = endpoint {