107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
package monitor
|
|
|
|
import "context"
|
|
import "fmt"
|
|
import "net"
|
|
import "os"
|
|
import "time"
|
|
|
|
import "golang.org/x/net/icmp"
|
|
import "golang.org/x/net/ipv4"
|
|
import "golang.org/x/net/ipv6"
|
|
|
|
import "codit/internal/models"
|
|
|
|
// ICMP protocol numbers (IANA).
|
|
const protoICMPv4 int = 1
|
|
const protoICMPv6 int = 58
|
|
|
|
// checkPing sends a single ICMP echo request using an unprivileged datagram
|
|
// socket (Linux net.ipv4.ping_group_range). If the socket cannot be opened
|
|
// (capability not granted) it reports a clear down state suggesting a TCP
|
|
// check instead, rather than requiring CAP_NET_RAW.
|
|
func checkPing(ctx context.Context, policy EgressPolicy, m models.Monitor, productID string) models.MonitorCheckResult {
|
|
var host string
|
|
var ip net.IP
|
|
var isV4 bool
|
|
var network string
|
|
var listenAddr string
|
|
var conn *icmp.PacketConn
|
|
var msgType icmp.Type
|
|
var proto int
|
|
var replyType icmp.Type
|
|
var message icmp.Message
|
|
var wb []byte
|
|
var rb []byte
|
|
var dst *net.UDPAddr
|
|
var start time.Time
|
|
var deadline time.Time
|
|
var n int
|
|
var parsed *icmp.Message
|
|
var err error
|
|
|
|
host, _ = hostAndPort(m.Target, "")
|
|
if host == "" {
|
|
return failResult("ping target requires a host")
|
|
}
|
|
ip, err = policy.CheckHostPort(ctx, host)
|
|
if err != nil {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, Message: classifyDialError(err)}
|
|
}
|
|
|
|
isV4 = ip.To4() != nil
|
|
if isV4 {
|
|
network = "udp4"
|
|
listenAddr = "0.0.0.0"
|
|
msgType = ipv4.ICMPTypeEcho
|
|
replyType = ipv4.ICMPTypeEchoReply
|
|
proto = protoICMPv4
|
|
} else {
|
|
network = "udp6"
|
|
listenAddr = "::"
|
|
msgType = ipv6.ICMPTypeEchoRequest
|
|
replyType = ipv6.ICMPTypeEchoReply
|
|
proto = protoICMPv6
|
|
}
|
|
|
|
conn, err = icmp.ListenPacket(network, listenAddr)
|
|
if err != nil {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, Message: fmt.Sprintf("ICMP ping not permitted on this host (try a TCP check): %v", err)}
|
|
}
|
|
defer conn.Close()
|
|
|
|
message = icmp.Message{
|
|
Type: msgType,
|
|
Code: 0,
|
|
Body: &icmp.Echo{ID: os.Getpid() & 0xffff, Seq: 1, Data: []byte(productID + "-monitor")},
|
|
}
|
|
wb, err = message.Marshal(nil)
|
|
if err != nil {
|
|
return failResult(fmt.Sprintf("failed to build ICMP packet: %v", err))
|
|
}
|
|
|
|
deadline = time.Now().Add(checkTimeout(m))
|
|
_ = conn.SetDeadline(deadline)
|
|
dst = &net.UDPAddr{IP: ip}
|
|
start = time.Now()
|
|
_, err = conn.WriteTo(wb, dst)
|
|
if err != nil {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, Message: fmt.Sprintf("send failed: %v", err)}
|
|
}
|
|
|
|
rb = make([]byte, 1500)
|
|
for {
|
|
n, _, err = conn.ReadFrom(rb)
|
|
if err != nil {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, LatencyMs: time.Since(start).Milliseconds(), Message: "no echo reply (timed out)"}
|
|
}
|
|
parsed, err = icmp.ParseMessage(proto, rb[:n])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if parsed.Type == replyType {
|
|
return models.MonitorCheckResult{OK: true, State: models.MonitorStateUp, LatencyMs: time.Since(start).Milliseconds(), Message: fmt.Sprintf("echo reply from %s", ip.String())}
|
|
}
|
|
}
|
|
}
|