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",