add go.d/tor (#18361)
Ilya Mashchenko committed
Aug 18, 2024 at 21:05 UTC
8db74085fa7fb8be81a6fa28d8221fe72139d1da
16 files changed
+877
-1
src/collectors/python.d.plugin/python.d.conf
+1
-1
@@ -42,7 +42,6 @@ go_expvar: no
42
# smartd_log: yes
43
# spigotmc: yes
44
# traefik: yes
45
-# tor: yes
45
# varnish: yes
46
# w1sensor: yes
47
# zscores: no
@@ -80,5 +79,6 @@ riakkv: no # Removed (replaced with go.d/riak).
79
sensors: no # Removed (replaced with go.d/sensors).
80
squid: no # Removed (replaced with go.d/squid).
81
tomcat: no # Removed (replaced with go.d/tomcat)
82
+tor: no # Removed (replaced with go.d/tor).
83
puppet: no # Removed (replaced with go.d/puppet).
84
uwsgi: no # Removed (replaced with go.d/uwsgi).
src/go/plugin/go.d/README.md
+1
@@ -142,6 +142,7 @@ see the appropriate collector readme.
142
| [systemdunits](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/systemdunits) | Systemd unit state |
143
| [tengine](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tengine) | Tengine |
144
| [tomcat](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tomcat) | Tomcat |
145
+| [tor](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tor) | Tor |
146
| [traefik](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/traefik) | Traefik |
147
| [upsd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/upsd) | UPSd (Nut) |
148
| [unbound](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/unbound) | Unbound |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -106,6 +106,7 @@ modules:
106
# systemdunits: yes
107
# tengine: yes
108
# tomcat: yes
109
+# tor: yes
110
# traefik: yes
111
# upsd: yes
112
# unbound: yes
src/go/plugin/go.d/config/go.d/sd/docker.conf
+7
@@ -76,6 +76,8 @@ classify:
76
expr: '{{ match "sp" .Image "*/squid */squid:*" }}'
77
- tags: "tengine"
78
expr: '{{ match "sp" .Image "*/tengine */tengine:*" }}'
79
+ - tags: "tor"
80
+ expr: '{{ and (eq .PrivatePort "9051") (match "sp" .Image "*/tor */tor:*") }}'
81
- tags: "tomcat"
82
expr: '{{ match "sp" .Image "tomcat tomcat:* */tomcat */tomcat:*" }}'
83
- tags: "vernemq"
@@ -243,6 +245,11 @@ compose:
245
module: tomcat
246
name: docker_{{.Name}}
247
url: http://{{.Address}}
248
+ - selector: "tor"
249
+ template: |
250
+ module: tor
251
+ name: docker_{{.Name}}
252
+ address: {{.Address}}
253
- selector: "vernemq"
254
template: |
255
module: vernemq
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -122,6 +122,8 @@ classify:
122
expr: '{{ and (eq .Port "9001") (eq .Comm "supervisord") }}'
123
- tags: "tomcat"
124
expr: '{{ and (eq .Port "8080") (glob .Cmdline "*tomcat*") }}'
125
+ - tags: "tor"
126
+ expr: '{{ and (eq .Port "9051") (eq .Comm "tor") }}'
127
- tags: "traefik"
128
expr: '{{ and (eq .Port "80" "8080") (eq .Comm "traefik") }}'
129
- tags: "unbound"
@@ -477,6 +479,11 @@ compose:
479
module: tomcat
480
name: local
481
url: http://{{.Address}}
482
+ - selector: "tor"
483
+ template: |
484
+ module: tor
485
+ name: local
486
+ address: {{.Address}}
487
- selector: "unbound"
488
template: |
489
module: unbound
src/go/plugin/go.d/config/go.d/tor.conf
new
+6
@@ -0,0 +1,6 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tor#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# address: 127.0.0.1:9051
src/go/plugin/go.d/modules/init.go
+1
@@ -98,6 +98,7 @@ import (
98
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/systemdunits"
99
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tengine"
100
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tomcat"
101
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tor"
102
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/traefik"
103
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/unbound"
104
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/upsd"
src/go/plugin/go.d/modules/tor/charts.go
new
+43
@@ -0,0 +1,43 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package tor
4
+
5
+import (
6
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7
+)
8
+
9
+const (
10
+ prioTraffic = module.Priority + iota
11
+ prioUptime
12
+)
13
+
14
+var charts = module.Charts{
15
+ trafficChart.Copy(),
16
+ uptimeChart.Copy(),
17
+}
18
+
19
+var trafficChart = module.Chart{
20
+ ID: "traffic",
21
+ Title: "Tor Traffic",
22
+ Units: "KiB/s",
23
+ Fam: "traffic",
24
+ Ctx: "tor.traffic",
25
+ Type: module.Area,
26
+ Priority: prioTraffic,
27
+ Dims: module.Dims{
28
+ {ID: "traffic/read", Name: "read", Algo: module.Incremental, Div: 1024},
29
+ {ID: "traffic/written", Name: "write", Algo: module.Incremental, Mul: -1, Div: 1024},
30
+ },
31
+}
32
+
33
+var uptimeChart = module.Chart{
34
+ ID: "uptime",
35
+ Title: "Tor Uptime",
36
+ Units: "seconds",
37
+ Fam: "uptime",
38
+ Ctx: "tor.uptime",
39
+ Priority: prioUptime,
40
+ Dims: module.Dims{
41
+ {ID: "uptime"},
42
+ },
43
+}
src/go/plugin/go.d/modules/tor/client.go
new
+117
@@ -0,0 +1,117 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package tor
4
+
5
+import (
6
+ "bytes"
7
+ "errors"
8
+ "fmt"
9
+ "strings"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
12
+)
13
+
14
+// https://spec.torproject.org/control-spec/index.html
15
+// https://github.com/torproject/stem/blob/master/stem/control.py
16
+
17
+const (
18
+ cmdAuthenticate = "AUTHENTICATE"
19
+ cmdQuit = "QUIT"
20
+ cmdGetInfo = "GETINFO"
21
+)
22
+
23
+type controlConn interface {
24
+ connect() error
25
+ disconnect()
26
+
27
+ getInfo(...string) ([]byte, error)
28
+}
29
+
30
+func newControlConn(conf Config) controlConn {
31
+ return &torControlClient{
32
+ password: conf.Password,
33
+ conn: socket.New(socket.Config{
34
+ Address: conf.Address,
35
+ ConnectTimeout: conf.Timeout.Duration(),
36
+ ReadTimeout: conf.Timeout.Duration(),
37
+ WriteTimeout: conf.Timeout.Duration(),
38
+ })}
39
+}
40
+
41
+type torControlClient struct {
42
+ password string
43
+ conn socket.Client
44
+}
45
+
46
+func (c *torControlClient) connect() error {
47
+ if err := c.conn.Connect(); err != nil {
48
+ return err
49
+ }
50
+
51
+ return c.authenticate()
52
+}
53
+
54
+func (c *torControlClient) authenticate() error {
55
+ // https://spec.torproject.org/control-spec/commands.html#authenticate
56
+
57
+ cmd := cmdAuthenticate
58
+ if c.password != "" {
59
+ cmd = fmt.Sprintf("%s \"%s\"", cmdAuthenticate, c.password)
60
+ }
61
+
62
+ var s string
63
+ err := c.conn.Command(cmd+"\n", func(bs []byte) bool {
64
+ s = string(bs)
65
+ return false
66
+ })
67
+ if err != nil {
68
+ return fmt.Errorf("authentication failed: %v", err)
69
+ }
70
+ if !strings.HasPrefix(s, "250") {
71
+ return fmt.Errorf("authentication failed: %s", s)
72
+ }
73
+ return nil
74
+}
75
+
76
+func (c *torControlClient) disconnect() {
77
+ // https://spec.torproject.org/control-spec/commands.html#quit
78
+
79
+ _ = c.conn.Command(cmdQuit+"\n", func(bs []byte) bool { return false })
80
+ _ = c.conn.Disconnect()
81
+}
82
+
83
+func (c *torControlClient) getInfo(keywords ...string) ([]byte, error) {
84
+ // https://spec.torproject.org/control-spec/commands.html#getinfo
85
+
86
+ if len(keywords) == 0 {
87
+ return nil, errors.New("no keywords specified")
88
+ }
89
+ cmd := fmt.Sprintf("%s %s", cmdGetInfo, strings.Join(keywords, " "))
90
+
91
+ var buf bytes.Buffer
92
+ var err error
93
+
94
+ clientErr := c.conn.Command(cmd+"\n", func(bs []byte) bool {
95
+ s := string(bs)
96
+
97
+ switch {
98
+ case strings.HasPrefix(s, "250-"):
99
+ buf.WriteString(strings.TrimPrefix(s, "250-"))
100
+ buf.WriteByte('\n')
101
+ return true
102
+ case strings.HasPrefix(s, "250 "):
103
+ return false
104
+ default:
105
+ err = errors.New(s)
106
+ return false
107
+ }
108
+ })
109
+ if clientErr != nil {
110
+ return nil, fmt.Errorf("command '%s' failed: %v", cmd, clientErr)
111
+ }
112
+ if err != nil {
113
+ return nil, fmt.Errorf("command '%s' failed: %v", cmd, err)
114
+ }
115
+
116
+ return buf.Bytes(), nil
117
+}
src/go/plugin/go.d/modules/tor/collect.go
new
+65
@@ -0,0 +1,65 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package tor
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "fmt"
9
+ "strconv"
10
+ "strings"
11
+)
12
+
13
+func (t *Tor) collect() (map[string]int64, error) {
14
+ if t.conn == nil {
15
+ conn, err := t.establishConnection()
16
+ if err != nil {
17
+ return nil, err
18
+ }
19
+ t.conn = conn
20
+ }
21
+
22
+ mx := make(map[string]int64)
23
+ if err := t.collectServerInfo(mx); err != nil {
24
+ t.Cleanup()
25
+ return nil, err
26
+ }
27
+
28
+ return mx, nil
29
+}
30
+
31
+func (t *Tor) collectServerInfo(mx map[string]int64) error {
32
+ resp, err := t.conn.getInfo("traffic/read", "traffic/written", "uptime")
33
+ if err != nil {
34
+ return err
35
+ }
36
+
37
+ sc := bufio.NewScanner(bytes.NewReader(resp))
38
+
39
+ for sc.Scan() {
40
+ line := sc.Text()
41
+
42
+ key, value, ok := strings.Cut(line, "=")
43
+ if !ok {
44
+ return fmt.Errorf("failed to parse metric: %s", line)
45
+ }
46
+
47
+ v, err := strconv.ParseInt(value, 10, 64)
48
+ if err != nil {
49
+ return fmt.Errorf("failed to parse metric %s value: %v", line, err)
50
+ }
51
+ mx[key] = v
52
+ }
53
+
54
+ return nil
55
+}
56
+
57
+func (t *Tor) establishConnection() (controlConn, error) {
58
+ conn := t.newConn(t.Config)
59
+
60
+ if err := conn.connect(); err != nil {
61
+ return nil, err
62
+ }
63
+
64
+ return conn, nil
65
+}
src/go/plugin/go.d/modules/tor/config_schema.json
new
+53
@@ -0,0 +1,53 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Tor collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 1
13
+ },
14
+ "address": {
15
+ "title": "Address",
16
+ "description": "The IP address and port where the Tor's Control Port listens for connections.",
17
+ "type": "string",
18
+ "default": "127.0.0.1:9051"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 1
26
+ },
27
+ "password": {
28
+ "title": "Password",
29
+ "description": "The password for authentication.",
30
+ "type": "string",
31
+ "sensitive": true
32
+ }
33
+ },
34
+ "required": [
35
+ "address"
36
+ ],
37
+ "additionalProperties": false,
38
+ "patternProperties": {
39
+ "^name$": {}
40
+ }
41
+ },
42
+ "uiSchema": {
43
+ "uiOptions": {
44
+ "fullPage": true
45
+ },
46
+ "timeout": {
47
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
48
+ },
49
+ "password": {
50
+ "ui:widget": "password"
51
+ }
52
+ }
53
+}
src/go/plugin/go.d/modules/tor/metadata.yaml
new
+135
@@ -0,0 +1,135 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-tor
5
+ plugin_name: go.d.plugin
6
+ module_name: tor
7
+ monitored_instance:
8
+ name: Tor
9
+ link: https://www.torproject.org/
10
+ categories:
11
+ - data-collection.vpns
12
+ icon_filename: "tor.svg"
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ info_provided_to_referring_integrations:
17
+ description: ""
18
+ keywords:
19
+ - tor
20
+ - traffic
21
+ - vpn
22
+ most_popular: false
23
+ overview:
24
+ data_collection:
25
+ metrics_description: |
26
+ Tracks Tor's download and upload traffic, as well as its uptime.
27
+ method_description: |
28
+ It reads the server's response to the [GETINFO](https://spec.torproject.org/control-spec/commands.html#getinfo) command.
29
+ supported_platforms:
30
+ include: []
31
+ exclude: []
32
+ multi_instance: true
33
+ additional_permissions:
34
+ description: ""
35
+ default_behavior:
36
+ auto_detection:
37
+ description: |
38
+ By default, it detects Tor instances running on localhost that are listening on port 9051.
39
+ On startup, it tries to collect metrics from:
40
+
41
+ - 127.0.0.1:9051
42
+ limits:
43
+ description: ""
44
+ performance_impact:
45
+ description: ""
46
+ setup:
47
+ prerequisites:
48
+ list:
49
+ - title: Enable Control Port
50
+ description: |
51
+ Enable `ControlPort` in `/etc/tor/torrc`.
52
+ configuration:
53
+ file:
54
+ name: go.d/tor.conf
55
+ options:
56
+ description: |
57
+ The following options can be defined globally: update_every, autodetection_retry.
58
+ folding:
59
+ title: Config options
60
+ enabled: true
61
+ list:
62
+ - name: update_every
63
+ description: Data collection frequency.
64
+ default_value: 1
65
+ required: false
66
+ - name: autodetection_retry
67
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
68
+ default_value: 0
69
+ required: false
70
+ - name: address
71
+ description: The IP address and port where the Tor's Control Port listens for connections.
72
+ default_value: 127.0.0.1:9051
73
+ required: true
74
+ - name: timeout
75
+ description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
76
+ default_value: 1
77
+ required: false
78
+ - name: password
79
+ description: Password for authentication.
80
+ default_value: ""
81
+ required: false
82
+ examples:
83
+ folding:
84
+ title: Config
85
+ enabled: true
86
+ list:
87
+ - name: Basic
88
+ description: A basic example configuration.
89
+ config: |
90
+ jobs:
91
+ - name: local
92
+ address: 127.0.0.1:9051
93
+ password: somePassword
94
+ - name: Multi-instance
95
+ description: |
96
+ > **Note**: When you define multiple jobs, their names must be unique.
97
+
98
+ Collecting metrics from local and remote instances.
99
+ config: |
100
+ jobs:
101
+ - name: local
102
+ address: 127.0.0.1:9051
103
+ password: somePassword
104
+
105
+ - name: remote
106
+ address: 203.0.113.0:9051
107
+ password: somePassword
108
+ troubleshooting:
109
+ problems:
110
+ list: []
111
+ alerts: []
112
+ metrics:
113
+ folding:
114
+ title: Metrics
115
+ enabled: false
116
+ description: ""
117
+ availability: []
118
+ scopes:
119
+ - name: global
120
+ description: "These metrics refer to the entire monitored application."
121
+ labels: []
122
+ metrics:
123
+ - name: tor.traffic
124
+ description: Tor Traffic
125
+ unit: "KiB/s"
126
+ chart_type: area
127
+ dimensions:
128
+ - name: read
129
+ - name: write
130
+ - name: tor.uptime
131
+ description: Tor Uptime
132
+ unit: "seconds"
133
+ chart_type: line
134
+ dimensions:
135
+ - name: uptime
src/go/plugin/go.d/modules/tor/testdata/config.json
new
+6
@@ -0,0 +1,6 @@
1
+{
2
+ "update_every": 123,
3
+ "address": "ok",
4
+ "timeout": 123.123,
5
+ "password": "ok"
6
+}
src/go/plugin/go.d/modules/tor/testdata/config.yaml
new
+4
@@ -0,0 +1,4 @@
1
+update_every: 123
2
+address: "ok"
3
+timeout: 123.123
4
+password: "ok"
src/go/plugin/go.d/modules/tor/tor.go
new
+102
@@ -0,0 +1,102 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package tor
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("tor", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Create: func() module.Module { return New() },
21
+ Config: func() any { return &Config{} },
22
+ })
23
+}
24
+
25
+func New() *Tor {
26
+ return &Tor{
27
+ Config: Config{
28
+ Address: "127.0.0.1:9051",
29
+ Timeout: web.Duration(time.Second * 1),
30
+ },
31
+ newConn: newControlConn,
32
+ charts: charts.Copy(),
33
+ }
34
+}
35
+
36
+type Config struct {
37
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38
+ Address string `yaml:"address" json:"address"`
39
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
40
+ Password string `yaml:"password" json:"password"`
41
+}
42
+
43
+type Tor struct {
44
+ module.Base
45
+ Config `yaml:",inline" json:""`
46
+
47
+ charts *module.Charts
48
+
49
+ newConn func(Config) controlConn
50
+ conn controlConn
51
+}
52
+
53
+func (t *Tor) Configuration() any {
54
+ return t.Config
55
+}
56
+
57
+func (t *Tor) Init() error {
58
+ if t.Address == "" {
59
+ t.Error("config: 'address' not set")
60
+ return errors.New("address not set")
61
+ }
62
+
63
+ return nil
64
+}
65
+
66
+func (t *Tor) Check() error {
67
+ mx, err := t.collect()
68
+ if err != nil {
69
+ t.Error(err)
70
+ return err
71
+ }
72
+
73
+ if len(mx) == 0 {
74
+ return errors.New("no metrics collected")
75
+ }
76
+
77
+ return nil
78
+}
79
+
80
+func (t *Tor) Charts() *module.Charts {
81
+ return t.charts
82
+}
83
+
84
+func (t *Tor) Collect() map[string]int64 {
85
+ mx, err := t.collect()
86
+ if err != nil {
87
+ t.Error(err)
88
+ }
89
+
90
+ if len(mx) == 0 {
91
+ return nil
92
+ }
93
+
94
+ return mx
95
+}
96
+
97
+func (t *Tor) Cleanup() {
98
+ if t.conn != nil {
99
+ t.conn.disconnect()
100
+ t.conn = nil
101
+ }
102
+}
src/go/plugin/go.d/modules/tor/tor_test.go
new
+328
@@ -0,0 +1,328 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package tor
4
+
5
+import (
6
+ "bufio"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "net"
11
+ "os"
12
+ "strings"
13
+ "testing"
14
+ "time"
15
+
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
17
+
18
+ "github.com/stretchr/testify/assert"
19
+ "github.com/stretchr/testify/require"
20
+)
21
+
22
+var (
23
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
24
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
25
+)
26
+
27
+func Test_testDataIsValid(t *testing.T) {
28
+ for name, data := range map[string][]byte{
29
+ "dataConfigJSON": dataConfigJSON,
30
+ "dataConfigYAML": dataConfigYAML,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+ }
34
+}
35
+
36
+func TestTor_ConfigurationSerialize(t *testing.T) {
37
+ module.TestConfigurationSerialize(t, &Tor{}, dataConfigJSON, dataConfigYAML)
38
+}
39
+
40
+func TestTor_Init(t *testing.T) {
41
+ tests := map[string]struct {
42
+ config Config
43
+ wantFail bool
44
+ }{
45
+ "success with default config": {
46
+ wantFail: false,
47
+ config: New().Config,
48
+ },
49
+ "fails if address not set": {
50
+ wantFail: true,
51
+ config: func() Config {
52
+ conf := New().Config
53
+ conf.Address = ""
54
+ return conf
55
+ }(),
56
+ },
57
+ }
58
+
59
+ for name, test := range tests {
60
+ t.Run(name, func(t *testing.T) {
61
+ tor := New()
62
+ tor.Config = test.config
63
+
64
+ if test.wantFail {
65
+ assert.Error(t, tor.Init())
66
+ } else {
67
+ assert.NoError(t, tor.Init())
68
+ }
69
+ })
70
+ }
71
+}
72
+
73
+func TestTor_Charts(t *testing.T) {
74
+ assert.NotNil(t, New().Charts())
75
+}
76
+
77
+func TestTor_Check(t *testing.T) {
78
+ tests := map[string]struct {
79
+ prepare func() (*Tor, *mockTorDaemon)
80
+ wantFail bool
81
+ }{
82
+ "success on valid response": {
83
+ wantFail: false,
84
+ prepare: prepareCaseOk,
85
+ },
86
+ "fails on connection refused": {
87
+ wantFail: true,
88
+ prepare: prepareCaseConnectionRefused,
89
+ },
90
+ }
91
+ for name, test := range tests {
92
+ t.Run(name, func(t *testing.T) {
93
+ tor, daemon := test.prepare()
94
+
95
+ defer func() {
96
+ assert.NoError(t, daemon.Close(), "daemon.Close()")
97
+ }()
98
+ go func() {
99
+ assert.NoError(t, daemon.Run(), "daemon.Run()")
100
+ }()
101
+
102
+ select {
103
+ case <-daemon.started:
104
+ case <-time.After(time.Second * 3):
105
+ t.Errorf("mock tor daemon start timed out")
106
+ }
107
+
108
+ require.NoError(t, tor.Init())
109
+
110
+ if test.wantFail {
111
+ assert.Error(t, tor.Check())
112
+ } else {
113
+ assert.NoError(t, tor.Check())
114
+ }
115
+
116
+ tor.Cleanup()
117
+
118
+ select {
119
+ case <-daemon.stopped:
120
+ case <-time.After(time.Second * 3):
121
+ t.Errorf("mock tor daemon stop timed out")
122
+ }
123
+ })
124
+ }
125
+}
126
+
127
+func TestTor_Collect(t *testing.T) {
128
+ tests := map[string]struct {
129
+ prepare func() (*Tor, *mockTorDaemon)
130
+ wantMetrics map[string]int64
131
+ wantCharts int
132
+ }{
133
+ "success on valid response": {
134
+ prepare: prepareCaseOk,
135
+ wantCharts: len(charts),
136
+ wantMetrics: map[string]int64{
137
+ "traffic/read": 100,
138
+ "traffic/written": 100,
139
+ "uptime": 100,
140
+ },
141
+ },
142
+ "fails on connection refused": {
143
+ prepare: prepareCaseConnectionRefused,
144
+ wantCharts: len(charts),
145
+ },
146
+ }
147
+
148
+ for name, test := range tests {
149
+ t.Run(name, func(t *testing.T) {
150
+ tor, daemon := test.prepare()
151
+
152
+ defer func() {
153
+ assert.NoError(t, daemon.Close(), "daemon.Close()")
154
+ }()
155
+ go func() {
156
+ assert.NoError(t, daemon.Run(), "daemon.Run()")
157
+ }()
158
+
159
+ select {
160
+ case <-daemon.started:
161
+ case <-time.After(time.Second * 3):
162
+ t.Errorf("mock tor daemon start timed out")
163
+ }
164
+
165
+ require.NoError(t, tor.Init())
166
+
167
+ mx := tor.Collect()
168
+
169
+ require.Equal(t, test.wantMetrics, mx)
170
+
171
+ assert.Equal(t, test.wantCharts, len(*tor.Charts()), "want charts")
172
+
173
+ if len(test.wantMetrics) > 0 {
174
+ module.TestMetricsHasAllChartsDims(t, tor.Charts(), mx)
175
+ }
176
+
177
+ tor.Cleanup()
178
+
179
+ select {
180
+ case <-daemon.stopped:
181
+ case <-time.After(time.Second * 3):
182
+ t.Errorf("mock tordaemon stop timed out")
183
+ }
184
+ })
185
+ }
186
+}
187
+
188
+func prepareCaseOk() (*Tor, *mockTorDaemon) {
189
+ daemon := &mockTorDaemon{
190
+ addr: "127.0.0.1:65001",
191
+ started: make(chan struct{}),
192
+ stopped: make(chan struct{}),
193
+ }
194
+
195
+ tor := New()
196
+ tor.Address = daemon.addr
197
+
198
+ return tor, daemon
199
+}
200
+
201
+func prepareCaseConnectionRefused() (*Tor, *mockTorDaemon) {
202
+ ch := make(chan struct{})
203
+ close(ch)
204
+
205
+ daemon := &mockTorDaemon{
206
+ addr: "127.0.0.1:65001",
207
+ dontStart: true,
208
+ started: ch,
209
+ stopped: ch,
210
+ }
211
+
212
+ tor := New()
213
+ tor.Address = daemon.addr
214
+
215
+ return tor, daemon
216
+}
217
+
218
+type mockTorDaemon struct {
219
+ addr string
220
+ srv net.Listener
221
+ started chan struct{}
222
+ stopped chan struct{}
223
+ dontStart bool
224
+ authenticated bool
225
+}
226
+
227
+func (m *mockTorDaemon) Run() error {
228
+ if m.dontStart {
229
+ return nil
230
+ }
231
+
232
+ srv, err := net.Listen("tcp", m.addr)
233
+ if err != nil {
234
+ return err
235
+ }
236
+
237
+ m.srv = srv
238
+
239
+ close(m.started)
240
+ defer close(m.stopped)
241
+
242
+ return m.handleConnections()
243
+}
244
+
245
+func (m *mockTorDaemon) Close() error {
246
+ if m.srv != nil {
247
+ err := m.srv.Close()
248
+ m.srv = nil
249
+ return err
250
+ }
251
+ return nil
252
+}
253
+
254
+func (m *mockTorDaemon) handleConnections() error {
255
+ conn, err := m.srv.Accept()
256
+ if err != nil || conn == nil {
257
+ return errors.New("could not accept connection")
258
+ }
259
+ return m.handleConnection(conn)
260
+}
261
+
262
+func (m *mockTorDaemon) handleConnection(conn net.Conn) error {
263
+ defer func() { _ = conn.Close() }()
264
+
265
+ rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
266
+ var line string
267
+ var err error
268
+
269
+ for {
270
+ if line, err = rw.ReadString('\n'); err != nil {
271
+ return fmt.Errorf("error reading from connection: %v", err)
272
+ }
273
+
274
+ line = strings.TrimSpace(line)
275
+
276
+ cmd, param, _ := strings.Cut(line, " ")
277
+
278
+ switch cmd {
279
+ case cmdQuit:
280
+ return m.handleQuit(conn)
281
+ case cmdAuthenticate:
282
+ err = m.handleAuthenticate(conn)
283
+ case cmdGetInfo:
284
+ err = m.handleGetInfo(conn, param)
285
+ default:
286
+ s := fmt.Sprintf("510 Unrecognized command \"%s\"\n", cmd)
287
+ _, _ = rw.WriteString(s)
288
+ return fmt.Errorf("unexpected command: %s", line)
289
+ }
290
+
291
+ _ = rw.Flush()
292
+
293
+ if err != nil {
294
+ return err
295
+ }
296
+ }
297
+}
298
+
299
+func (m *mockTorDaemon) handleQuit(conn io.Writer) error {
300
+ _, err := conn.Write([]byte("250 closing connection\n"))
301
+ return err
302
+}
303
+
304
+func (m *mockTorDaemon) handleAuthenticate(conn io.Writer) error {
305
+ m.authenticated = true
306
+ _, err := conn.Write([]byte("250 OK\n"))
307
+ return err
308
+}
309
+
310
+func (m *mockTorDaemon) handleGetInfo(conn io.Writer, keywords string) error {
311
+ if !m.authenticated {
312
+ _, _ = conn.Write([]byte("514 Authentication required\n"))
313
+ return errors.New("authentication required")
314
+ }
315
+
316
+ keywords = strings.Trim(keywords, "\"")
317
+
318
+ for _, k := range strings.Fields(keywords) {
319
+ s := fmt.Sprintf("250-%s=%d\n", k, 100)
320
+
321
+ if _, err := conn.Write([]byte(s)); err != nil {
322
+ return err
323
+ }
324
+ }
325
+
326
+ _, err := conn.Write([]byte("250 OK\n"))
327
+ return err
328
+}