diff options
Diffstat (limited to '')
l--------- | src/go/plugin/go.d/modules/wireguard/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/charts.go | 152 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/collect.go | 109 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/config_schema.json | 25 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/integrations/wireguard.md | 204 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/metadata.yaml | 121 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/testdata/config.json | 3 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/testdata/config.yaml | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/wireguard.go | 106 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/wireguard/wireguard_test.go | 509 |
10 files changed, 1231 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/wireguard/README.md b/src/go/plugin/go.d/modules/wireguard/README.md new file mode 120000 index 000000000..389e494d7 --- /dev/null +++ b/src/go/plugin/go.d/modules/wireguard/README.md @@ -0,0 +1 @@ +integrations/wireguard.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/wireguard/charts.go b/src/go/plugin/go.d/modules/wireguard/charts.go new file mode 100644 index 000000000..c2defa9b3 --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/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/plugin/go.d/modules/wireguard/collect.go b/src/go/plugin/go.d/modules/wireguard/collect.go new file mode 100644 index 000000000..cbcc180ec --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/wireguard/config_schema.json b/src/go/plugin/go.d/modules/wireguard/config_schema.json new file mode 100644 index 000000000..5ff8ff717 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/wireguard/integrations/wireguard.md b/src/go/plugin/go.d/modules/wireguard/integrations/wireguard.md new file mode 100644 index 000000000..2460cc839 --- /dev/null +++ b/src/go/plugin/go.d/modules/wireguard/integrations/wireguard.md @@ -0,0 +1,204 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/wireguard/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/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 + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +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 + ``` + +### Getting Logs + +If you're encountering problems with the `wireguard` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep wireguard +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep wireguard /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep wireguard +``` + + diff --git a/src/go/plugin/go.d/modules/wireguard/metadata.yaml b/src/go/plugin/go.d/modules/wireguard/metadata.yaml new file mode 100644 index 000000000..0ac680d58 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/wireguard/testdata/config.json b/src/go/plugin/go.d/modules/wireguard/testdata/config.json new file mode 100644 index 000000000..0e3f7c403 --- /dev/null +++ b/src/go/plugin/go.d/modules/wireguard/testdata/config.json @@ -0,0 +1,3 @@ +{ + "update_every": 123 +} diff --git a/src/go/plugin/go.d/modules/wireguard/testdata/config.yaml b/src/go/plugin/go.d/modules/wireguard/testdata/config.yaml new file mode 100644 index 000000000..f21a3a7a0 --- /dev/null +++ b/src/go/plugin/go.d/modules/wireguard/testdata/config.yaml @@ -0,0 +1 @@ +update_every: 123 diff --git a/src/go/plugin/go.d/modules/wireguard/wireguard.go b/src/go/plugin/go.d/modules/wireguard/wireguard.go new file mode 100644 index 000000000..fdd42e193 --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/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/plugin/go.d/modules/wireguard/wireguard_test.go b/src/go/plugin/go.d/modules/wireguard/wireguard_test.go new file mode 100644 index 000000000..c9d27cbd0 --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/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 +} |