Skip to content

Commit

Permalink
enhancement: Wireguard service collector (#5)
Browse files Browse the repository at this point in the history
This commit introduce Wireguard metrics and documentation improvements.
  • Loading branch information
vincentnonim authored Feb 29, 2024
1 parent de613b3 commit f6a240c
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 17 deletions.
57 changes: 40 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**
Expand All @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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_KEY>
OPS_API_SECRET: <OPS_API_SECRET>
ports:
- "8080:8080"
```
### Systemd
Expand All @@ -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
Expand All @@ -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:

Expand All @@ -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
```
Expand Down
6 changes: 6 additions & 0 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
89 changes: 89 additions & 0 deletions internal/collector/wireguard.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions opnsense/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
112 changes: 112 additions & 0 deletions opnsense/wireguard.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit f6a240c

Please sign in to comment.