go.d/portcheck: add UDP support (#18569)
Ilya Mashchenko committed
Sep 18, 2024 at 00:02 UTC
15878f929dd839757dd2db257b67f9bc8ef716ee
11 files changed
+557
-184
src/go/plugin/go.d/modules/portcheck/charts.go
+87
-38
@@ -13,54 +13,103 @@ const (
13
prioCheckStatus = module.Priority + iota
14
prioCheckInStatusDuration
15
prioCheckLatency
16
+
17
+ prioUDPCheckStatus
18
+ prioUDPCheckInStatusDuration
19
)
20
18
-var chartsTmpl = module.Charts{
19
- checkStatusChartTmpl.Copy(),
20
- checkInStateDurationChartTmpl.Copy(),
21
- checkConnectionLatencyChartTmpl.Copy(),
21
+var tcpPortChartsTmpl = module.Charts{
22
+ tcpPortCheckStatusChartTmpl.Copy(),
23
+ tcpPortCheckInStateDurationChartTmpl.Copy(),
24
+ tcpPortCheckConnectionLatencyChartTmpl.Copy(),
25
}
26
24
-var checkStatusChartTmpl = module.Chart{
25
- ID: "port_%d_status",
26
- Title: "TCP Check Status",
27
- Units: "boolean",
28
- Fam: "status",
29
- Ctx: "portcheck.status",
30
- Priority: prioCheckStatus,
31
- Dims: module.Dims{
32
- {ID: "port_%d_success", Name: "success"},
33
- {ID: "port_%d_failed", Name: "failed"},
34
- {ID: "port_%d_timeout", Name: "timeout"},
35
- },
27
+var udpPortChartsTmpl = module.Charts{
28
+ udpPortCheckStatusChartTmpl.Copy(),
29
+ udpPortCheckInStatusDurationChartTmpl.Copy(),
30
}
31
38
-var checkInStateDurationChartTmpl = module.Chart{
39
- ID: "port_%d_current_state_duration",
40
- Title: "Current State Duration",
41
- Units: "seconds",
42
- Fam: "status duration",
43
- Ctx: "portcheck.state_duration",
44
- Priority: prioCheckInStatusDuration,
45
- Dims: module.Dims{
46
- {ID: "port_%d_current_state_duration", Name: "time"},
47
- },
32
+var (
33
+ tcpPortCheckStatusChartTmpl = module.Chart{
34
+ ID: "port_%d_status",
35
+ Title: "TCP Check Status",
36
+ Units: "boolean",
37
+ Fam: "status",
38
+ Ctx: "portcheck.status",
39
+ Priority: prioCheckStatus,
40
+ Dims: module.Dims{
41
+ {ID: "tcp_port_%d_success", Name: "success"},
42
+ {ID: "tcp_port_%d_failed", Name: "failed"},
43
+ {ID: "tcp_port_%d_timeout", Name: "timeout"},
44
+ },
45
+ }
46
+ tcpPortCheckInStateDurationChartTmpl = module.Chart{
47
+ ID: "port_%d_current_state_duration",
48
+ Title: "Current State Duration",
49
+ Units: "seconds",
50
+ Fam: "status duration",
51
+ Ctx: "portcheck.state_duration",
52
+ Priority: prioCheckInStatusDuration,
53
+ Dims: module.Dims{
54
+ {ID: "tcp_port_%d_current_state_duration", Name: "time"},
55
+ },
56
+ }
57
+ tcpPortCheckConnectionLatencyChartTmpl = module.Chart{
58
+ ID: "port_%d_connection_latency",
59
+ Title: "TCP Connection Latency",
60
+ Units: "ms",
61
+ Fam: "latency",
62
+ Ctx: "portcheck.latency",
63
+ Priority: prioCheckLatency,
64
+ Dims: module.Dims{
65
+ {ID: "tcp_port_%d_latency", Name: "time"},
66
+ },
67
+ }
68
+)
69
+
70
+var (
71
+ udpPortCheckStatusChartTmpl = module.Chart{
72
+ ID: "udp_port_%d_check_status",
73
+ Title: "UDP Port Check Status",
74
+ Units: "status",
75
+ Fam: "status",
76
+ Ctx: "portcheck.udp_port_status",
77
+ Priority: prioUDPCheckStatus,
78
+ Dims: module.Dims{
79
+ {ID: "udp_port_%d_open_filtered", Name: "open/filtered"},
80
+ {ID: "udp_port_%d_closed", Name: "closed"},
81
+ },
82
+ }
83
+ udpPortCheckInStatusDurationChartTmpl = module.Chart{
84
+ ID: "udp_port_%d_current_status_duration",
85
+ Title: "UDP Port Current Status Duration",
86
+ Units: "seconds",
87
+ Fam: "status duration",
88
+ Ctx: "portcheck.udp_port_status_duration",
89
+ Priority: prioUDPCheckInStatusDuration,
90
+ Dims: module.Dims{
91
+ {ID: "udp_port_%d_current_status_duration", Name: "time"},
92
+ },
93
+ }
94
+)
95
+
96
+func (pc *PortCheck) addTCPPortCharts(port *tcpPort) {
97
+ charts := newPortCharts(pc.Host, port.number, tcpPortChartsTmpl.Copy())
98
+
99
+ if err := pc.Charts().Add(*charts...); err != nil {
100
+ pc.Warning(err)
101
+ }
102
}
103
50
-var checkConnectionLatencyChartTmpl = module.Chart{
51
- ID: "port_%d_connection_latency",
52
- Title: "TCP Connection Latency",
53
- Units: "ms",
54
- Fam: "latency",
55
- Ctx: "portcheck.latency",
56
- Priority: prioCheckLatency,
57
- Dims: module.Dims{
58
- {ID: "port_%d_latency", Name: "time"},
59
- },
104
+func (pc *PortCheck) addUDPPortCharts(port *udpPort) {
105
+ charts := newPortCharts(pc.Host, port.number, udpPortChartsTmpl.Copy())
106
+
107
+ if err := pc.Charts().Add(*charts...); err != nil {
108
+ pc.Warning(err)
109
+ }
110
}
111
62
-func newPortCharts(host string, port int) *module.Charts {
63
- charts := chartsTmpl.Copy()
112
+func newPortCharts(host string, port int, charts *module.Charts) *module.Charts {
113
for _, chart := range *charts {
114
chart.Labels = []module.Label{
115
{Key: "host", Value: host},
src/go/plugin/go.d/modules/portcheck/check_tcp_port.go
new
+56
@@ -0,0 +1,56 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package portcheck
4
+
5
+import (
6
+ "time"
7
+)
8
+
9
+const (
10
+ tcpPortCheckStateSuccess = "success"
11
+ tcpPortCheckStateTimeout = "timeout"
12
+ tcpPortCheckStateFailed = "failed"
13
+)
14
+
15
+type tcpPort struct {
16
+ number int
17
+ state string
18
+ inState int
19
+ latency int
20
+}
21
+
22
+func (pc *PortCheck) checkTCPPort(port *tcpPort) {
23
+ start := time.Now()
24
+
25
+ addr := pc.address(port.number)
26
+ conn, err := pc.dialTCP("tcp", addr, pc.Timeout.Duration())
27
+
28
+ dur := time.Since(start)
29
+
30
+ defer func() {
31
+ if conn != nil {
32
+ _ = conn.Close()
33
+ }
34
+ }()
35
+
36
+ if err != nil {
37
+ if v, ok := err.(interface{ Timeout() bool }); ok && v.Timeout() {
38
+ pc.setTcpPortCheckState(port, tcpPortCheckStateTimeout)
39
+ } else {
40
+ pc.setTcpPortCheckState(port, tcpPortCheckStateFailed)
41
+ }
42
+ return
43
+ }
44
+
45
+ pc.setTcpPortCheckState(port, tcpPortCheckStateSuccess)
46
+ port.latency = durationToMs(dur)
47
+}
48
+
49
+func (pc *PortCheck) setTcpPortCheckState(port *tcpPort, state string) {
50
+ if port.state == state {
51
+ port.inState += pc.UpdateEvery
52
+ } else {
53
+ port.inState = pc.UpdateEvery
54
+ port.state = state
55
+ }
56
+}
src/go/plugin/go.d/modules/portcheck/check_udp_port.go
new
+167
@@ -0,0 +1,167 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package portcheck
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "net"
9
+ "time"
10
+
11
+ "golang.org/x/net/icmp"
12
+ "golang.org/x/net/ipv4"
13
+ "golang.org/x/net/ipv6"
14
+)
15
+
16
+const (
17
+ udpPortCheckStateOpenFiltered = "open_filtered"
18
+ udpPortCheckStateClosed = "closed"
19
+)
20
+
21
+type udpPort struct {
22
+ number int
23
+ state string
24
+ inState int
25
+
26
+ err error
27
+}
28
+
29
+func (pc *PortCheck) checkUDPPort(port *udpPort) {
30
+ port.err = nil
31
+
32
+ timeout := time.Duration(max(float64(100*time.Millisecond), float64(pc.Timeout.Duration())*0.7))
33
+ addr := pc.address(port.number)
34
+
35
+ open, err := pc.scanUDP(addr, timeout)
36
+ if err != nil {
37
+ pc.Warningf("UDP port check failed for '%s': %v", addr, err)
38
+ port.err = err
39
+ return
40
+ }
41
+
42
+ state := udpPortCheckStateOpenFiltered
43
+ if !open {
44
+ state = udpPortCheckStateClosed
45
+ }
46
+
47
+ pc.setUDPPortCheckState(port, state)
48
+}
49
+
50
+func (pc *PortCheck) setUDPPortCheckState(port *udpPort, state string) {
51
+ if port.state == state {
52
+ port.inState += pc.UpdateEvery
53
+ } else {
54
+ port.inState = pc.UpdateEvery
55
+ port.state = state
56
+ }
57
+}
58
+
59
+func scanUDPPort(address string, timeout time.Duration) (bool, error) {
60
+ // With this scan type, we send 0-byte UDP packets to the port on the target system.
61
+ // Receipt of an ICMP Destination Unreachable message signifies the port is closed;
62
+ // otherwise it is assumed open (timeout).
63
+ // This is equivalent to "close"/"open/filtered" states reported by nmap.
64
+
65
+ raddr, err := net.ResolveUDPAddr("udp", address)
66
+ if err != nil {
67
+ return false, fmt.Errorf("failed to resolve UDP address: %w", err)
68
+ }
69
+
70
+ network, icmpNetwork, icmpProto := getUDPNetworkParams(raddr.IP)
71
+
72
+ udpConn, err := net.DialUDP(network, nil, raddr)
73
+ if err != nil {
74
+ return false, fmt.Errorf("failed to open UDP connection to '%s': %w", raddr.String(), err)
75
+ }
76
+ defer func() { _ = udpConn.Close() }()
77
+
78
+ icmpConn, err := icmp.ListenPacket(icmpNetwork, "")
79
+ if err != nil {
80
+ return false, fmt.Errorf("failed to listen for ICMP packets: %w", err)
81
+ }
82
+ defer func() { _ = icmpConn.Close() }()
83
+
84
+ if _, err = udpConn.Write([]byte{}); err != nil {
85
+ return false, fmt.Errorf("failed to send UDP packet: %w", err)
86
+ }
87
+
88
+ return readICMPResponse(icmpConn, udpConn, icmpProto, timeout)
89
+}
90
+
91
+func readICMPResponse(icmpConn *icmp.PacketConn, udpConn *net.UDPConn, icmpProto int, timeout time.Duration) (bool, error) {
92
+ buff := make([]byte, 1500)
93
+
94
+ if err := icmpConn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
95
+ return false, fmt.Errorf("failed to set read deadline on ICMP connection: %w", err)
96
+ }
97
+
98
+ localPort := uint16(udpConn.LocalAddr().(*net.UDPAddr).Port)
99
+
100
+ for {
101
+ n, _, err := icmpConn.ReadFrom(buff)
102
+ if err != nil {
103
+ if errors.Is(err, net.ErrClosed) {
104
+ return false, fmt.Errorf("ICMP connection closed unexpectedly")
105
+ }
106
+ var netErr net.Error
107
+ if errors.As(err, &netErr) && netErr.Timeout() {
108
+ return true, nil // Timeout means no ICMP response, assume port is open
109
+ }
110
+ return false, fmt.Errorf("failed to read ICMP packet: %w", err)
111
+ }
112
+
113
+ if n == 0 {
114
+ continue
115
+ }
116
+
117
+ msg, err := icmp.ParseMessage(icmpProto, buff[:n])
118
+ if err != nil {
119
+ return false, fmt.Errorf("failed to parse ICMP message: %w", err)
120
+ }
121
+
122
+ if msg.Type != ipv4.ICMPTypeDestinationUnreachable && msg.Type != ipv6.ICMPTypeDestinationUnreachable {
123
+ continue
124
+ }
125
+
126
+ body, ok := msg.Body.(*icmp.DstUnreach)
127
+ if !ok {
128
+ continue
129
+ }
130
+
131
+ srcPort, err := extractSourcePort(msg.Type, body.Data)
132
+ if err != nil {
133
+ return false, err
134
+ }
135
+
136
+ if srcPort == localPort {
137
+ return false, nil // Received ICMP Destination Unreachable, port is closed
138
+ }
139
+ }
140
+}
141
+
142
+func getUDPNetworkParams(ip net.IP) (network, icmpNetwork string, icmpProto int) {
143
+ if ip.To4() != nil {
144
+ return "udp4", "ip4:icmp", 1
145
+ }
146
+ return "udp6", "ip6:ipv6-icmp", 58
147
+}
148
+
149
+func extractSourcePort(msgType icmp.Type, data []byte) (uint16, error) {
150
+ const udpHeaderLen = 8
151
+ var headerLen, minLen int
152
+
153
+ switch msgType {
154
+ case ipv4.ICMPTypeDestinationUnreachable:
155
+ headerLen, minLen = ipv4.HeaderLen, ipv4.HeaderLen+udpHeaderLen
156
+ case ipv6.ICMPTypeDestinationUnreachable:
157
+ headerLen, minLen = ipv6.HeaderLen, ipv6.HeaderLen+udpHeaderLen
158
+ default:
159
+ return 0, fmt.Errorf("unexpected ICMP message type: %v", msgType)
160
+ }
161
+
162
+ if len(data) < minLen {
163
+ return 0, fmt.Errorf("ICMP message too short: want %d got %d", minLen, len(data))
164
+ }
165
+
166
+ return (uint16(data[headerLen]) << udpHeaderLen) | uint16(data[headerLen+1]), nil
167
+}
src/go/plugin/go.d/modules/portcheck/collect.go
+62
-45
@@ -3,75 +3,92 @@
3
package portcheck
4
5
import (
6
+ "errors"
7
"fmt"
8
+ "net"
9
+ "strconv"
10
+ "strings"
11
"sync"
12
"time"
13
)
14
11
-type checkState string
12
-
13
-const (
14
- checkStateSuccess checkState = "success"
15
- checkStateTimeout checkState = "timeout"
16
- checkStateFailed checkState = "failed"
17
-)
18
-
15
func (pc *PortCheck) collect() (map[string]int64, error) {
16
wg := &sync.WaitGroup{}
17
22
- for _, p := range pc.ports {
18
+ for _, port := range pc.tcpPorts {
19
+ wg.Add(1)
20
+ port := port
21
+ go func() { defer wg.Done(); pc.checkTCPPort(port) }()
22
+ }
23
+ for _, port := range pc.udpPorts {
24
wg.Add(1)
24
- go func(p *port) { pc.checkPort(p); wg.Done() }(p)
25
+ port := port
26
+ go func() { defer wg.Done(); pc.checkUDPPort(port) }()
27
}
28
+
29
wg.Wait()
30
31
+ // FIXME: in state time calculation
32
+
33
mx := make(map[string]int64)
34
30
- for _, p := range pc.ports {
31
- mx[fmt.Sprintf("port_%d_current_state_duration", p.number)] = int64(p.inState)
32
- mx[fmt.Sprintf("port_%d_latency", p.number)] = int64(p.latency)
33
- mx[fmt.Sprintf("port_%d_%s", p.number, checkStateSuccess)] = 0
34
- mx[fmt.Sprintf("port_%d_%s", p.number, checkStateTimeout)] = 0
35
- mx[fmt.Sprintf("port_%d_%s", p.number, checkStateFailed)] = 0
36
- mx[fmt.Sprintf("port_%d_%s", p.number, p.state)] = 1
35
+ for _, p := range pc.tcpPorts {
36
+ if !pc.seenTcpPorts[p.number] {
37
+ pc.seenTcpPorts[p.number] = true
38
+ pc.addTCPPortCharts(p)
39
+ }
40
+
41
+ px := fmt.Sprintf("tcp_port_%d_", p.number)
42
+
43
+ mx[px+"current_state_duration"] = int64(p.inState)
44
+ mx[px+"latency"] = int64(p.latency)
45
+ mx[px+tcpPortCheckStateSuccess] = 0
46
+ mx[px+tcpPortCheckStateTimeout] = 0
47
+ mx[px+tcpPortCheckStateFailed] = 0
48
+ mx[px+p.state] = 1
49
}
50
39
- return mx, nil
40
-}
51
+ if pc.doUdpPorts {
52
+ for _, p := range pc.udpPorts {
53
+ if p.err != nil {
54
+ if isListenOpNotPermittedError(p.err) {
55
+ pc.doUdpPorts = false
56
+ break
57
+ }
58
+ continue
59
+ }
60
42
-func (pc *PortCheck) checkPort(p *port) {
43
- start := time.Now()
44
- conn, err := pc.dial("tcp", fmt.Sprintf("%s:%d", pc.Host, p.number), pc.Timeout.Duration())
45
- dur := time.Since(start)
61
+ if !pc.seenUdpPorts[p.number] {
62
+ pc.seenUdpPorts[p.number] = true
63
+ pc.addUDPPortCharts(p)
64
+ }
65
47
- defer func() {
48
- if conn != nil {
49
- _ = conn.Close()
50
- }
51
- }()
52
-
53
- if err != nil {
54
- v, ok := err.(interface{ Timeout() bool })
55
- if ok && v.Timeout() {
56
- pc.setPortState(p, checkStateTimeout)
57
- } else {
58
- pc.setPortState(p, checkStateFailed)
66
+ px := fmt.Sprintf("udp_port_%d_", p.number)
67
+
68
+ mx[px+"current_status_duration"] = int64(p.inState)
69
+ mx[px+udpPortCheckStateOpenFiltered] = 0
70
+ mx[px+udpPortCheckStateClosed] = 0
71
+ mx[px+p.state] = 1
72
}
60
- return
73
}
62
- pc.setPortState(p, checkStateSuccess)
63
- p.latency = durationToMs(dur)
74
+
75
+ return mx, nil
76
}
77
66
-func (pc *PortCheck) setPortState(p *port, s checkState) {
67
- if p.state != s {
68
- p.inState = pc.UpdateEvery
69
- p.state = s
70
- } else {
71
- p.inState += pc.UpdateEvery
72
- }
78
+func (pc *PortCheck) address(port int) string {
79
+ // net.JoinHostPort expects literal IPv6 address, it adds []
80
+ host := strings.Trim(pc.Host, "[]")
81
+ return net.JoinHostPort(host, strconv.Itoa(port))
82
}
83
84
func durationToMs(duration time.Duration) int {
85
return int(duration) / (int(time.Millisecond) / int(time.Nanosecond))
86
}
87
+
88
+func isListenOpNotPermittedError(err error) bool {
89
+ // icmp.ListenPacket failed (socket: operation not permitted)
90
+ var opErr *net.OpError
91
+ return errors.As(err, &opErr) &&
92
+ opErr.Op == "listen" &&
93
+ strings.Contains(opErr.Error(), "operation not permitted")
94
+}
src/go/plugin/go.d/modules/portcheck/config_schema.json
+22
-5
@@ -22,10 +22,11 @@
22
"host": {
23
"title": "Network host",
24
"description": "The IP address or domain name of the network host.",
25
- "type": "string"
25
+ "type": "string",
26
+ "default": "127.0.0.1"
27
},
28
"ports": {
28
- "title": "Ports",
29
+ "title": "TCP ports",
30
"description": "A list of ports to monitor for TCP service availability and response time.",
31
"type": [
32
"array",
@@ -36,13 +37,25 @@
37
"type": "integer",
38
"minimum": 1
39
},
39
- "minItems": 1,
40
+ "uniqueItems": true
41
+ },
42
+ "udp_ports": {
43
+ "title": "UDP ports",
44
+ "description": "A list of ports to monitor for UDP service availability.",
45
+ "type": [
46
+ "array",
47
+ "null"
48
+ ],
49
+ "items": {
50
+ "title": "Port",
51
+ "type": "integer",
52
+ "minimum": 1
53
+ },
54
"uniqueItems": true
55
}
56
},
57
"required": [
44
- "host",
45
- "ports"
58
+ "host"
59
],
60
"additionalProperties": false,
61
"patternProperties": {
@@ -61,6 +74,10 @@
74
},
75
"ports": {
76
"ui:listFlavour": "list"
77
+ },
78
+ "udp_ports": {
79
+ "ui:help": "The collector sends 0-byte UDP packets to each port on the target system. If an ICMP Destination Unreachable message is received, the port is considered closed. Otherwise, it is assumed to be open or filtered (if no response is received within the timeout). This approach is similar to the behavior of the `close`/`open/filtered` states reported by `nmap`. However, note that the `open/filtered` state is a best-effort determination, as the collector does not actually exchange data with the application on the target system.",
80
+ "ui:listFlavour": "list"
81
}
82
}
83
}
src/go/plugin/go.d/modules/portcheck/init.go
+11
-28
@@ -6,44 +6,27 @@ import (
6
"errors"
7
"net"
8
"time"
9
-
10
- "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
9
)
10
13
-type dialFunc func(network, address string, timeout time.Duration) (net.Conn, error)
14
-
15
-type port struct {
16
- number int
17
- state checkState
18
- inState int
19
- latency int
20
-}
11
+type dialTCPFunc func(network, address string, timeout time.Duration) (net.Conn, error)
12
13
func (pc *PortCheck) validateConfig() error {
14
if pc.Host == "" {
24
- return errors.New("'host' parameter not set")
15
+ return errors.New("missing required parameter: 'host' must be specified")
16
}
26
- if len(pc.Ports) == 0 {
27
- return errors.New("'ports' parameter not set")
17
+ if len(pc.Ports) == 0 && len(pc.UDPPorts) == 0 {
18
+ return errors.New("missing required parameters: at least one of 'ports' (TCP) or 'udp_ports' (UDP) must be specified")
19
}
20
return nil
21
}
22
32
-func (pc *PortCheck) initCharts() (*module.Charts, error) {
33
- charts := module.Charts{}
34
-
35
- for _, port := range pc.Ports {
36
- if err := charts.Add(*newPortCharts(pc.Host, port)...); err != nil {
37
- return nil, err
38
- }
39
- }
40
-
41
- return &charts, nil
42
-}
43
-
44
-func (pc *PortCheck) initPorts() (ports []*port) {
23
+func (pc *PortCheck) initPorts() (tcpPorts []*tcpPort, udpPorts []*udpPort) {
24
for _, p := range pc.Ports {
46
- ports = append(ports, &port{number: p})
25
+ tcpPorts = append(tcpPorts, &tcpPort{number: p})
26
}
48
- return ports
27
+ for _, p := range pc.UDPPorts {
28
+ udpPorts = append(udpPorts, &udpPort{number: p})
29
+ }
30
+
31
+ return tcpPorts, udpPorts
32
}
src/go/plugin/go.d/modules/portcheck/metadata.yaml
+69
-8
@@ -5,7 +5,7 @@ modules:
5
plugin_name: go.d.plugin
6
module_name: portcheck
7
monitored_instance:
8
- name: TCP Endpoints
8
+ name: TCP/UDP Endpoints
9
link: ""
10
icon_filename: globe.svg
11
categories:
@@ -20,7 +20,29 @@ modules:
20
overview:
21
data_collection:
22
metrics_description: |
23
- This collector monitors TCP services availability and response time.
23
+ Collector for monitoring service availability and response time. It can be used to check if specific ports are open or reachable on a target system.
24
+
25
+ It supports both TCP and UDP protocols over IPv4 and IPv6 networks.
26
+
27
+ | Protocol | Check Description |
28
+ |----------|-----------------------------------------------------------------------------------------------------------------------------|
29
+ | TCP | Attempts to establish a TCP connection to the specified ports on the target system. |
30
+ | UDP | Sends a 0-byte UDP packet to the specified ports on the target system and analyzes ICMP responses to determine port status. |
31
+
32
+ Possible TCP statuses:
33
+
34
+ | TCP Status | Description |
35
+ |------------|-------------------------------------------------------------|
36
+ | success | Connection established successfully. |
37
+ | timeout | Connection timed out after waiting for configured duration. |
38
+ | failed | An error occurred during the connection attempt. |
39
+
40
+ Possible UDP statuses:
41
+
42
+ | TCP Status | Description |
43
+ |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
44
+ | open/filtered | No response received within the configured timeout. This status indicates the port is either open or filtered, but the exact state cannot be determined definitively. |
45
+ | closed | Received an ICMP Destination Unreachable message, indicating the port is closed. |
46
method_description: ""
47
supported_platforms:
48
include: []
@@ -61,7 +83,7 @@ modules:
83
default_value: ""
84
required: true
85
- name: ports
64
- description: Remote host ports. Must be specified in numeric format.
86
+ description: Remote host TCP ports. Must be specified in numeric format.
87
default_value: ""
88
required: true
89
- name: timeout
@@ -73,24 +95,42 @@ modules:
95
title: Config
96
enabled: true
97
list:
76
- - name: Check SSH and telnet
98
+ - name: Check TCP ports (IPv4)
99
description: An example configuration.
100
config: |
101
jobs:
80
- - name: server1
102
+ - name: local
103
host: 127.0.0.1
104
ports:
105
- 22
106
- 23
85
- - name: Check webserver with IPv6 address
107
+ - name: Check TCP ports (IPv6)
108
description: An example configuration.
109
config: |
110
jobs:
89
- - name: server2
111
+ - name: local
112
host: "[2001:DB8::1]"
113
ports:
114
- 80
115
- 8080
116
+ - name: Check UDP ports (IPv4)
117
+ description: An example configuration.
118
+ config: |
119
+ jobs:
120
+ - name: local
121
+ host: 127.0.0.1
122
+ udp_ports:
123
+ - 3120
124
+ - 3121
125
+ - name: Check UDP ports (IPv6)
126
+ description: An example configuration.
127
+ config: |
128
+ jobs:
129
+ - name: local
130
+ host: [::1]
131
+ udp_ports:
132
+ - 3120
133
+ - 3121
134
- name: Multi-instance
135
description: |
136
> **Note**: When you define multiple jobs, their names must be unique.
@@ -132,7 +172,7 @@ modules:
172
description: ""
173
availability: []
174
scopes:
135
- - name: tcp endpoint
175
+ - name: TCP endpoint
176
description: These metrics refer to the TCP endpoint.
177
labels:
178
- name: host
@@ -160,3 +200,24 @@ modules:
200
chart_type: line
201
dimensions:
202
- name: time
203
+ - name: UDP endpoint
204
+ description: These metrics refer to the UDP endpoint.
205
+ labels:
206
+ - name: host
207
+ description: host
208
+ - name: port
209
+ description: port
210
+ metrics:
211
+ - name: portcheck.udp_port_status
212
+ description: UDP Port Check Status
213
+ unit: status
214
+ chart_type: line
215
+ dimensions:
216
+ - name: open/filtered
217
+ - name: closed
218
+ - name: portcheck.udp_port_status_duration
219
+ description: UDP Port Current Status Duration
220
+ unit: seconds
221
+ chart_type: line
222
+ dimensions:
223
+ - name: time
src/go/plugin/go.d/modules/portcheck/portcheck.go
+32
-13
@@ -4,6 +4,7 @@ package portcheck
4
5
import (
6
_ "embed"
7
+ "errors"
8
"net"
9
"time"
10
@@ -30,7 +31,15 @@ func New() *PortCheck {
31
Config: Config{
32
Timeout: confopt.Duration(time.Second * 2),
33
},
33
- dial: net.DialTimeout,
34
+ charts: &module.Charts{},
35
+
36
+ dialTCP: net.DialTimeout,
37
+
38
+ scanUDP: scanUDPPort,
39
+ doUdpPorts: true,
40
+
41
+ seenUdpPorts: make(map[int]bool),
42
+ seenTcpPorts: make(map[int]bool),
43
}
44
}
45
@@ -38,6 +47,7 @@ type Config struct {
47
UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
48
Host string `yaml:"host" json:"host"`
49
Ports []int `yaml:"ports" json:"ports"`
50
+ UDPPorts []int `yaml:"udp_ports,omitempty" json:"udp_ports"`
51
Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
52
}
53
@@ -47,9 +57,15 @@ type PortCheck struct {
57
58
charts *module.Charts
59
50
- dial dialFunc
60
+ dialTCP dialTCPFunc
61
+ scanUDP func(address string, timeout time.Duration) (bool, error)
62
52
- ports []*port
63
+ tcpPorts []*tcpPort
64
+ seenTcpPorts map[int]bool
65
+
66
+ udpPorts []*udpPort
67
+ seenUdpPorts map[int]bool
68
+ doUdpPorts bool
69
}
70
71
func (pc *PortCheck) Configuration() any {
@@ -62,23 +78,25 @@ func (pc *PortCheck) Init() error {
78
return err
79
}
80
65
- charts, err := pc.initCharts()
66
- if err != nil {
67
- pc.Errorf("init charts: %v", err)
68
- return err
69
- }
70
- pc.charts = charts
71
-
72
- pc.ports = pc.initPorts()
81
+ pc.tcpPorts, pc.udpPorts = pc.initPorts()
82
83
pc.Debugf("using host: %s", pc.Host)
75
- pc.Debugf("using ports: %v", pc.Ports)
76
- pc.Debugf("using TCP connection timeout: %s", pc.Timeout)
84
+ pc.Debugf("using ports: tcp %v udp %v", pc.Ports, pc.UDPPorts)
85
+ pc.Debugf("using connection timeout: %s", pc.Timeout)
86
87
return nil
88
}
89
90
func (pc *PortCheck) Check() error {
91
+ mx, err := pc.collect()
92
+ if err != nil {
93
+ return err
94
+ }
95
+
96
+ if len(mx) == 0 {
97
+ return errors.New("no metrics collected")
98
+ }
99
+
100
return nil
101
}
102
@@ -95,6 +113,7 @@ func (pc *PortCheck) Collect() map[string]int64 {
113
if len(mx) == 0 {
114
return nil
115
}
116
+
117
return mx
118
}
119
src/go/plugin/go.d/modules/portcheck/portcheck_test.go
+46
-47
@@ -40,7 +40,7 @@ func TestPortCheck_Init(t *testing.T) {
40
job.Host = "127.0.0.1"
41
job.Ports = []int{39001, 39002}
42
assert.NoError(t, job.Init())
43
- assert.Len(t, job.ports, 2)
43
+ assert.Len(t, job.tcpPorts, 2)
44
}
45
func TestPortCheck_InitNG(t *testing.T) {
46
job := New()
@@ -53,7 +53,7 @@ func TestPortCheck_InitNG(t *testing.T) {
53
}
54
55
func TestPortCheck_Check(t *testing.T) {
56
- assert.NoError(t, New().Check())
56
+ assert.Error(t, New().Check())
57
}
58
59
func TestPortCheck_Cleanup(t *testing.T) {
@@ -65,7 +65,6 @@ func TestPortCheck_Charts(t *testing.T) {
65
job.Ports = []int{1, 2}
66
job.Host = "localhost"
67
require.NoError(t, job.Init())
68
- assert.Len(t, *job.Charts(), len(chartsTmpl)*len(job.Ports))
68
}
69
70
func TestPortCheck_Collect(t *testing.T) {
@@ -74,7 +73,7 @@ func TestPortCheck_Collect(t *testing.T) {
73
job.Host = "127.0.0.1"
74
job.Ports = []int{39001, 39002}
75
job.UpdateEvery = 5
77
- job.dial = testDial(nil)
76
+ job.dialTCP = testDial(nil)
77
require.NoError(t, job.Init())
78
require.NoError(t, job.Check())
79
@@ -87,16 +86,16 @@ func TestPortCheck_Collect(t *testing.T) {
86
}
87
88
expected := map[string]int64{
90
- "port_39001_current_state_duration": int64(job.UpdateEvery),
91
- "port_39001_failed": 0,
92
- "port_39001_latency": 0,
93
- "port_39001_success": 1,
94
- "port_39001_timeout": 0,
95
- "port_39002_current_state_duration": int64(job.UpdateEvery),
96
- "port_39002_failed": 0,
97
- "port_39002_latency": 0,
98
- "port_39002_success": 1,
99
- "port_39002_timeout": 0,
89
+ "tcp_port_39001_current_state_duration": int64(job.UpdateEvery * 2),
90
+ "tcp_port_39001_failed": 0,
91
+ "tcp_port_39001_latency": 0,
92
+ "tcp_port_39001_success": 1,
93
+ "tcp_port_39001_timeout": 0,
94
+ "tcp_port_39002_current_state_duration": int64(job.UpdateEvery * 2),
95
+ "tcp_port_39002_failed": 0,
96
+ "tcp_port_39002_latency": 0,
97
+ "tcp_port_39002_success": 1,
98
+ "tcp_port_39002_timeout": 0,
99
}
100
collected := job.Collect()
101
copyLatency(expected, collected)
@@ -104,54 +103,54 @@ func TestPortCheck_Collect(t *testing.T) {
103
assert.Equal(t, expected, collected)
104
105
expected = map[string]int64{
107
- "port_39001_current_state_duration": int64(job.UpdateEvery) * 2,
108
- "port_39001_failed": 0,
109
- "port_39001_latency": 0,
110
- "port_39001_success": 1,
111
- "port_39001_timeout": 0,
112
- "port_39002_current_state_duration": int64(job.UpdateEvery) * 2,
113
- "port_39002_failed": 0,
114
- "port_39002_latency": 0,
115
- "port_39002_success": 1,
116
- "port_39002_timeout": 0,
106
+ "tcp_port_39001_current_state_duration": int64(job.UpdateEvery) * 3,
107
+ "tcp_port_39001_failed": 0,
108
+ "tcp_port_39001_latency": 0,
109
+ "tcp_port_39001_success": 1,
110
+ "tcp_port_39001_timeout": 0,
111
+ "tcp_port_39002_current_state_duration": int64(job.UpdateEvery) * 3,
112
+ "tcp_port_39002_failed": 0,
113
+ "tcp_port_39002_latency": 0,
114
+ "tcp_port_39002_success": 1,
115
+ "tcp_port_39002_timeout": 0,
116
}
117
collected = job.Collect()
118
copyLatency(expected, collected)
119
120
assert.Equal(t, expected, collected)
121
123
- job.dial = testDial(errors.New("checkStateFailed"))
122
+ job.dialTCP = testDial(errors.New("checkStateFailed"))
123
124
expected = map[string]int64{
126
- "port_39001_current_state_duration": int64(job.UpdateEvery),
127
- "port_39001_failed": 1,
128
- "port_39001_latency": 0,
129
- "port_39001_success": 0,
130
- "port_39001_timeout": 0,
131
- "port_39002_current_state_duration": int64(job.UpdateEvery),
132
- "port_39002_failed": 1,
133
- "port_39002_latency": 0,
134
- "port_39002_success": 0,
135
- "port_39002_timeout": 0,
125
+ "tcp_port_39001_current_state_duration": int64(job.UpdateEvery),
126
+ "tcp_port_39001_failed": 1,
127
+ "tcp_port_39001_latency": 0,
128
+ "tcp_port_39001_success": 0,
129
+ "tcp_port_39001_timeout": 0,
130
+ "tcp_port_39002_current_state_duration": int64(job.UpdateEvery),
131
+ "tcp_port_39002_failed": 1,
132
+ "tcp_port_39002_latency": 0,
133
+ "tcp_port_39002_success": 0,
134
+ "tcp_port_39002_timeout": 0,
135
}
136
collected = job.Collect()
137
copyLatency(expected, collected)
138
139
assert.Equal(t, expected, collected)
140
142
- job.dial = testDial(timeoutError{})
141
+ job.dialTCP = testDial(timeoutError{})
142
143
expected = map[string]int64{
145
- "port_39001_current_state_duration": int64(job.UpdateEvery),
146
- "port_39001_failed": 0,
147
- "port_39001_latency": 0,
148
- "port_39001_success": 0,
149
- "port_39001_timeout": 1,
150
- "port_39002_current_state_duration": int64(job.UpdateEvery),
151
- "port_39002_failed": 0,
152
- "port_39002_latency": 0,
153
- "port_39002_success": 0,
154
- "port_39002_timeout": 1,
144
+ "tcp_port_39001_current_state_duration": int64(job.UpdateEvery),
145
+ "tcp_port_39001_failed": 0,
146
+ "tcp_port_39001_latency": 0,
147
+ "tcp_port_39001_success": 0,
148
+ "tcp_port_39001_timeout": 1,
149
+ "tcp_port_39002_current_state_duration": int64(job.UpdateEvery),
150
+ "tcp_port_39002_latency": 0,
151
+ "tcp_port_39002_success": 0,
152
+ "tcp_port_39002_timeout": 1,
153
+ "tcp_port_39002_failed": 0,
154
}
155
collected = job.Collect()
156
copyLatency(expected, collected)
@@ -159,7 +158,7 @@ func TestPortCheck_Collect(t *testing.T) {
158
assert.Equal(t, expected, collected)
159
}
160
162
-func testDial(err error) dialFunc {
161
+func testDial(err error) dialTCPFunc {
162
return func(_, _ string, _ time.Duration) (net.Conn, error) { return &net.TCPConn{}, err }
163
}
164
src/go/plugin/go.d/modules/portcheck/testdata/config.json
+3
@@ -4,5 +4,8 @@
4
"ports": [
5
123
6
],
7
+ "udp_ports": [
8
+ 123
9
+ ],
10
"timeout": 123.123
11
}
src/go/plugin/go.d/modules/portcheck/testdata/config.yaml
+2
@@ -2,4 +2,6 @@ update_every: 123
2
host: "ok"
3
ports:
4
- 123
5
+udp_ports:
6
+ - 123
7
timeout: 123.123