Multiple interfaces from command line support (#31)

This commit is contained in:
Francesco Cogno 2020-05-09 16:48:52 +02:00 committed by GitHub
parent 2013f8b5af
commit 52740036bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 93 deletions

2
Cargo.lock generated
View file

@ -685,7 +685,7 @@ dependencies = [
[[package]] [[package]]
name = "prometheus_wireguard_exporter" name = "prometheus_wireguard_exporter"
version = "3.3.0" version = "3.3.1"
dependencies = [ dependencies = [
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "prometheus_wireguard_exporter" name = "prometheus_wireguard_exporter"
version = "3.3.0" version = "3.3.1"
authors = ["Francesco Cogno <francesco.cogno@outlook.com>"] authors = ["Francesco Cogno <francesco.cogno@outlook.com>"]
description = "Prometheus WireGuard Exporter" description = "Prometheus WireGuard Exporter"
edition = "2018" edition = "2018"

View file

@ -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) [![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) [![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.0) [![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) [![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 ## Intro
@ -18,15 +18,15 @@ A Prometheus exporter for [WireGuard](https://www.wireguard.com), written in Rus
## Changelog ## 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. * **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)). * 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 ## 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 [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. * You need [WireGuard](https://www.wireguard.com) *and* the `wg` CLI in the path. The tool will call `wg show <interface(s)>|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 ## Compilation
@ -48,15 +48,15 @@ cargo install prometheus_wireguard_exporter
Start the binary with `-h` to get the complete syntax. The parameters are: Start the binary with `-h` to get the complete syntax. The parameters are:
| Parameter | Mandatory | Valid values | Default | Description | | Parameter | Mandatory | Valid values | Default | Accepts multiple occurrences? | Description |
| -- | -- | -- | -- | -- | | -- | -- | -- | -- | -- | -- |
| `-v` | no | <switch> | | Enable verbose mode. | `-v` | no | <switch> | | 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. | `-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 | Specify the service port. This is the port 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 | | This flag adds the *friendly_name* attribute to the exported entries. See [Friendly names](#friendly-names) for more details. | `-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 | <switch> | off | Enable the allowed ip + subnet split mode for the labels. | `-s` | no | <switch> | off | No | Enable the allowed ip + subnet split mode for the labels.
| `-r` | no | <switch> | off | Exports peer's remote ip and port as labels (if available). | `-r` | no | <switch> | off | No | 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 <interface> dump` parameter. | `-i` | no | your interface name(s) | `all` | Yes | Specifies the interface(s) passed to the `wg show <interface> 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). 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 User=root
Group=root Group=root
Type=simple 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] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -20,81 +20,83 @@ use prometheus_exporter_base::render_prometheus;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
fn wg_with_text(
wg_config_str: &str,
wg_output_stdout_str: &str,
options: Arc<Options>,
) -> Result<String, failure::Error> {
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( async fn perform_request(
_req: Request<Body>, _req: Request<Body>,
options: Arc<Options>, options: Arc<Options>,
) -> Result<String, failure::Error> { ) -> Result<String, failure::Error> {
trace!("perform_request"); let interfaces_to_handle = match &options.interfaces {
debug!("options == {:?}", options); Some(interfaces_str) => interfaces_str.clone(),
None => vec!["all".to_owned()],
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
}; };
if let Some(extract_names_config_file) = &options.extract_names_config_file { let peer_entry_contents =
let wg_config_string = ::std::fs::read_to_string(extract_names_config_file)?; if let Some(extract_names_config_file) = &options.extract_names_config_file {
wg_with_text(&wg_config_string as &str, &output_stdout_str, options) 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 { } else {
let wg = WireGuard::try_from(&output_stdout_str as &str)?; None
Ok(wg.render_with_names( };
None,
let mut wg_accumulator: Option<WireGuard> = 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.separate_allowed_ips,
options.export_remote_ip_and_port, options.export_remote_ip_and_port,
)) ))
} else {
panic!();
} }
} }
@ -136,14 +138,18 @@ async fn main() {
.takes_value(false), .takes_value(false),
) )
.arg( .arg(
Arg::with_name("extract_names_config_file") Arg::with_name("extract_names_config_files")
.short("n") .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)") .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)) .takes_value(true))
.arg( .arg(
Arg::with_name("interface") Arg::with_name("interfaces")
.short("i") .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)) .takes_value(true))
.get_matches(); .get_matches();

View file

@ -3,21 +3,30 @@ pub(crate) struct Options {
pub verbose: bool, pub verbose: bool,
pub separate_allowed_ips: bool, pub separate_allowed_ips: bool,
pub extract_names_config_file: Option<String>, pub extract_names_config_file: Option<String>,
pub interface: Option<String>, pub interfaces: Option<Vec<String>>,
pub export_remote_ip_and_port: bool, pub export_remote_ip_and_port: bool,
} }
impl Options { impl Options {
pub fn from_claps(matches: &clap::ArgMatches<'_>) -> Options { pub fn from_claps(matches: &clap::ArgMatches<'_>) -> Options {
Options { let options = Options {
verbose: matches.is_present("verbose"), verbose: matches.is_present("verbose"),
separate_allowed_ips: matches.is_present("separate_allowed_ips"), separate_allowed_ips: matches.is_present("separate_allowed_ips"),
extract_names_config_file: matches extract_names_config_file: matches
.value_of("extract_names_config_file") .value_of("extract_names_config_files")
.map(|e| e.to_owned()), .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"), export_remote_ip_and_port: matches.is_present("export_remote_ip_and_port"),
} };
options
} }
} }

View file

@ -126,6 +126,18 @@ impl TryFrom<&str> for WireGuard {
} }
impl 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( pub(crate) fn render_with_names(
&self, &self,
pehm: Option<&PeerEntryHashMap>, pehm: Option<&PeerEntryHashMap>,
@ -165,7 +177,21 @@ impl WireGuard {
let mut s_latest_handshake = Vec::new(); let mut s_latest_handshake = Vec::new();
s_latest_handshake.push(pc_latest_handshake.render_header()); 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<Endpoint>)> = self
.interfaces
.iter()
.collect::<Vec<(&String, &Vec<Endpoint>)>>();
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 { for endpoint in endpoints {
// only show remote endpoints // only show remote endpoints
if let Endpoint::Remote(ep) = endpoint { if let Endpoint::Remote(ep) = endpoint {