Add support for "rootless" ping (#642)
authorDavid Leadbeater <dgl@dgl.cx>
Tue, 16 Jun 2020 14:38:05 +0000 (15:38 +0100)
committerGitHub <noreply@github.com>
Tue, 16 Jun 2020 14:38:05 +0000 (15:38 +0100)
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 <dgl@dgl.cx>
CONFIGURATION.md
README.md
prober/icmp.go

index 0ab0d21c8fbe357414f148aa92f42e261bb697b8..f20707faae45c79bc8093fbf437f2fc19faa15b1 100644 (file)
@@ -217,7 +217,8 @@ validate_additional_rrs:
 # The source IP address.
 [ source_ip_address: <string> ]
 
-# 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: <boolean> | default = false ]
 
 # The size of the payload.
index 67fdbec04d86a946d19c3eabe0a4388c5e696d79..1fb8f3bfb41ce048aad8d8e3196d04468f8dedda 100644 (file)
--- 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/
index 565988dd562182c07064c7d9654ce75e22c2f98d..1a5668e0d0f157098d770f8e0c2b22b3fd7c53be 100644 (file)
@@ -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