Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
162 строки
4.4 KiB
Go
162 строки
4.4 KiB
Go
// Package cudp provides UDP probe / port check functionality for RSMon.
|
|
//
|
|
// Semantics: UDP is a connectionless protocol so a successful connect
|
|
// (net.Dial("udp", ...)) only means the kernel resolved the route to
|
|
// host:port — the remote may silently drop the packet. The check
|
|
// therefore:
|
|
//
|
|
// 1. Resolves the address and opens a UDP "connection".
|
|
//
|
|
// 2. Writes a small probe packet with the dial deadline active.
|
|
//
|
|
// 3. Sets a short read deadline and waits for any reply.
|
|
//
|
|
// - OK is reported only when a reply is received from the server.
|
|
// - ERR is reported when the dial itself fails (refused, network
|
|
// unreachable, no route, ...).
|
|
// - WARN is reported when no reply is received within the deadline
|
|
// because that is the most common UDP behavior for a real
|
|
// service that isn't echoing probes; we still consider it
|
|
// "monitoring" — the route is reachable — but flag it as worth
|
|
// investigating.
|
|
//
|
|
// Settings consumed from models.CheckSettings:
|
|
// - port (string): UDP port to probe. Defaults to 53 when empty.
|
|
// - timeout (int, seconds): per-probe budget. Clamped to >= 1s.
|
|
// - host (string): optional override of the monitor host.
|
|
package cudp
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"rocketgit.ru/rsmon/worker/app/models"
|
|
)
|
|
|
|
const (
|
|
stateOK = "OK"
|
|
stateERR = "ERR"
|
|
stateWARN = "WARN"
|
|
|
|
defaultTimeout = 5 * time.Second
|
|
minTimeout = 1 * time.Second
|
|
|
|
probeSize = 16
|
|
)
|
|
|
|
var probe = []byte("rsmon-udp-probe")
|
|
|
|
// Perform executes a single UDP probe check.
|
|
func Perform(c *models.Check) *Result {
|
|
r := &Result{}
|
|
settings := c.GetSettings()
|
|
|
|
host := c.Monitor.Host
|
|
if settings.Host != "" {
|
|
host = settings.Host
|
|
}
|
|
if host == "" {
|
|
r.State = stateERR
|
|
r.Error = fmt.Errorf("udp: empty host")
|
|
return r
|
|
}
|
|
|
|
port := settings.Port
|
|
if port == "" {
|
|
port = "53"
|
|
}
|
|
|
|
timeout := time.Duration(settings.Timeout) * time.Second
|
|
if timeout <= 0 {
|
|
timeout = defaultTimeout
|
|
}
|
|
if timeout < minTimeout {
|
|
timeout = minTimeout
|
|
}
|
|
|
|
addr := net.JoinHostPort(host, port)
|
|
|
|
dialer := &net.Dialer{Timeout: timeout, DualStack: true}
|
|
start := time.Now()
|
|
conn, err := dialer.Dial("udp", addr)
|
|
if err != nil {
|
|
r.Duration = time.Since(start)
|
|
r.State = stateERR
|
|
r.Error = err
|
|
return r
|
|
}
|
|
defer conn.Close() //nolint:errcheck // accepted lint exception: best-effort close
|
|
|
|
r.RemoteAddr = conn.RemoteAddr().String()
|
|
|
|
if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
|
|
r.Duration = time.Since(start)
|
|
r.State = stateERR
|
|
r.Error = fmt.Errorf("set write deadline: %w", err)
|
|
return r
|
|
}
|
|
if _, err := conn.Write(probe[:minInt(probeSize, len(probe))]); err != nil {
|
|
r.Duration = time.Since(start)
|
|
r.State = stateERR
|
|
r.Error = fmt.Errorf("write probe: %w", err)
|
|
return r
|
|
}
|
|
|
|
// If the dial succeeded, give the read a fraction of the budget
|
|
// so the whole check still fits inside Settings.Timeout.
|
|
readBudget := timeout
|
|
if readBudget > 2*time.Second {
|
|
readBudget = 2 * time.Second
|
|
}
|
|
if err := conn.SetReadDeadline(time.Now().Add(readBudget)); err != nil {
|
|
r.Duration = time.Since(start)
|
|
r.State = stateERR
|
|
r.Error = fmt.Errorf("set read deadline: %w", err)
|
|
return r
|
|
}
|
|
buf := make([]byte, 1500)
|
|
_, readErr := conn.Read(buf)
|
|
r.Duration = time.Since(start)
|
|
|
|
switch {
|
|
case readErr == nil:
|
|
r.State = stateOK
|
|
r.Infos = append(r.Infos, fmt.Sprintf("udp %s replied in %.2fms", addr, float64(r.Duration.Microseconds())/1000.0))
|
|
case isTimeout(readErr):
|
|
// UDP services rarely echo unrecognized probes back. Reaching
|
|
// the host without a reply is meaningful, but ambiguous: the
|
|
// service might be working or firewalled. Surface as WARN.
|
|
r.State = stateWARN
|
|
r.Warnings = append(r.Warnings, fmt.Sprintf("udp %s reachable but no reply within %s", addr, readBudget))
|
|
default:
|
|
// Any other read error (connection reset, ...) still means the
|
|
// kernel could route the packet; report as ERR so operators
|
|
// know to investigate the service.
|
|
r.State = stateERR
|
|
r.Error = readErr
|
|
}
|
|
return r
|
|
}
|
|
|
|
func isTimeout(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
type timeout interface{ Timeout() bool }
|
|
if t, ok := err.(timeout); ok {
|
|
return t.Timeout()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// minInt mirrors the standard library min() for ints without taking
|
|
// a dependency on Go 1.21+. We keep a custom name to avoid clashing
|
|
// with the built-in and being flagged by the linter.
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|