From 3007522d1435628a00f86d7fe62ad4d24caba715 Mon Sep 17 00:00:00 2001 From: David Leadbeater Date: Tue, 16 Jun 2020 15:38:05 +0100 Subject: [PATCH] Add support for "rootless" ping (#642) This works for Linux and Darwin. On Linux the user running the exporter needs to be a member of a group with an ID in the range specified in the sysctl net.ipv4.ping_group_range. Signed-off-by: David Leadbeater --- CONFIGURATION.md | 3 +- README.md | 13 ++++++-- prober/icmp.go | 84 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 0ab0d21..f20707f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -217,7 +217,8 @@ validate_additional_rrs: # The source IP address. [ source_ip_address: ] -# Set the DF-bit in the IP-header. Only works with ip4 and on *nix systems. +# Set the DF-bit in the IP-header. Only works with ip4, on *nix systems and +# requires raw sockets (i.e. root or CAP_NET_RAW on Linux). [ dont_fragment: | default = false ] # The size of the payload. diff --git a/README.md b/README.md index 67fdbec..1fb8f3b 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,16 @@ scrape_configs: The ICMP probe requires elevated privileges to function: * *Windows*: Administrator privileges are required. -* *Linux*: root user _or_ `CAP_NET_RAW` capability is required. - * Can be set by executing `setcap cap_net_raw+ep blackbox_exporter` -* *BSD / OS X*: root user is required. +* *Linux*: either a user with a group within `net.ipv4.ping_group_range`, the + `CAP_NET_RAW` capability or the root user is required. + * Your distribution may configure `net.ipv4.ping_group_range` by default in + `/etc/sysctl.conf` or similar. If not you can set + `net.ipv4.ping_group_range = 0 2147483647` to allow any user the ability + to use ping. + * Alternatively the capability can be set by executing `setcap cap_net_raw+ep + blackbox_exporter` +* *BSD*: root user is required. +* *OS X*: No additional privileges are needed. [circleci]: https://circleci.com/gh/prometheus/blackbox_exporter [hub]: https://hub.docker.com/r/prom/blackbox-exporter/ diff --git a/prober/icmp.go b/prober/icmp.go index 565988d..1a5668e 100644 --- a/prober/icmp.go +++ b/prober/icmp.go @@ -19,6 +19,7 @@ import ( "math/rand" "net" "os" + "runtime" "sync" "time" @@ -98,6 +99,11 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr setupStart := time.Now() level.Info(logger).Log("msg", "Creating socket") + + unprivileged := false + // Unprivileged sockets are supported on Darwin and Linux only. + tryUnprivileged := runtime.GOOS == "darwin" || runtime.GOOS == "linux" + if ip.IP.To4() == nil { requestType = ipv6.ICMPTypeEchoRequest replyType = ipv6.ICMPTypeEchoReply @@ -105,10 +111,24 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr if srcIP == nil { srcIP = net.ParseIP("::") } - icmpConn, err := icmp.ListenPacket("ip6:ipv6-icmp", srcIP.String()) - if err != nil { - level.Error(logger).Log("msg", "Error listening to socket", "err", err) - return + + var icmpConn *icmp.PacketConn + if tryUnprivileged { + // "udp" here means unprivileged -- not the protocol "udp". + icmpConn, err = icmp.ListenPacket("udp6", srcIP.String()) + if err != nil { + level.Debug(logger).Log("msg", "Unable to do unprivileged listen on socket, will attempt privileged", "err", err) + } else { + unprivileged = true + } + } + + if !unprivileged { + icmpConn, err = icmp.ListenPacket("ip6:ipv6-icmp", srcIP.String()) + if err != nil { + level.Error(logger).Log("msg", "Error listening to socket", "err", err) + return + } } socket = icmpConn @@ -119,10 +139,25 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr if srcIP == nil { srcIP = net.ParseIP("0.0.0.0") } - icmpConn, err := net.ListenPacket("ip4:icmp", srcIP.String()) - if err != nil { - level.Error(logger).Log("msg", "Error listening to socket", "err", err) - return + + var icmpConn *icmp.PacketConn + // If the user has set the don't fragment option we cannot use unprivileged + // sockets as it is not possible to set IP header level options. + if tryUnprivileged && !module.ICMP.DontFragment { + icmpConn, err = icmp.ListenPacket("udp4", srcIP.String()) + if err != nil { + level.Debug(logger).Log("msg", "Unable to do unprivileged listen on socket, will attempt privileged", "err", err) + } else { + unprivileged = true + } + } + + if !unprivileged { + icmpConn, err = icmp.ListenPacket("ip4:icmp", srcIP.String()) + if err != nil { + level.Error(logger).Log("msg", "Error listening to socket", "err", err) + return + } } if module.ICMP.DontFragment { @@ -139,6 +174,11 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr defer socket.Close() + var dst net.Addr = ip + if unprivileged { + dst = &net.UDPAddr{IP: ip.IP, Zone: ip.Zone} + } + var data []byte if module.ICMP.PayloadSize != 0 { data = make([]byte, module.ICMP.PayloadSize) @@ -164,22 +204,36 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr level.Error(logger).Log("msg", "Error marshalling packet", "err", err) return } + durationGaugeVec.WithLabelValues("setup").Add(time.Since(setupStart).Seconds()) level.Info(logger).Log("msg", "Writing out packet") rttStart := time.Now() - if _, err = socket.WriteTo(wb, ip); err != nil { + if _, err = socket.WriteTo(wb, dst); err != nil { level.Warn(logger).Log("msg", "Error writing to socket", "err", err) return } - // Reply should be the same except for the message type. + // Reply should be the same except for the message type and ID if + // unprivileged sockets were used and the kernel used its own. wm.Type = replyType + // Unprivileged cannot set IDs on Linux. + idUnknown := unprivileged && runtime.GOOS == "linux" + if idUnknown { + body.ID = 0 + } wb, err = wm.Marshal(nil) if err != nil { level.Error(logger).Log("msg", "Error marshalling packet", "err", err) return } + if idUnknown { + // If the ID is unknown (due to unprivileged sockets) we also cannot know + // the checksum in userspace. + wb[2] = 0 + wb[3] = 0 + } + rb := make([]byte, 65536) deadline, _ := ctx.Deadline() if err := socket.SetReadDeadline(deadline); err != nil { @@ -197,10 +251,16 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr level.Error(logger).Log("msg", "Error reading from socket", "err", err) continue } - if peer.String() != ip.String() { + if peer.String() != dst.String() { continue } - if replyType == ipv6.ICMPTypeEchoReply { + if idUnknown { + // Clear the ID from the packet, as the kernel will have replaced it (and + // kept track of our packet for us, hence clearing is safe). + rb[4] = 0 + rb[5] = 0 + } + if idUnknown || replyType == ipv6.ICMPTypeEchoReply { // Clear checksum to make comparison succeed. rb[2] = 0 rb[3] = 0 -- 2.25.1