1/*
2** Zabbix
3** Copyright (C) 2001-2021 Zabbix SIA
4**
5** This program is free software; you can redistribute it and/or modify
6** it under the terms of the GNU General Public License as published by
7** the Free Software Foundation; either version 2 of the License, or
8** (at your option) any later version.
9**
10** This program is distributed in the hope that it will be useful,
11** but WITHOUT ANY WARRANTY; without even the implied warranty of
12** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13** GNU General Public License for more details.
14**
15** You should have received a copy of the GNU General Public License
16** along with this program; if not, write to the Free Software
17** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18**/
19
20package udp
21
22import (
23	"bytes"
24	"errors"
25	"math"
26	"net"
27	"strconv"
28	"time"
29
30	"zabbix.com/pkg/conf"
31	"zabbix.com/pkg/log"
32	"zabbix.com/pkg/plugin"
33)
34
35const (
36	errorInvalidFirstParam = "Invalid first parameter."
37	errorInvalidThirdParam = "Invalid third parameter."
38	errorTooManyParams     = "Too many parameters."
39	errorUnsupportedMetric = "Unsupported metric."
40)
41
42const (
43	ntpVersion          = 3
44	ntpModeServer       = 4
45	ntpTransmitRspStart = 24
46	ntpTransmitRspEnd   = 32
47	ntpTransmitReqStart = 40
48	ntpPacketSize       = 48
49	ntpEpochOffset      = 2208988800.0
50	ntpScale            = 4294967296.0
51)
52
53type Options struct {
54	Timeout  time.Duration `conf:"optional,range=1:30"`
55	Capacity int           `conf:"optional,range=1:100"`
56}
57
58// Plugin -
59type Plugin struct {
60	plugin.Base
61	options Options
62}
63
64var impl Plugin
65
66func (p *Plugin) createRequest(req []byte) {
67	// NTP configure request settings by specifying the first byte as
68	// 00 011 011 (or 0x1B)
69	// |  |   +-- client mode (3)
70	// |  + ----- version (3)
71	// + -------- leap year indicator, 0 no warning
72	req[0] = 0x1B
73
74	transmitTime := time.Now().Unix() + ntpEpochOffset
75	f := float64(transmitTime) / ntpScale
76
77	for i := 0; i < 8; i++ {
78		f *= 256.0
79		k := int64(f)
80
81		if k >= 256 {
82			k = 255
83		}
84
85		req[ntpTransmitReqStart+i] = byte(k)
86		f -= float64(k)
87	}
88}
89
90func (p *Plugin) validateResponse(rsp []byte, ln int, req []byte) int {
91	if ln != ntpPacketSize {
92		log.Debugf("invalid response size: %d", ln)
93		return 0
94	}
95
96	if !bytes.Equal(req[ntpTransmitReqStart:], rsp[ntpTransmitRspStart:ntpTransmitRspEnd]) {
97		log.Debugf("originate timestamp in the response does not match transmit timestamp in the request: 0x%x 0x%x",
98			rsp[ntpTransmitRspStart:ntpTransmitRspEnd], req[40:])
99
100		return 0
101	}
102
103	version := (rsp[0] >> 3) & 7
104	if version != ntpVersion {
105		log.Debugf("invalid NTP version in the response: %d", version)
106		return 0
107	}
108
109	mode := rsp[0] & 7
110	if mode != ntpModeServer {
111		log.Debugf("invalid mode in the response: %d", mode)
112		return 0
113	}
114
115	if 15 < rsp[1] {
116		log.Debugf("invalid stratum in the response: %d", rsp[1])
117		return 0
118	}
119
120	var f float64
121	for i := 0; i < 8; i++ {
122		f = 256*f + float64(rsp[40+i])
123	}
124
125	transmit := f / ntpScale
126	if transmit == 0 {
127		log.Debugf("invalid transmit timestamp in the response: %v", transmit)
128		return 0
129	}
130
131	return 1
132}
133
134func (p *Plugin) udpExpect(service string, address string) (result int) {
135	var conn net.Conn
136	var err error
137
138	if conn, err = net.DialTimeout("udp", address, time.Second*p.options.Timeout); err != nil {
139		log.Debugf("UDP expect network error: cannot connect to [%s]: %s", address, err.Error())
140		return
141	}
142	defer conn.Close()
143
144	if err = conn.SetDeadline(time.Now().Add(time.Second * p.options.Timeout)); err != nil {
145		return
146	}
147
148	req := make([]byte, ntpPacketSize)
149	p.createRequest(req)
150
151	if _, err = conn.Write(req); err != nil {
152		log.Debugf("UDP expect network error: cannot write to [%s]: %s", address, err.Error())
153		return
154	}
155
156	var ln int
157	rsp := make([]byte, ntpPacketSize)
158
159	if ln, err = conn.Read(rsp); err != nil {
160		log.Debugf("UDP expect network error: cannot read from [%s]: %s", address, err.Error())
161		return
162	}
163
164	return p.validateResponse(rsp, ln, req)
165}
166
167func (p *Plugin) exportNetService(params []string) int {
168	var ip, port string
169	service := params[0]
170
171	if len(params) > 1 && params[1] != "" {
172		ip = params[1]
173	} else {
174		ip = "127.0.0.1"
175	}
176
177	if len(params) == 3 && params[2] != "" {
178		port = params[2]
179	} else {
180		port = service
181	}
182
183	return p.udpExpect(service, net.JoinHostPort(ip, port))
184}
185
186func toFixed(num float64, precision int) float64 {
187	output := math.Pow(10, float64(precision))
188	return math.Round(num*output) / output
189}
190
191func (p *Plugin) exportNetServicePerf(params []string) float64 {
192	const floatPrecision = 0.0001
193
194	start := time.Now()
195	ret := p.exportNetService(params)
196
197	if ret == 1 {
198		elapsedTime := toFixed(time.Since(start).Seconds(), 6)
199
200		if elapsedTime < floatPrecision {
201			elapsedTime = floatPrecision
202		}
203		return elapsedTime
204	}
205	return 0.0
206}
207
208// Export -
209func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
210	switch key {
211	case "net.udp.service", "net.udp.service.perf":
212		if len(params) > 3 {
213			err = errors.New(errorTooManyParams)
214			return
215		}
216		if len(params) < 1 || (len(params) == 1 && len(params[0]) == 0) {
217			err = errors.New(errorInvalidFirstParam)
218			return
219		}
220		if params[0] != "ntp" {
221			err = errors.New(errorInvalidFirstParam)
222			return
223		}
224
225		if len(params) == 3 && len(params[2]) != 0 {
226			if _, err = strconv.ParseUint(params[2], 10, 16); err != nil {
227				err = errors.New(errorInvalidThirdParam)
228				return
229			}
230		}
231
232		if key == "net.udp.service" {
233			return p.exportNetService(params), nil
234		} else if key == "net.udp.service.perf" {
235			return p.exportNetServicePerf(params), nil
236		}
237	}
238
239	/* SHOULD_NEVER_HAPPEN */
240	return nil, errors.New(errorUnsupportedMetric)
241}
242
243func (p *Plugin) Configure(global *plugin.GlobalOptions, options interface{}) {
244	if err := conf.Unmarshal(options, &p.options); err != nil {
245		p.Warningf("cannot unmarshal configuration options: %s", err)
246	}
247	if p.options.Timeout == 0 {
248		p.options.Timeout = time.Duration(global.Timeout)
249	}
250}
251
252func (p *Plugin) Validate(options interface{}) error {
253	var o Options
254	return conf.Unmarshal(options, &o)
255}
256
257func init() {
258	plugin.RegisterMetrics(&impl, "UDP",
259		"net.udp.service", "Checks if service is running and responding to UDP requests.",
260		"net.udp.service.perf", "Checks performance of UDP service.")
261}
262