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