Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "rootless" ping #642

Merged
merged 2 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way is to do it in the systemd unit which starts blackbox_exporter:

[Service]
...
AmbientCapabilities=CAP_NET_RAW

* *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 {
brian-brazil marked this conversation as resolved.
Show resolved Hide resolved
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