Initial commit

This commit is contained in:
Francesco Cogno 2019-04-23 23:06:35 +02:00
commit 143a450778
No known key found for this signature in database
GPG key ID: 8D28AE01B9AB2BB2
12 changed files with 1657 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
**/*.rs.bk
*.vim

1105
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

28
Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "prometheus_wireguard_exporter"
version = "0.1.0"
authors = ["Francesco Cogno <francesco.cogno@outlook.com>"]
description = "Prometheus WireGuard Exporter"
edition = "2018"
readme = "README.md"
license = "MIT"
repository = "https://github.com/MindFlavor/prometheus_wireguard_exporter"
documentation = "https://github.com/MindFlavor/prometheus_wireguard_exporter"
homepage = "https://github.com/MindFlavor/prometheus_wireguard_exporter"
keywords = ["prometheus", "exporter", "wireguard"]
categories = ["database"]
[dependencies]
log = "0.4.6"
env_logger = "0.6.1"
futures = "0.1.26"
clap = "2.33.0"
serde_json = "1.0.39"
serde = "1.0.90"
serde_derive = "1.0.90"
failure = "0.1.5"
hyper = "0.12.27"
http = "0.1.17"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Francesco Cogno
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
README.md Normal file
View file

@ -0,0 +1,67 @@
# Prometheus WireGuard Exporter
[![legal](https://img.shields.io/github/license/mindflavor/prometheus_wireguard_exporter.svg)](LICENSE)
[![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)
[![tag](https://img.shields.io/github/tag/mindflavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/0.1.0)
[![release](https://img.shields.io/github/release/MindFlavor/prometheus_wireguard_exporter.svg)](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/0.1.0)
[![commitssince](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/0.1.0.svg)](https://img.shields.io/github/commits-since/mindflavor/prometheus_wireguard_exporter/0.1.0.svg)
## Intro
A Prometheus exporter for [WireGuard](https://www.wireguard.com), written in Rust. This tool exports the `wg show all dump` results in a format that [Prometheus](https://prometheus.io/) can understand. The exporter is very light on your server resources, both in terms of memory and CPU usage.
![](extra/00.png)
## 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.
## Compilation
To compile the latest master version:
```bash
git clone https://github.com/MindFlavor/prometheus_wireguard_exporter.git
cd prometheus_wireguard_exporter
cargo install --path .
```
If you want the latest release you can simply use:
```bash
cargo install prometheus_wireguard_exporter
```
## Usage
Start the binary with `-h` to get the complete syntax. The parameters are:
| Parameter | Mandatory | Valid values | Default | Description |
| -- | -- | -- | -- | -- |
| `-v` | no | <switch> | | Enable verbose mode.
| `-p` | no | any valid port number | 9576 | Specify the service port. This is the port your Prometheus instance should point to.
Once started, the tool will listen on the specified port (or the default one, 9576, 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:9576` (or whichever port you choose).
### Systemd service file
Now add the exporter to the Prometheus exporters as usual. I recommend to start it as a service. It's necessary to run it as root (if there is a non-root way to call `wg show all dump` please let me know). My systemd service file is like this one:
```
[Unit]
Description=Prometheus WireGuard Exporter
Wants=network-online.target
After=network-online.target
[Service]
User=root
Group=root
Type=simple
ExecStart=/usr/local/bin/prometheus_wireguard_exporter
[Install]
WantedBy=multi-user.target
```

4
example.json Normal file
View file

@ -0,0 +1,4 @@
[
{ "path": "/home/mindflavor", "recursive": true },
{ "path": "/home/mindflavor/.cargo", "recursive": false }
]

BIN
extra/00.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

69
src/exporter_error.rs Normal file
View file

@ -0,0 +1,69 @@
#[derive(Debug, Fail)]
pub(crate) enum ExporterError {
#[allow(dead_code)]
#[fail(display = "Generic error")]
Generic {},
#[fail(display = "Hyper error: {}", e)]
Hyper { e: hyper::error::Error },
#[fail(display = "http error: {}", e)]
Http { e: http::Error },
#[fail(display = "UTF-8 error: {}", e)]
UTF8 { e: std::string::FromUtf8Error },
#[fail(display = "JSON format error: {}", e)]
JSON { e: serde_json::error::Error },
#[fail(display = "IO Error: {}", e)]
IO { e: std::io::Error },
#[fail(display = "UTF8 conversion error: {}", e)]
Utf8 { e: std::str::Utf8Error },
#[fail(display = "int conversion error: {}", e)]
ParseInt { e: std::num::ParseIntError },
}
impl From<std::io::Error> for ExporterError {
fn from(e: std::io::Error) -> Self {
ExporterError::IO { e }
}
}
impl From<hyper::error::Error> for ExporterError {
fn from(e: hyper::error::Error) -> Self {
ExporterError::Hyper { e }
}
}
impl From<http::Error> for ExporterError {
fn from(e: http::Error) -> Self {
ExporterError::Http { e }
}
}
impl From<std::string::FromUtf8Error> for ExporterError {
fn from(e: std::string::FromUtf8Error) -> Self {
ExporterError::UTF8 { e }
}
}
impl From<serde_json::error::Error> for ExporterError {
fn from(e: serde_json::error::Error) -> Self {
ExporterError::JSON { e }
}
}
impl From<std::str::Utf8Error> for ExporterError {
fn from(e: std::str::Utf8Error) -> Self {
ExporterError::Utf8 { e }
}
}
impl From<std::num::ParseIntError> for ExporterError {
fn from(e: std::num::ParseIntError) -> Self {
ExporterError::ParseInt { e }
}
}

134
src/main.rs Normal file
View file

@ -0,0 +1,134 @@
extern crate serde_json;
#[macro_use]
extern crate failure;
use clap;
use clap::Arg;
use futures::future::{done, ok, Either, Future};
use http::StatusCode;
use hyper::service::service_fn;
use hyper::{Body, Request, Response, Server};
use log::{error, info, trace};
use std::env;
mod options;
use options::Options;
mod exporter_error;
use exporter_error::ExporterError;
mod render_to_prometheus;
use render_to_prometheus::RenderToPrometheus;
mod wireguard;
use std::convert::TryFrom;
use std::process::Command;
use std::string::String;
use wireguard::WireGuard;
fn check_compliance(req: &Request<Body>) -> Result<(), Response<Body>> {
if req.uri() != "/metrics" {
trace!("uri not allowed {}", req.uri());
Err(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(hyper::Body::empty())
.unwrap())
} else if req.method() != "GET" {
trace!("method not allowed {}", req.method());
Err(Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.body(hyper::Body::empty())
.unwrap())
} else {
Ok(())
}
}
fn handle_request(
req: Request<Body>,
options: Options,
) -> impl Future<Item = Response<Body>, Error = failure::Error> {
trace!("{:?}", req);
done(check_compliance(&req)).then(move |res| match res {
Ok(_) => Either::A(perform_request(req, &options).then(|res| match res {
Ok(body) => ok(body),
Err(err) => {
error!("internal server error: {:?}", err);
ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(hyper::Body::empty())
.unwrap())
}
})),
Err(err) => Either::B(ok(err)),
})
}
fn perform_request(
_req: Request<Body>,
_options: &Options,
) -> impl Future<Item = Response<Body>, Error = ExporterError> {
trace!("perform_request");
done(
Command::new("wg")
.arg("show")
.arg("all")
.arg("dump")
.output(),
)
.from_err()
.and_then(|output| {
done(String::from_utf8(output.stdout))
.from_err()
.and_then(|output_str| {
trace!("{}", output_str);
done(WireGuard::try_from(&output_str as &str))
.from_err()
.and_then(|wg| ok(Response::new(Body::from(wg.render()))))
})
})
}
fn main() {
let matches = clap::App::new("prometheus_wireguard_exporter")
.version("0.1")
.author("Francesco Cogno <francesco.cogno@outlook.com>")
.arg(
Arg::with_name("port")
.short("p")
.help("exporter port (default 9576)")
.default_value("9576")
.takes_value(true),
)
.arg(
Arg::with_name("verbose")
.short("v")
.help("verbose logging")
.takes_value(false),
)
.get_matches();
let options = Options::from_claps(&matches);
if options.verbose {
env::set_var("RUST_LOG", "prometheus_wireguard_exporter=trace");
} else {
env::set_var("RUST_LOG", "prometheus_wireguard_exporter=info");
}
env_logger::init();
info!("using options: {:?}", options);
let bind = matches.value_of("port").unwrap();
let bind = u16::from_str_radix(&bind, 10).expect("port must be a valid number");
let addr = ([0, 0, 0, 0], bind).into();
info!("starting exporter on {}", addr);
let new_svc = move || {
let options = options.clone();
service_fn(move |req| handle_request(req, options.clone()))
};
let server = Server::bind(&addr)
.serve(new_svc)
.map_err(|e| eprintln!("server error: {}", e));
hyper::rt::run(server);
}

12
src/options.rs Normal file
View file

@ -0,0 +1,12 @@
#[derive(Debug, Clone)]
pub(crate) struct Options {
pub verbose: bool,
}
impl Options {
pub fn from_claps(matches: &clap::ArgMatches<'_>) -> Options {
Options {
verbose: matches.is_present("verbose"),
}
}
}

View file

@ -0,0 +1,3 @@
pub trait RenderToPrometheus {
fn render(&self) -> String;
}

211
src/wireguard.rs Normal file
View file

@ -0,0 +1,211 @@
use crate::exporter_error::ExporterError;
use crate::render_to_prometheus::RenderToPrometheus;
use log::{debug, trace};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const EMPTY: &str = "(none)";
#[derive(Default, Debug, Clone)]
pub(crate) struct LocalEndpoint {
pub public_key: String,
pub private_key: String,
pub local_port: u32,
pub persistent_keepalive: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct RemoteEndpoint {
pub public_key: String,
pub remote_ip: Option<String>,
pub remote_port: Option<u32>,
pub local_ip: String,
pub local_subnet: String,
pub latest_handshake: SystemTime,
pub sent_bytes: u128,
pub received_bytes: u128,
pub persistent_keepalive: bool,
}
#[derive(Debug, Clone)]
pub(crate) enum Endpoint {
Local(LocalEndpoint),
Remote(RemoteEndpoint),
}
fn to_option_string(s: &str) -> Option<String> {
if s == EMPTY {
None
} else {
Some(s.to_owned())
}
}
fn to_bool(s: &str) -> bool {
s != "off"
}
#[derive(Debug, Clone)]
pub(crate) struct WireGuard {
pub interfaces: HashMap<String, Vec<Endpoint>>,
}
impl TryFrom<&str> for WireGuard {
type Error = ExporterError;
fn try_from(input: &str) -> Result<Self, Self::Error> {
debug!("wireguard::try_from({}) called", input);
let mut wg = WireGuard {
interfaces: HashMap::new(),
};
for line in input.lines() {
let v: Vec<&str> = line.split('\t').filter(|s| !s.is_empty()).collect();
debug!("v == {:?}", v);
let endpoint = if v.len() == 5 {
// this is the local interface
Endpoint::Local(LocalEndpoint {
public_key: v[1].to_owned(),
private_key: v[2].to_owned(),
local_port: v[3].parse::<u32>().unwrap(),
persistent_keepalive: to_bool(v[4]),
})
} else {
// remote endpoint
let public_key = v[1].to_owned();
let (remote_ip, remote_port) = if let Some(ip_and_port) = to_option_string(v[3]) {
let toks: Vec<&str> = ip_and_port.split(':').collect();
(
Some(toks[0].to_owned()),
Some(toks[1].parse::<u32>().unwrap()),
)
} else {
(None, None)
};
let tok: Vec<&str> = v[4].split('/').collect();
let (local_ip, local_subnet) = (tok[0].to_owned(), tok[1].to_owned());
// the latest_handhshake is based on Linux representation: a tick is a second. So
// the hack here is: add N seconds to the UNIX_EPOCH constant. This wil not work
// on other platforms if the returned ticks are *not* seconds. Sadly I did not find
// an alternative way to initialize a SystemTime from a tick number.
Endpoint::Remote(RemoteEndpoint {
public_key,
remote_ip,
remote_port,
local_ip,
local_subnet,
latest_handshake: UNIX_EPOCH
.checked_add(Duration::from_secs(v[5].parse::<u64>()?))
.unwrap(),
sent_bytes: v[6].parse::<u128>().unwrap(),
received_bytes: v[7].parse::<u128>().unwrap(),
persistent_keepalive: to_bool(v[8]),
})
};
trace!("{:?}", endpoint);
if let Some(endpoints) = wg.interfaces.get_mut(v[0]) {
endpoints.push(endpoint);
} else {
let mut new_vec = Vec::new();
new_vec.push(endpoint);
wg.interfaces.insert(v[0].to_owned(), new_vec);
}
}
trace!("{:?}", wg);
Ok(wg)
}
}
impl RenderToPrometheus for WireGuard {
fn render(&self) -> String {
let mut latest_handshakes = Vec::new();
let mut sent_bytes = Vec::new();
let mut received_bytes = Vec::new();
for (interface, endpoints) in self.interfaces.iter() {
for endpoint in endpoints {
// only show remote endpoints
if let Endpoint::Remote(ep) = endpoint {
debug!("{:?}", ep);
sent_bytes.push(format!("wireguard_sent_bytes{{inteface=\"{}\", public_key=\"{}\", local_ip=\"{}\", local_subnet=\"{}\"}} {}\n", interface, ep.public_key, ep.local_ip, ep.local_subnet, ep.sent_bytes));
received_bytes.push(format!("wireguard_received_bytes{{inteface=\"{}\", public_key=\"{}\", local_ip=\"{}\", local_subnet=\"{}\"}} {}\n", interface, ep.public_key, ep.local_ip, ep.local_subnet, ep.received_bytes));
latest_handshakes.push(format!("wireguard_latest_handshake_seconds{{inteface=\"{}\", public_key=\"{}\", local_ip=\"{}\", local_subnet=\"{}\"}} {}\n", interface, ep.public_key, ep.local_ip, ep.local_subnet, ::std::time::SystemTime::now().duration_since(ep.latest_handshake).unwrap().as_secs()));
}
}
}
let mut s = String::new();
s.push_str(
"# HELP wireguard_sent_bytes Bytes sent to the peer
# TYPE wireguard_sent_bytes counter\n",
);
for peer in sent_bytes {
s.push_str(&peer);
}
s.push_str(
"# HELP wireguard_received_bytes Bytes received from the peer
# TYPE wireguard_received_bytes counter\n",
);
for peer in received_bytes {
s.push_str(&peer);
}
s.push_str(
"# HELP wireguard_latest_handshake_seconds Seconds from the last handshake
# TYPE wireguard_latest_handshake_seconds gauge\n",
);
for peer in latest_handshakes {
s.push_str(&peer);
}
debug!("{}", s);
s
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEXT : &'static str = "wg0\t000q4qAC0ExW/BuGSmVR1nxH9JAXT6g9Wd3oEGy5lA=\t0000u8LWR682knVm350lnuqlCJzw5SNLW9Nf96P+m8=\t51820\toff
wg0\t2S7mA0vEMethCNQrJpJKE81/JmhgtB+tHHLYQhgM6kk=\t(none)\t37.159.76.245:29159\t10.70.0.2/32\t1555771458\t10288508\t139524160\toff
wg0\tqnoxQoQI8KKMupLnSSureORV0wMmH7JryZNsmGVISzU=\t(none)\t(none)\t10.70.0.3/32\t0\t0\t0\toff
wg0\tL2UoJZN7RmEKsMmqaJgKG0m1S2Zs2wd2ptAf+kb3008=\t(none)\t(none)\t10.70.0.4/32\t0\t0\t0\toff
wg0\tMdVOIPKt9K2MPj/sO2NlWQbOnFJ6L/qX80mmhQwsUlA=\t(none)\t(none)\t10.70.0.50/32\t0\t0\t0\toff
wg2\tMdVOIPKt9K2MPj/sO2NlWQbOnFJcL/qX80mmhQwsUlA=\t(none)\t(none)\t10.70.5.50/32\t0\t0\t0\toff
pollo\tYdVOIPKt9K2MPsO2NlWQbOnFJcL/qX80mmhQwsUlA=\t(none)\t(none)\t10.70.70.50/32\t0\t0\t0\toff
wg0\t928vO9Lf4+Mo84cWu4k1oRyzf0AR7FTGoPKHGoTMSHk=\t(none)\t5.90.62.106:21741\t10.70.0.80/32\t1555344925\t283012\t6604620\toff
";
#[test]
fn test_parse() {
let a = WireGuard::try_from(TEXT).unwrap();
println!("{:?}", a);
assert!(a.interfaces.len() == 3);
assert!(a.interfaces["wg0"].len() == 6);
let e1 = match &a.interfaces["wg0"][1] {
Endpoint::Local(_) => panic!(),
Endpoint::Remote(re) => re,
};
assert!(e1.public_key == "2S7mA0vEMethCNQrJpJKE81/JmhgtB+tHHLYQhgM6kk=");
}
#[test]
fn test_parse_and_serialize() {
let a = WireGuard::try_from(TEXT).unwrap();
let s = a.render();
println!("{}", s);
}
}