Skip to content

Commit

Permalink
Add support for "rootless" ping (#642)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dgl authored Jun 16, 2020
1 parent 9bbe703 commit 3007522
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 16 deletions.
3 changes: 2 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
84 changes: 72 additions & 12 deletions prober/icmp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"math/rand"
"net"
"os"
"runtime"
"sync"
"time"

Expand Down Expand Up @@ -98,17 +99,36 @@ 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

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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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
Expand Down

0 comments on commit 3007522

Please sign in to comment.