summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/wireguard
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/wireguard/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/charts.go152
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/collect.go109
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/config_schema.json25
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/integrations/wireguard.md169
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/metadata.yaml121
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.json3
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.yaml1
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/wireguard.go106
-rw-r--r--src/go/collectors/go.d.plugin/modules/wireguard/wireguard_test.go509
10 files changed, 1196 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/README.md b/src/go/collectors/go.d.plugin/modules/wireguard/README.md
new file mode 120000
index 000000000..389e494d7
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/README.md
@@ -0,0 +1 @@
+integrations/wireguard.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/charts.go b/src/go/collectors/go.d.plugin/modules/wireguard/charts.go
new file mode 100644
index 000000000..fe7f89e9b
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/charts.go
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package wireguard
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioDeviceNetworkIO = module.Priority + iota
+ prioDevicePeers
+ prioPeerNetworkIO
+ prioPeerLatestHandShake
+)
+
+var (
+ deviceChartsTmpl = module.Charts{
+ deviceNetworkIOChartTmpl.Copy(),
+ devicePeersChartTmpl.Copy(),
+ }
+
+ deviceNetworkIOChartTmpl = module.Chart{
+ ID: "device_%s_network_io",
+ Title: "Device traffic",
+ Units: "B/s",
+ Fam: "device traffic",
+ Ctx: "wireguard.device_network_io",
+ Type: module.Area,
+ Priority: prioDeviceNetworkIO,
+ Dims: module.Dims{
+ {ID: "device_%s_receive", Name: "receive", Algo: module.Incremental},
+ {ID: "device_%s_transmit", Name: "transmit", Algo: module.Incremental, Mul: -1},
+ },
+ }
+ devicePeersChartTmpl = module.Chart{
+ ID: "device_%s_peers",
+ Title: "Device peers",
+ Units: "peers",
+ Fam: "device peers",
+ Ctx: "wireguard.device_peers",
+ Priority: prioDevicePeers,
+ Dims: module.Dims{
+ {ID: "device_%s_peers", Name: "peers"},
+ },
+ }
+)
+
+var (
+ peerChartsTmpl = module.Charts{
+ peerNetworkIOChartTmpl.Copy(),
+ peerLatestHandShakeChartTmpl.Copy(),
+ }
+
+ peerNetworkIOChartTmpl = module.Chart{
+ ID: "peer_%s_network_io",
+ Title: "Peer traffic",
+ Units: "B/s",
+ Fam: "peer traffic",
+ Ctx: "wireguard.peer_network_io",
+ Type: module.Area,
+ Priority: prioPeerNetworkIO,
+ Dims: module.Dims{
+ {ID: "peer_%s_receive", Name: "receive", Algo: module.Incremental},
+ {ID: "peer_%s_transmit", Name: "transmit", Algo: module.Incremental, Mul: -1},
+ },
+ }
+ peerLatestHandShakeChartTmpl = module.Chart{
+ ID: "peer_%s_latest_handshake_ago",
+ Title: "Peer time elapsed since the latest handshake",
+ Units: "seconds",
+ Fam: "peer latest handshake",
+ Ctx: "wireguard.peer_latest_handshake_ago",
+ Priority: prioPeerLatestHandShake,
+ Dims: module.Dims{
+ {ID: "peer_%s_latest_handshake_ago", Name: "time"},
+ },
+ }
+)
+
+func newDeviceCharts(device string) *module.Charts {
+ charts := deviceChartsTmpl.Copy()
+
+ for _, c := range *charts {
+ c.ID = fmt.Sprintf(c.ID, device)
+ c.Labels = []module.Label{
+ {Key: "device", Value: device},
+ }
+ for _, d := range c.Dims {
+ d.ID = fmt.Sprintf(d.ID, device)
+ }
+ }
+
+ return charts
+}
+
+func (w *WireGuard) addNewDeviceCharts(device string) {
+ charts := newDeviceCharts(device)
+
+ if err := w.Charts().Add(*charts...); err != nil {
+ w.Warning(err)
+ }
+}
+
+func (w *WireGuard) removeDeviceCharts(device string) {
+ prefix := fmt.Sprintf("device_%s", device)
+
+ for _, c := range *w.Charts() {
+ if strings.HasPrefix(c.ID, prefix) {
+ c.MarkRemove()
+ c.MarkNotCreated()
+ }
+ }
+}
+
+func newPeerCharts(id, device, pubKey string) *module.Charts {
+ charts := peerChartsTmpl.Copy()
+
+ for _, c := range *charts {
+ c.ID = fmt.Sprintf(c.ID, id)
+ c.Labels = []module.Label{
+ {Key: "device", Value: device},
+ {Key: "public_key", Value: pubKey},
+ }
+ for _, d := range c.Dims {
+ d.ID = fmt.Sprintf(d.ID, id)
+ }
+ }
+
+ return charts
+}
+
+func (w *WireGuard) addNewPeerCharts(id, device, pubKey string) {
+ charts := newPeerCharts(id, device, pubKey)
+
+ if err := w.Charts().Add(*charts...); err != nil {
+ w.Warning(err)
+ }
+}
+
+func (w *WireGuard) removePeerCharts(id string) {
+ prefix := fmt.Sprintf("peer_%s", id)
+
+ for _, c := range *w.Charts() {
+ if strings.HasPrefix(c.ID, prefix) {
+ c.MarkRemove()
+ c.MarkNotCreated()
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/collect.go b/src/go/collectors/go.d.plugin/modules/wireguard/collect.go
new file mode 100644
index 000000000..cbcc180ec
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/collect.go
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package wireguard
+
+import (
+ "fmt"
+ "time"
+
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+func (w *WireGuard) collect() (map[string]int64, error) {
+ if w.client == nil {
+ client, err := w.newWGClient()
+ if err != nil {
+ return nil, fmt.Errorf("creating WireGuard client: %v", err)
+ }
+ w.client = client
+ }
+
+ // TODO: probably we need to get a list of interfaces and query interfaces using client.Device()
+ // https://github.com/WireGuard/wgctrl-go/blob/3d4a969bb56bb6931f6661af606bc9c4195b4249/internal/wglinux/client_linux.go#L79-L80
+ devices, err := w.client.Devices()
+ if err != nil {
+ return nil, fmt.Errorf("retrieving WireGuard devices: %v", err)
+ }
+
+ if len(devices) == 0 {
+ w.Info("no WireGuard devices found on the host system")
+ }
+
+ now := time.Now()
+ if w.cleanupLastTime.IsZero() {
+ w.cleanupLastTime = now
+ }
+
+ mx := make(map[string]int64)
+
+ w.collectDevicesPeers(mx, devices, now)
+
+ if now.Sub(w.cleanupLastTime) > w.cleanupEvery {
+ w.cleanupLastTime = now
+ w.cleanupDevicesPeers(devices)
+ }
+
+ return mx, nil
+}
+
+func (w *WireGuard) collectDevicesPeers(mx map[string]int64, devices []*wgtypes.Device, now time.Time) {
+ for _, d := range devices {
+ if !w.devices[d.Name] {
+ w.devices[d.Name] = true
+ w.addNewDeviceCharts(d.Name)
+ }
+
+ mx["device_"+d.Name+"_peers"] = int64(len(d.Peers))
+ if len(d.Peers) == 0 {
+ mx["device_"+d.Name+"_receive"] = 0
+ mx["device_"+d.Name+"_transmit"] = 0
+ continue
+ }
+
+ for _, p := range d.Peers {
+ if p.LastHandshakeTime.IsZero() {
+ continue
+ }
+
+ pubKey := p.PublicKey.String()
+ id := peerID(d.Name, pubKey)
+
+ if !w.peers[id] {
+ w.peers[id] = true
+ w.addNewPeerCharts(id, d.Name, pubKey)
+ }
+
+ mx["device_"+d.Name+"_receive"] += p.ReceiveBytes
+ mx["device_"+d.Name+"_transmit"] += p.TransmitBytes
+ mx["peer_"+id+"_receive"] = p.ReceiveBytes
+ mx["peer_"+id+"_transmit"] = p.TransmitBytes
+ mx["peer_"+id+"_latest_handshake_ago"] = int64(now.Sub(p.LastHandshakeTime).Seconds())
+ }
+ }
+}
+
+func (w *WireGuard) cleanupDevicesPeers(devices []*wgtypes.Device) {
+ seenDevices, seenPeers := make(map[string]bool), make(map[string]bool)
+ for _, d := range devices {
+ seenDevices[d.Name] = true
+ for _, p := range d.Peers {
+ seenPeers[peerID(d.Name, p.PublicKey.String())] = true
+ }
+ }
+ for d := range w.devices {
+ if !seenDevices[d] {
+ delete(w.devices, d)
+ w.removeDeviceCharts(d)
+ }
+ }
+ for p := range w.peers {
+ if !seenPeers[p] {
+ delete(w.peers, p)
+ w.removePeerCharts(p)
+ }
+ }
+}
+
+func peerID(device, peerPublicKey string) string {
+ return device + "_" + peerPublicKey
+}
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/config_schema.json b/src/go/collectors/go.d.plugin/modules/wireguard/config_schema.json
new file mode 100644
index 000000000..5ff8ff717
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/config_schema.json
@@ -0,0 +1,25 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "WireGuard collector configuration.",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/integrations/wireguard.md b/src/go/collectors/go.d.plugin/modules/wireguard/integrations/wireguard.md
new file mode 100644
index 000000000..11ff605b6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/integrations/wireguard.md
@@ -0,0 +1,169 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/wireguard/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/wireguard/metadata.yaml"
+sidebar_label: "WireGuard"
+learn_status: "Published"
+learn_rel_path: "Collecting Metrics/VPNs"
+most_popular: False
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
+endmeta-->
+
+# WireGuard
+
+
+<img src="https://netdata.cloud/img/wireguard.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: wireguard
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This collector monitors WireGuard VPN devices and peers traffic.
+
+
+It connects to the local WireGuard instance using [wireguard-go client](https://github.com/WireGuard/wireguard-go).
+
+
+This collector is supported on all platforms.
+
+This collector supports collecting metrics from multiple instances of this integration, including remote instances.
+
+This collector requires the CAP_NET_ADMIN capability, but it is set automatically during installation, so no manual configuration is needed.
+
+
+### Default Behavior
+
+#### Auto-Detection
+
+It automatically detects instances running on localhost.
+
+
+#### Limits
+
+Doesn't work if Netdata or WireGuard is installed in the container.
+
+
+#### Performance Impact
+
+The default configuration for this integration is not expected to impose a significant performance impact on the system.
+
+
+## Metrics
+
+Metrics grouped by *scope*.
+
+The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
+
+
+
+### Per device
+
+These metrics refer to the VPN network interface.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| device | VPN network interface |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| wireguard.device_network_io | receive, transmit | B/s |
+| wireguard.device_peers | peers | peers |
+
+### Per peer
+
+These metrics refer to the VPN peer.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| device | VPN network interface |
+| public_key | Public key of a peer |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| wireguard.peer_network_io | receive, transmit | B/s |
+| wireguard.peer_latest_handshake_ago | time | seconds |
+
+
+
+## Alerts
+
+There are no alerts configured by default for this integration.
+
+
+## Setup
+
+### Prerequisites
+
+No action required.
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/wireguard.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/wireguard.conf
+```
+#### Options
+
+The following options can be defined globally: update_every, autodetection_retry.
+
+
+<details open><summary>Config options</summary>
+
+| Name | Description | Default | Required |
+|:----|:-----------|:-------|:--------:|
+| update_every | Data collection frequency. | 1 | no |
+| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no |
+
+</details>
+
+#### Examples
+There are no configuration examples.
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+To troubleshoot issues with the `wireguard` collector, run the `go.d.plugin` with the debug option enabled. The output
+should give you clues as to why the collector isn't working.
+
+- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
+ your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
+
+ ```bash
+ cd /usr/libexec/netdata/plugins.d/
+ ```
+
+- Switch to the `netdata` user.
+
+ ```bash
+ sudo -u netdata -s
+ ```
+
+- Run the `go.d.plugin` to debug the collector:
+
+ ```bash
+ ./go.d.plugin -d -m wireguard
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/metadata.yaml b/src/go/collectors/go.d.plugin/modules/wireguard/metadata.yaml
new file mode 100644
index 000000000..0ac680d58
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/metadata.yaml
@@ -0,0 +1,121 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-wireguard
+ plugin_name: go.d.plugin
+ module_name: wireguard
+ monitored_instance:
+ name: WireGuard
+ link: https://www.wireguard.com/
+ categories:
+ - data-collection.vpns
+ icon_filename: wireguard.svg
+ keywords:
+ - wireguard
+ - vpn
+ - security
+ most_popular: false
+ info_provided_to_referring_integrations:
+ description: ""
+ related_resources:
+ integrations:
+ list: []
+ overview:
+ data_collection:
+ metrics_description: |
+ This collector monitors WireGuard VPN devices and peers traffic.
+ method_description: |
+ It connects to the local WireGuard instance using [wireguard-go client](https://github.com/WireGuard/wireguard-go).
+ default_behavior:
+ auto_detection:
+ description: |
+ It automatically detects instances running on localhost.
+ limits:
+ description: |
+ Doesn't work if Netdata or WireGuard is installed in the container.
+ performance_impact:
+ description: ""
+ additional_permissions:
+ description: |
+ This collector requires the CAP_NET_ADMIN capability, but it is set automatically during installation, so no manual configuration is needed.
+ multi_instance: true
+ supported_platforms:
+ include: []
+ exclude: []
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/wireguard.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every, autodetection_retry.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 1
+ required: false
+ - name: autodetection_retry
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
+ default_value: 0
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list: []
+ troubleshooting:
+ problems:
+ list: []
+ alerts: []
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: device
+ description: These metrics refer to the VPN network interface.
+ labels:
+ - name: device
+ description: VPN network interface
+ metrics:
+ - name: wireguard.device_network_io
+ description: Device traffic
+ unit: B/s
+ chart_type: area
+ dimensions:
+ - name: receive
+ - name: transmit
+ - name: wireguard.device_peers
+ description: Device peers
+ unit: peers
+ chart_type: line
+ dimensions:
+ - name: peers
+ - name: peer
+ description: These metrics refer to the VPN peer.
+ labels:
+ - name: device
+ description: VPN network interface
+ - name: public_key
+ description: Public key of a peer
+ metrics:
+ - name: wireguard.peer_network_io
+ description: Peer traffic
+ unit: B/s
+ chart_type: area
+ dimensions:
+ - name: receive
+ - name: transmit
+ - name: wireguard.peer_latest_handshake_ago
+ description: Peer time elapsed since the latest handshake
+ unit: seconds
+ chart_type: line
+ dimensions:
+ - name: time
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.json b/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.json
new file mode 100644
index 000000000..0e3f7c403
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.json
@@ -0,0 +1,3 @@
+{
+ "update_every": 123
+}
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.yaml
new file mode 100644
index 000000000..f21a3a7a0
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/testdata/config.yaml
@@ -0,0 +1 @@
+update_every: 123
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/wireguard.go b/src/go/collectors/go.d.plugin/modules/wireguard/wireguard.go
new file mode 100644
index 000000000..59d3a5bc9
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/wireguard.go
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package wireguard
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "golang.zx2c4.com/wireguard/wgctrl"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("wireguard", module.Creator{
+ JobConfigSchema: configSchema,
+ Create: func() module.Module { return New() },
+ Config: func() any { return &Config{} },
+ })
+}
+
+func New() *WireGuard {
+ return &WireGuard{
+ newWGClient: func() (wgClient, error) { return wgctrl.New() },
+ charts: &module.Charts{},
+ devices: make(map[string]bool),
+ peers: make(map[string]bool),
+ cleanupEvery: time.Minute,
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
+}
+
+type (
+ WireGuard struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ client wgClient
+ newWGClient func() (wgClient, error)
+
+ cleanupLastTime time.Time
+ cleanupEvery time.Duration
+ devices map[string]bool
+ peers map[string]bool
+ }
+ wgClient interface {
+ Devices() ([]*wgtypes.Device, error)
+ Close() error
+ }
+)
+
+func (w *WireGuard) Configuration() any {
+ return w.Config
+}
+
+func (w *WireGuard) Init() error {
+ return nil
+}
+
+func (w *WireGuard) Check() error {
+ mx, err := w.collect()
+ if err != nil {
+ w.Error(err)
+ return err
+ }
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+ return nil
+}
+
+func (w *WireGuard) Charts() *module.Charts {
+ return w.charts
+}
+
+func (w *WireGuard) Collect() map[string]int64 {
+ mx, err := w.collect()
+ if err != nil {
+ w.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+ return mx
+}
+
+func (w *WireGuard) Cleanup() {
+ if w.client == nil {
+ return
+ }
+ if err := w.client.Close(); err != nil {
+ w.Warningf("cleanup: error on closing connection: %v", err)
+ }
+ w.client = nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/wireguard/wireguard_test.go b/src/go/collectors/go.d.plugin/modules/wireguard/wireguard_test.go
new file mode 100644
index 000000000..6f13d3375
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/wireguard/wireguard_test.go
@@ -0,0 +1,509 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package wireguard
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+ } {
+ assert.NotNil(t, data, name)
+ }
+}
+
+func TestWireGuard_ConfigurationSerialize(t *testing.T) {
+ module.TestConfigurationSerialize(t, &WireGuard{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestWireGuard_Init(t *testing.T) {
+ assert.NoError(t, New().Init())
+}
+
+func TestWireGuard_Charts(t *testing.T) {
+ assert.Len(t, *New().Charts(), 0)
+
+}
+
+func TestWireGuard_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func(w *WireGuard)
+ wantClose bool
+ }{
+ "after New": {
+ wantClose: false,
+ prepare: func(w *WireGuard) {},
+ },
+ "after Init": {
+ wantClose: false,
+ prepare: func(w *WireGuard) { _ = w.Init() },
+ },
+ "after Check": {
+ wantClose: true,
+ prepare: func(w *WireGuard) { _ = w.Init(); _ = w.Check() },
+ },
+ "after Collect": {
+ wantClose: true,
+ prepare: func(w *WireGuard) { _ = w.Init(); _ = w.Collect() },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ w := New()
+ m := &mockClient{}
+ w.newWGClient = func() (wgClient, error) { return m, nil }
+
+ test.prepare(w)
+
+ require.NotPanics(t, w.Cleanup)
+
+ if test.wantClose {
+ assert.True(t, m.closeCalled)
+ } else {
+ assert.False(t, m.closeCalled)
+ }
+ })
+ }
+}
+
+func TestWireGuard_Check(t *testing.T) {
+ tests := map[string]struct {
+ wantFail bool
+ prepare func(w *WireGuard)
+ }{
+ "success when devices and peers found": {
+ wantFail: false,
+ prepare: func(w *WireGuard) {
+ m := &mockClient{}
+ d1 := prepareDevice(1)
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ d1.Peers = append(d1.Peers, preparePeer("12"))
+ m.devices = append(m.devices, d1)
+ w.client = m
+ },
+ },
+ "success when devices and no peers found": {
+ wantFail: false,
+ prepare: func(w *WireGuard) {
+ m := &mockClient{}
+ m.devices = append(m.devices, prepareDevice(1))
+ w.client = m
+ },
+ },
+ "fail when no devices and no peers found": {
+ wantFail: true,
+ prepare: func(w *WireGuard) {
+ w.client = &mockClient{}
+ },
+ },
+ "fail when error on retrieving devices": {
+ wantFail: true,
+ prepare: func(w *WireGuard) {
+ w.client = &mockClient{errOnDevices: true}
+ },
+ },
+ "fail when error on creating client": {
+ wantFail: true,
+ prepare: func(w *WireGuard) {
+ w.newWGClient = func() (wgClient, error) { return nil, errors.New("mock.newWGClient() error") }
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ w := New()
+ require.NoError(t, w.Init())
+ test.prepare(w)
+
+ if test.wantFail {
+ assert.Error(t, w.Check())
+ } else {
+ assert.NoError(t, w.Check())
+ }
+ })
+ }
+}
+
+func TestWireGuard_Collect(t *testing.T) {
+ type testCaseStep struct {
+ prepareMock func(m *mockClient)
+ check func(t *testing.T, w *WireGuard)
+ }
+ tests := map[string][]testCaseStep{
+ "several devices no peers": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(1))
+ m.devices = append(m.devices, prepareDevice(2))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ mx := w.Collect()
+
+ expected := map[string]int64{
+ "device_wg1_peers": 0,
+ "device_wg1_receive": 0,
+ "device_wg1_transmit": 0,
+ "device_wg2_peers": 0,
+ "device_wg2_receive": 0,
+ "device_wg2_transmit": 0,
+ }
+
+ copyLatestHandshake(mx, expected)
+ assert.Equal(t, expected, mx)
+ assert.Equal(t, len(deviceChartsTmpl)*2, len(*w.Charts()))
+ },
+ },
+ },
+ "several devices several peers each": {
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := prepareDevice(1)
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ d1.Peers = append(d1.Peers, preparePeer("12"))
+ m.devices = append(m.devices, d1)
+
+ d2 := prepareDevice(2)
+ d2.Peers = append(d2.Peers, preparePeer("21"))
+ d2.Peers = append(d2.Peers, preparePeer("22"))
+ m.devices = append(m.devices, d2)
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ mx := w.Collect()
+
+ expected := map[string]int64{
+ "device_wg1_peers": 2,
+ "device_wg1_receive": 0,
+ "device_wg1_transmit": 0,
+ "device_wg2_peers": 2,
+ "device_wg2_receive": 0,
+ "device_wg2_transmit": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ "peer_wg2_cGVlcjIxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg2_cGVlcjIxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg2_cGVlcjIxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ "peer_wg2_cGVlcjIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg2_cGVlcjIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg2_cGVlcjIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ }
+
+ copyLatestHandshake(mx, expected)
+ assert.Equal(t, expected, mx)
+ assert.Equal(t, len(deviceChartsTmpl)*2+len(peerChartsTmpl)*4, len(*w.Charts()))
+ },
+ },
+ },
+ "peers without last handshake time": {
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := prepareDevice(1)
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ d1.Peers = append(d1.Peers, preparePeer("12"))
+ d1.Peers = append(d1.Peers, prepareNoLastHandshakePeer("13"))
+ d1.Peers = append(d1.Peers, prepareNoLastHandshakePeer("14"))
+ m.devices = append(m.devices, d1)
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ mx := w.Collect()
+
+ expected := map[string]int64{
+ "device_wg1_peers": 4,
+ "device_wg1_receive": 0,
+ "device_wg1_transmit": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg1_cGVlcjEyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ }
+
+ copyLatestHandshake(mx, expected)
+ assert.Equal(t, expected, mx)
+ assert.Equal(t, len(deviceChartsTmpl)+len(peerChartsTmpl)*2, len(*w.Charts()))
+ },
+ },
+ },
+ "device added at runtime": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(1))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*1, len(*w.Charts()))
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(2))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ mx := w.Collect()
+
+ expected := map[string]int64{
+ "device_wg1_peers": 0,
+ "device_wg1_receive": 0,
+ "device_wg1_transmit": 0,
+ "device_wg2_peers": 0,
+ "device_wg2_receive": 0,
+ "device_wg2_transmit": 0,
+ }
+ copyLatestHandshake(mx, expected)
+ assert.Equal(t, expected, mx)
+ assert.Equal(t, len(deviceChartsTmpl)*2, len(*w.Charts()))
+
+ },
+ },
+ },
+ "device removed at run time, no cleanup occurred": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(1))
+ m.devices = append(m.devices, prepareDevice(2))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = m.devices[:len(m.devices)-1]
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*2, len(*w.Charts()))
+ assert.Equal(t, 0, calcObsoleteCharts(w.Charts()))
+ },
+ },
+ },
+ "device removed at run time, cleanup occurred": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(1))
+ m.devices = append(m.devices, prepareDevice(2))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = m.devices[:len(m.devices)-1]
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ w.cleanupEvery = time.Second
+ time.Sleep(time.Second)
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*2, len(*w.Charts()))
+ assert.Equal(t, len(deviceChartsTmpl)*1, calcObsoleteCharts(w.Charts()))
+ },
+ },
+ },
+ "peer added at runtime": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.devices = append(m.devices, prepareDevice(1))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*1, len(*w.Charts()))
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := m.devices[0]
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ mx := w.Collect()
+
+ expected := map[string]int64{
+ "device_wg1_peers": 1,
+ "device_wg1_receive": 0,
+ "device_wg1_transmit": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_latest_handshake_ago": 60,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_receive": 0,
+ "peer_wg1_cGVlcjExAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=_transmit": 0,
+ }
+ copyLatestHandshake(mx, expected)
+ assert.Equal(t, expected, mx)
+ assert.Equal(t, len(deviceChartsTmpl)*1+len(peerChartsTmpl)*1, len(*w.Charts()))
+ },
+ },
+ },
+ "peer removed at run time, no cleanup occurred": {
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := prepareDevice(1)
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ d1.Peers = append(d1.Peers, preparePeer("12"))
+ m.devices = append(m.devices, d1)
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := m.devices[0]
+ d1.Peers = d1.Peers[:len(d1.Peers)-1]
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*1+len(peerChartsTmpl)*2, len(*w.Charts()))
+ assert.Equal(t, 0, calcObsoleteCharts(w.Charts()))
+ },
+ },
+ },
+ "peer removed at run time, cleanup occurred": {
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := prepareDevice(1)
+ d1.Peers = append(d1.Peers, preparePeer("11"))
+ d1.Peers = append(d1.Peers, preparePeer("12"))
+ m.devices = append(m.devices, d1)
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ _ = w.Collect()
+ },
+ },
+ {
+ prepareMock: func(m *mockClient) {
+ d1 := m.devices[0]
+ d1.Peers = d1.Peers[:len(d1.Peers)-1]
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ w.cleanupEvery = time.Second
+ time.Sleep(time.Second)
+ _ = w.Collect()
+ assert.Equal(t, len(deviceChartsTmpl)*1+len(peerChartsTmpl)*2, len(*w.Charts()))
+ assert.Equal(t, len(peerChartsTmpl)*1, calcObsoleteCharts(w.Charts()))
+ },
+ },
+ },
+ "fails if no devices found": {
+ {
+ prepareMock: func(m *mockClient) {},
+ check: func(t *testing.T, w *WireGuard) {
+ assert.Equal(t, map[string]int64(nil), w.Collect())
+ },
+ },
+ },
+ "fails if error on getting devices list": {
+ {
+ prepareMock: func(m *mockClient) {
+ m.errOnDevices = true
+ },
+ check: func(t *testing.T, w *WireGuard) {
+ assert.Equal(t, map[string]int64(nil), w.Collect())
+ },
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ w := New()
+ require.NoError(t, w.Init())
+ m := &mockClient{}
+ w.client = m
+
+ for i, step := range test {
+ t.Run(fmt.Sprintf("step[%d]", i), func(t *testing.T) {
+ step.prepareMock(m)
+ step.check(t, w)
+ })
+ }
+ })
+ }
+}
+
+type mockClient struct {
+ devices []*wgtypes.Device
+ errOnDevices bool
+ closeCalled bool
+}
+
+func (m *mockClient) Devices() ([]*wgtypes.Device, error) {
+ if m.errOnDevices {
+ return nil, errors.New("mock.Devices() error")
+ }
+ return m.devices, nil
+}
+
+func (m *mockClient) Close() error {
+ m.closeCalled = true
+ return nil
+}
+
+func prepareDevice(num uint8) *wgtypes.Device {
+ return &wgtypes.Device{
+ Name: fmt.Sprintf("wg%d", num),
+ }
+}
+
+func preparePeer(s string) wgtypes.Peer {
+ b := make([]byte, 32)
+ b = append(b[:0], fmt.Sprintf("peer%s", s)...)
+ k, _ := wgtypes.NewKey(b[:32])
+
+ return wgtypes.Peer{
+ PublicKey: k,
+ LastHandshakeTime: time.Now().Add(-time.Minute),
+ ReceiveBytes: 0,
+ TransmitBytes: 0,
+ }
+}
+
+func prepareNoLastHandshakePeer(s string) wgtypes.Peer {
+ p := preparePeer(s)
+ var lh time.Time
+ p.LastHandshakeTime = lh
+ return p
+}
+
+func copyLatestHandshake(dst, src map[string]int64) {
+ for k, v := range src {
+ if strings.HasSuffix(k, "latest_handshake_ago") {
+ if _, ok := dst[k]; ok {
+ dst[k] = v
+ }
+ }
+ }
+}
+
+func calcObsoleteCharts(charts *module.Charts) int {
+ var num int
+ for _, c := range *charts {
+ if c.Obsolete {
+ num++
+ }
+ }
+ return num
+}