Files

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())}
}
}
}