From 7be9884aeccaf94c0833284528fa155c1afa616a Mon Sep 17 00:00:00 2001 From: Vadim Babadzhanyan Date: Fri, 23 Aug 2024 13:10:20 +0300 Subject: [PATCH] Feat Prometheus metrics (#1299) * Russian translation * Add Prometheus metrics [Feat]: Simple Stats API #1285 * Revert "Add Prometheus metrics" This reverts commit a998f6be8a0c54a5daffe70a0fc3d8b9ed53a960. * Add Prometheus metrics [Feat]: Simple Stats API #1285 * Fix short link. Generate One Time Link (#1301) Co-authored-by: Vadim Babadzhanyan * fix one time links (#1304) Closes #1302 Co-authored-by: Bernd Storath <999999bst@gmail.com> * fixup: issue templates due to labels reorg Signed-off-by: Philip H <47042125+pheiduck@users.noreply.github.com> * Separate port for prometheus metrics Add Prometheus metrics [Feat]: Simple Stats API #1285 * Separate port for prometheus metrics Add Prometheus metrics [Feat]: Simple Stats API #1285 * Fix port in Readme Separate port for prometheus metrics Add Prometheus metrics [Feat]: Simple Stats API #1285 * Add Prometheus port in Service Separate port for prometheus metrics Add Prometheus metrics [Feat]: Simple Stats API #1285 * Revert "Add Prometheus port in Service" This reverts commit a7376abcf1fe2b729ab05ba0d49977ab5a2642ea. * Revert "Fix port in Readme" This reverts commit 9760bde2f2dc4428b0bf0b27d91bbded1c2ad05d. * Revert "Separate port for prometheus metrics" This reverts commit 58f5b6806e20c7704ff04247f384d30c2845a34e. * Revert "Separate port for prometheus metrics" This reverts commit 6d246ea4bda265f8b8b9e99acb336aeb26c9fa17. * Add Prometheus metrics with Basic Auth [Feat]: Simple Stats API #1285 * Disable by default [Feat]: Simple Stats API #1285 * [Feat]: Simple Stats API #1285 * Update README.md --------- Co-authored-by: Vadim Babadzhanyan Co-authored-by: Bernd Storath Co-authored-by: Philip H <47042125+pheiduck@users.noreply.github.com> --- README.md | 5 +++ docker-compose.yml | 2 ++ src/config.js | 2 ++ src/lib/Server.js | 60 +++++++++++++++++++++++++++++++---- src/lib/WireGuard.js | 73 +++++++++++++++++++++++++++++++++++++++++++ src/package-lock.json | 19 +++++++++++ src/package.json | 1 + 7 files changed, 156 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e58f7fc..37b158a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host! * Traffic Stats (default off) * One Time Links (default off) * Client Expiry (default off) +* Prometheus metrics support ## Requirements @@ -88,6 +89,8 @@ To automatically install & run wg-easy, simply run: The Web UI will now be available on `http://0.0.0.0:51821`. +The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) + > 💡 Your configuration files will be saved in `~/.wg-easy` WireGuard Easy can be launched with Docker Compose as well - just download @@ -127,6 +130,8 @@ These options can be configured by setting environment variables using `-e KEY=" | `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) | | `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. | | `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name | +| `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`| +| `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. | > If you change `WG_PORT`, make sure to also change the exposed port. diff --git a/docker-compose.yml b/docker-compose.yml index 71c155a..095557b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,8 @@ services: # - WG_ENABLE_ONE_TIME_LINKS=true # - UI_ENABLE_SORT_CLIENTS=true # - WG_ENABLE_EXPIRES_TIME=true + # - ENABLE_PROMETHEUS_METRICS=false + # - PROMETHEUS_METRICS_PASSWORD=$$2a$$12$$vkvKpeEAHD78gasyawIod.1leBMKg8sBwKW.pQyNsq78bXV3INf2G # (needs double $$, hash of 'prometheus_password'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash) image: ghcr.io/wg-easy/wg-easy container_name: wg-easy diff --git a/src/config.js b/src/config.js index ba461bf..01f0ce2 100644 --- a/src/config.js +++ b/src/config.js @@ -41,3 +41,5 @@ module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false'; module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false'; module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false'; +module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'false'; +module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD; diff --git a/src/lib/Server.js b/src/lib/Server.js index 8e5617c..17e5058 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -2,6 +2,7 @@ const bcrypt = require('bcryptjs'); const crypto = require('node:crypto'); +const basicAuth = require('basic-auth'); const { createServer } = require('node:http'); const { stat, readFile } = require('node:fs/promises'); const { resolve, sep } = require('node:path'); @@ -36,9 +37,12 @@ const { WG_ENABLE_ONE_TIME_LINKS, UI_ENABLE_SORT_CLIENTS, WG_ENABLE_EXPIRES_TIME, + ENABLE_PROMETHEUS_METRICS, + PROMETHEUS_METRICS_PASSWORD, } = require('../config'); const requiresPassword = !!PASSWORD_HASH; +const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD; /** * Checks if `password` matches the PASSWORD_HASH. @@ -48,13 +52,12 @@ const requiresPassword = !!PASSWORD_HASH; * @param {string} password String to test * @returns {boolean} true if matching environment, otherwise false */ -const isPasswordValid = (password) => { +const isPasswordValid = (password, hash) => { if (typeof password !== 'string') { return false; } - - if (PASSWORD_HASH) { - return bcrypt.compareSync(password, PASSWORD_HASH); + if (hash) { + return bcrypt.compareSync(password, hash); } return false; @@ -162,7 +165,7 @@ module.exports = class Server { }); } - if (!isPasswordValid(password)) { + if (!isPasswordValid(password, PASSWORD_HASH)) { throw createError({ status: 401, message: 'Incorrect Password', @@ -192,7 +195,7 @@ module.exports = class Server { } if (req.url.startsWith('/api/') && req.headers['authorization']) { - if (isPasswordValid(req.headers['authorization'])) { + if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) { return next(); } return res.status(401).json({ @@ -332,6 +335,51 @@ module.exports = class Server { }); }; + // Prometheus Metrics API + const routerPrometheusMetrics = createRouter(); + app.use(routerPrometheusMetrics); + + // Check Prometheus credentials + app.use( + fromNodeMiddleware((req, res, next) => { + if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) { + return next(); + } + const user = basicAuth(req); + if (requiresPrometheusPassword && !user) { + res.statusCode = 401; + return { error: 'Not Logged In' }; + } + + if (user.pass) { + if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) { + return next(); + } + res.statusCode = 401; + return { error: 'Incorrect Password' }; + } + res.statusCode = 401; + return { error: 'Not Logged In' }; + }), + ); + + // Prometheus Routes + routerPrometheusMetrics + .get('/metrics', defineEventHandler(async (event) => { + setHeader(event, 'Content-Type', 'text/plain'); + if (ENABLE_PROMETHEUS_METRICS === 'true') { + return WireGuard.getMetrics(); + } + return ''; + })) + .get('/metrics/json', defineEventHandler(async (event) => { + setHeader(event, 'Content-Type', 'application/json'); + if (ENABLE_PROMETHEUS_METRICS === 'true') { + return WireGuard.getMetricsJSON(); + } + return ''; + })); + // backup_restore const router3 = createRouter(); app.use(router3); diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js index b07a7af..1aaed9a 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -160,6 +160,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' latestHandshakeAt: null, transferRx: null, transferTx: null, + endpoint: null, })); // Loop WireGuard status @@ -188,6 +189,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' client.latestHandshakeAt = latestHandshakeAt === '0' ? null : new Date(Number(`${latestHandshakeAt}000`)); + client.endpoint = endpoint === '(none)' ? null : endpoint; client.transferRx = Number(transferRx); client.transferTx = Number(transferTx); client.persistentKeepalive = persistentKeepalive; @@ -430,4 +432,75 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; } } + async getMetrics() { + const clients = await this.getClients(); + let wireguardPeerCount = 0; + let wireguardEnabledPeersCount = 0; + let wireguardConnectedPeersCount = 0; + let wireguardSentBytes = ''; + let wireguardReceivedBytes = ''; + let wireguardLatestHandshakeSeconds = ''; + for (const client of Object.values(clients)) { + wireguardPeerCount++; + if (client.enabled === true) { + wireguardEnabledPeersCount++; + } + if (client.endpoint !== null) { + wireguardConnectedPeersCount++; + } + wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferTx)}\n`; + wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferRx)}\n`; + wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`; + } + + let returnText = '# HELP wg-easy and wireguard metrics\n'; + + returnText += '\n# HELP wireguard_configured_peers\n'; + returnText += '# TYPE wireguard_configured_peers gauge\n'; + returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`; + + returnText += '\n# HELP wireguard_enabled_peers\n'; + returnText += '# TYPE wireguard_enabled_peers gauge\n'; + returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`; + + returnText += '\n# HELP wireguard_connected_peers\n'; + returnText += '# TYPE wireguard_connected_peers gauge\n'; + returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`; + + returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n'; + returnText += '# TYPE wireguard_sent_bytes counter\n'; + returnText += `${wireguardSentBytes}`; + + returnText += '\n# HELP wireguard_received_bytes Bytes received from the peer\n'; + returnText += '# TYPE wireguard_received_bytes counter\n'; + returnText += `${wireguardReceivedBytes}`; + + returnText += '\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n'; + returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n'; + returnText += `${wireguardLatestHandshakeSeconds}`; + + return returnText; + } + + async getMetricsJSON() { + const clients = await this.getClients(); + let wireguardPeerCount = 0; + let wireguardEnabledPeersCount = 0; + let wireguardConnectedPeersCount = 0; + for (const client of Object.values(clients)) { + wireguardPeerCount++; + if (client.enabled === true) { + wireguardEnabledPeersCount++; + } + if (client.endpoint !== null) { + wireguardConnectedPeersCount++; + } + } + return { + wireguard_configured_peers: Number(wireguardPeerCount), + wireguard_enabled_peers: Number(wireguardEnabledPeersCount), + wireguard_connected_peers: Number(wireguardConnectedPeersCount), + }; + } + }; diff --git a/src/package-lock.json b/src/package-lock.json index b5413fb..06683c9 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.1", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6", @@ -992,6 +993,24 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", diff --git a/src/package.json b/src/package.json index b0032c2..007f08c 100644 --- a/src/package.json +++ b/src/package.json @@ -15,6 +15,7 @@ "author": "Emile Nijssen", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6",