From f6a240c4d0fa3b4c218aa3c634ee348346556219 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 29 Feb 2024 22:38:52 +0100 Subject: [PATCH] enhancement: Wireguard service collector (#5) This commit introduce Wireguard metrics and documentation improvements. --- README.md | 57 +++++++++++----- internal/collector/collector.go | 6 ++ internal/collector/wireguard.go | 89 +++++++++++++++++++++++++ main.go | 8 +++ opnsense/client.go | 1 + opnsense/wireguard.go | 112 ++++++++++++++++++++++++++++++++ 6 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 internal/collector/wireguard.go create mode 100644 opnsense/wireguard.go diff --git a/README.md b/README.md index ac79350..0a7003d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The missing OPNsense exporter for Prometheus - **[About](#about)** - **[OPNsense User Permissions](#opnsense-user-permissions)** - **[Development](#development)** -- **[Usage](#usage)** +- **[Usage](#usage)** - **[Docker](#docker)** - **[Docker Compose](#docker-compose)** - **[Systemd](#systemd)** @@ -23,7 +23,7 @@ The missing OPNsense exporter for Prometheus - **[SSL/TLS](#ssltls)** - **[Exporters](#exporters)** - **[All Options](#all-options)** -- **[Grafana Dashboard](#grafana-dashboard)** +- **[Grafana Dashboard](#grafana-dashboard)** ## About @@ -71,7 +71,7 @@ make lint **TODO** -### Docker +### Docker The following command will start the exporter and expose the metrics on port 8080. Replace `ops.example.com`, `your-api-key`, `your-api-secret` and `instance1` with your own values. @@ -85,14 +85,34 @@ docker run -p 8080:8080 ghcr.io/athennamind/opnsense-exporter:latest \ --opnsense.api-key=your-api-key \ --opnsense.api-secret=your-api-secret \ --exporter.instance-label=instance1 \ - --web.listen-address=:8080 + --web.listen-address=:8080 ``` TODO: Add example how to add custom CA certificates to the container. ### Docker Compose -**TODO** +```yaml +version: '3' +services: + opnsense-exporter: + image: ghcr.io/athennamind/opnsense-exporter:latest + container_name: opensense-exporter + restart: always + command: + - /opnsense-exporter + - --opnsense.protocol=https + - --opnsense.address=ops.example.com + - --exporter.instance-label=instance1 + - --web.listen-address=:8080 + #- --exporter.disable-arp-table + #- --exporter.disable-cron-table + environment: + OPS_API_KEY: + OPS_API_SECRET: + ports: + - "8080:8080" +``` ### Systemd @@ -104,7 +124,7 @@ TODO: Add example how to add custom CA certificates to the container. ## Configuration -The configuration of this tool is following the standart alongside the Prometheus ecosystem. This exporter can be configured using command-line flags or environment variables. +The configuration of this tool is following the standard alongside the Prometheus ecosystem. This exporter can be configured using command-line flags or environment variables. ### OPNsense API @@ -126,10 +146,11 @@ If you want to disable TLS certificate verification, you can use the following f ### Exporters -Gathering metrics for specific subsystems can be disabled with the following following flags: +Gathering metrics for specific subsystems can be disabled with the following flags: - `--exporter.disable-arp-table` - Disable the scraping of ARP table. Defaults to `false`. - `--exporter.disable-cron-table` - Disable the scraping of Cron tasks. Defaults to `false`. +- `--exporter.disable-wireguard` - Disable the scraping of Wireguard service. Defaults to `false`. To disable the exporter metrics itself use the following flag: @@ -142,29 +163,31 @@ Flags: -h, --[no-]help Show context-sensitive help (also try --help-long and --help-man). --log.level="info" Log level. One of: [debug, info, warn, error] --log.format="logfmt" Log format. One of: [logfmt, json] - --web.telemetry-path="/metrics" + --web.telemetry-path="/metrics" Path under which to expose metrics. - --[no-]web.disable-exporter-metrics + --[no-]web.disable-exporter-metrics Exclude metrics about the exporter itself (promhttp_*, process_*, go_*). ($OPNSENSE_EXPORTER_DISABLE_EXPORTER_METRICS) --runtime.gomaxprocs=2 The target number of CPUs that the Go runtime will run on (GOMAXPROCS) ($GOMAXPROCS) - --exporter.instance-label=EXPORTER.INSTANCE-LABEL + --exporter.instance-label=EXPORTER.INSTANCE-LABEL Label to use to identify the instance in every metric. If you have multiple instances of the exporter, you can differentiate them by using different value in this flag, that represents the instance of the target OPNsense. ($OPNSENSE_EXPORTER_INSTANCE_LABEL) - --[no-]exporter.disable-arp-table + --[no-]exporter.disable-arp-table Disable the scraping of the ARP table ($OPNSENSE_EXPORTER_DISABLE_ARP_TABLE) - --[no-]exporter.disable-cron-table + --[no-]exporter.disable-cron-table Disable the scraping of the cron table ($OPNSENSE_EXPORTER_DISABLE_CRON_TABLE) - --opnsense.protocol=OPNSENSE.PROTOCOL + --[no-]exporter.disable-wireguard + Disable the scraping of Wireguard service ($OPNSENSE_EXPORTER_DISABLE_WIREGUARD) + --opnsense.protocol=OPNSENSE.PROTOCOL Protocol to use to connect to OPNsense API. One of: [http, https] ($OPNSENSE_EXPORTER_OPS_PROTOCOL) - --opnsense.address=OPNSENSE.ADDRESS + --opnsense.address=OPNSENSE.ADDRESS Hostname or IP address of OPNsense API ($OPNSENSE_EXPORTER_OPS_API) - --opnsense.api-key=OPNSENSE.API-KEY + --opnsense.api-key=OPNSENSE.API-KEY API key to use to connect to OPNsense API ($OPNSENSE_EXPORTER_OPS_API_KEY) - --opnsense.api-secret=OPNSENSE.API-SECRET + --opnsense.api-secret=OPNSENSE.API-SECRET API secret to use to connect to OPNsense API ($OPNSENSE_EXPORTER_OPS_API_SECRET) --[no-]opnsense.insecure Disable TLS certificate verification ($OPNSENSE_EXPORTER_OPS_INSECURE) --[no-]web.systemd-socket Use systemd socket activation listeners instead of port listeners (Linux only). - --web.listen-address=:8080 ... + --web.listen-address=:8080 ... Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. --web.config.file="" [EXPERIMENTAL] Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md ``` diff --git a/internal/collector/collector.go b/internal/collector/collector.go index be22f44..cee501d 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -65,6 +65,12 @@ func WithoutCronCollector() Option { return withoutCollectorInstance("cron") } +// WithoutWireguardCollector Option +// removes the wireguard collector from the list of collectors +func WithoutWireguardCollector() Option { + return withoutCollectorInstance("wireguard") +} + // New creates a new Collector instance. func New(client *opnsense.Client, log log.Logger, instanceName string, options ...Option) (*Collector, error) { diff --git a/internal/collector/wireguard.go b/internal/collector/wireguard.go new file mode 100644 index 0000000..8b2b2bc --- /dev/null +++ b/internal/collector/wireguard.go @@ -0,0 +1,89 @@ +package collector + +import ( + "github.com/AthennaMind/opnsense-exporter/opnsense" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +type WireguardCollector struct { + log log.Logger + subsystem string + instance string + instances *prometheus.Desc + TransferRx *prometheus.Desc + TransferTx *prometheus.Desc + LatestHandshake *prometheus.Desc +} + +func init() { + collectorInstances = append(collectorInstances, &WireguardCollector{ + subsystem: "wireguard", + }) +} + +func (c *WireguardCollector) Name() string { + return c.subsystem +} + +func (c *WireguardCollector) Register(namespace, instanceLabel string, log log.Logger) { + c.log = log + c.instance = instanceLabel + + level.Debug(c.log). + Log("msg", "Registering collector", "collector", c.Name()) + + c.instances = buildPrometheusDesc(c.subsystem, "interfaces_status", + "Wireguard interface (1 = up, 0 = down)", + []string{"device", "device_type", "device_name"}, + ) + + c.TransferRx = buildPrometheusDesc(c.subsystem, "peer_received_bytes_total", + "Bytes received by this wireguard peer", + []string{"device", "device_type", "device_name", "peer_name"}, + ) + + c.TransferTx = buildPrometheusDesc(c.subsystem, "peer_transmitted_bytes_total", + "Bytes transmitted by this wireguard peer", + []string{"device", "device_type", "device_name", "peer_name"}, + ) + + c.LatestHandshake = buildPrometheusDesc(c.subsystem, "peer_last_handshake_seconds", + "Last handshake by peer in seconds", + []string{"device", "device_type", "device_name", "peer_name"}, + ) +} + +func (c *WireguardCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.instances + ch <- c.LatestHandshake + ch <- c.TransferRx + ch <- c.TransferTx +} + +func (c *WireguardCollector) update(ch chan<- prometheus.Metric, desc *prometheus.Desc, valueType prometheus.ValueType, value float64, labelValues ...string) { + ch <- prometheus.MustNewConstMetric( + desc, valueType, value, labelValues..., + ) +} + +func (c *WireguardCollector) Update(client *opnsense.Client, ch chan<- prometheus.Metric) *opnsense.APICallError { + data, err := client.FetchWireguardConfig() + + if err != nil { + return err + } + + for _, instance := range data.Interfaces { + c.update(ch, c.instances, prometheus.GaugeValue, float64(instance.Status), instance.Device, instance.DeviceType, instance.DeviceName, c.instance) + } + + for _, instance := range data.Peers { + c.update(ch, c.LatestHandshake, prometheus.CounterValue, float64(instance.LatestHandshake), instance.Device, instance.DeviceType, instance.DeviceName, instance.Name, c.instance) + c.update(ch, c.TransferRx, prometheus.CounterValue, float64(instance.TransferRx), instance.Device, instance.DeviceType, instance.DeviceName, instance.Name, c.instance) + c.update(ch, c.TransferTx, prometheus.CounterValue, float64(instance.TransferTx), instance.Device, instance.DeviceType, instance.DeviceName, instance.Name, c.instance) + } + + return nil +} diff --git a/main.go b/main.go index 48770bb..e369473 100644 --- a/main.go +++ b/main.go @@ -60,6 +60,10 @@ func main() { "exporter.disable-cron-table", "Disable the scraping of the cron table", ).Envar("OPNSENSE_EXPORTER_DISABLE_CRON_TABLE").Default("false").Bool() + wireguardCollectorDisabled = kingpin.Flag( + "exporter.disable-wireguard", + "Disable the scraping of Wireguard service", + ).Envar("OPNSENSE_EXPORTER_DISABLE_WIREGUARD").Default("false").Bool() opnsenseProtocol = kingpin.Flag( "opnsense.protocol", "Protocol to use to connect to OPNsense API. One of: [http, https]", @@ -152,6 +156,10 @@ func main() { collectorOptionFuncs = append(collectorOptionFuncs, collector.WithoutCronCollector()) } + if *wireguardCollectorDisabled { + collectorOptionFuncs = append(collectorOptionFuncs, collector.WithoutWireguardCollector()) + } + collectorInstance, err := collector.New(&opnsenseClient, logger, *instanceLabel, collectorOptionFuncs...) if err != nil { diff --git a/opnsense/client.go b/opnsense/client.go index 9e47131..7b5627f 100644 --- a/opnsense/client.go +++ b/opnsense/client.go @@ -78,6 +78,7 @@ func NewClient(protocol, address, key, secret, userAgentVersion string, sslInsec "gatewaysStatus": "api/routes/gateway/status", "unboundDNSStatus": "api/unbound/diagnostics/stats", "cronJobs": "api/cron/settings/searchJobs", + "wireguardClients": "api/wireguard/service/show", }, headers: map[string]string{ "Accept": "application/json", diff --git a/opnsense/wireguard.go b/opnsense/wireguard.go new file mode 100644 index 0000000..5b5ab52 --- /dev/null +++ b/opnsense/wireguard.go @@ -0,0 +1,112 @@ +package opnsense + +import ( + "github.com/go-kit/log" + "github.com/go-kit/log/level" +) + +type wireguardClientsResponse struct { + Rows []struct { + IfId string `json:"if"` + IfType string `json:"type"` + LatestHandshake float64 `json:"latest-handshake"` + TransferRx float64 `json:"transfer-rx"` + TransferTx float64 `json:"transfer-tx"` + Status string `json:"status"` + Name string `json:"name"` + IfName string `json:"ifname"` + } `json:"rows"` + RowCount int `json:"rowCount"` + Total int `json:"total"` + Current int `json:"current"` +} + +// WGInterfaceStatus is the custom type that represents the status of a Wireguard interface +type WGInterfaceStatus int + +const ( + WGInterfaceStatusDown WGInterfaceStatus = iota + WGInterfaceStatusUp + WGInterfaceStatusUnknown +) + +type WireguardPeers struct { + Device string + DeviceName string + DeviceType string + LatestHandshake float64 + TransferRx float64 + TransferTx float64 + Name string +} + +type WireguardInterfaces struct { + Device string + DeviceType string + Status WGInterfaceStatus + Name string + DeviceName string +} + +type WireguardClients struct { + Peers []WireguardPeers + Interfaces []WireguardInterfaces +} + +// parseWGInterfaceStatus parses a string status to a WGInterfaceStatus type. +func parseWGInterfaceStatus(statusTranslated string, logger log.Logger, originalStatus string) WGInterfaceStatus { + switch statusTranslated { + case "up": + return WGInterfaceStatusUp + case "down": + return WGInterfaceStatusDown + default: + level.Warn(logger). + Log("msg", "unknown wireguard interface status detected", "status", originalStatus) + return WGInterfaceStatusUnknown + } +} + +func (c *Client) FetchWireguardConfig() (WireguardClients, *APICallError) { + var response wireguardClientsResponse + var data WireguardClients + + url, ok := c.endpoints["wireguardClients"] + if !ok { + return data, &APICallError{ + Endpoint: string(url), + Message: "Unable to fetch Wireguard stats", + StatusCode: 0, + } + } + + if err := c.do("GET", url, nil, &response); err != nil { + return data, err + } + + for _, v := range response.Rows { + + if v.IfType == "interface" { + data.Interfaces = append(data.Interfaces, WireguardInterfaces{ + Device: v.IfId, + DeviceType: v.IfType, + Status: parseWGInterfaceStatus(v.Status, c.log, v.Status), + Name: v.Name, + DeviceName: v.IfName, + }) + } + if v.IfType == "peer" { + data.Peers = append(data.Peers, WireguardPeers{ + DeviceType: v.IfType, + LatestHandshake: v.LatestHandshake, + TransferRx: v.TransferRx, + TransferTx: v.TransferTx, + Name: v.Name, + DeviceName: v.IfName, + Device: v.IfId, + }) + } + } + + return data, nil +}