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 probe_icmp_reply_ttl_total #694

Merged
merged 1 commit into from
Oct 8, 2020
Merged
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
183 changes: 99 additions & 84 deletions prober/icmp.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,21 @@ func getICMPSequence() uint16 {

func ProbeICMP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) {
var (
socket net.PacketConn
requestType icmp.Type
replyType icmp.Type
requestType icmp.Type
replyType icmp.Type
icmpConn *icmp.PacketConn
v4RawConn *ipv4.RawConn
hopLimitFlagSet bool = true

durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "probe_icmp_duration_seconds",
Help: "Duration of icmp request by phase",
}, []string{"phase"})

hopLimitGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_icmp_reply_hop_limit",
Help: "Replied packet hop limit (TTL for ipv4)",
})
)

for _, lv := range []string{"resolve", "setup", "rtt"} {
Expand All @@ -81,7 +88,8 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr

registry.MustRegister(durationGaugeVec)

ip, lookupTime, err := chooseProtocol(ctx, module.ICMP.IPProtocol, module.ICMP.IPProtocolFallback, target, registry, logger)
dstIPAddr, lookupTime, err := chooseProtocol(ctx, module.ICMP.IPProtocol, module.ICMP.IPProtocolFallback, target, registry, logger)

if err != nil {
level.Warn(logger).Log("msg", "Error resolving address", "err", err)
return false
Expand All @@ -104,15 +112,14 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
// Unprivileged sockets are supported on Darwin and Linux only.
tryUnprivileged := runtime.GOOS == "darwin" || runtime.GOOS == "linux"

if ip.IP.To4() == nil {
if dstIPAddr.IP.To4() == nil {
requestType = ipv6.ICMPTypeEchoRequest
replyType = ipv6.ICMPTypeEchoReply

if srcIP == nil {
srcIP = net.ParseIP("::")
}

var icmpConn *icmp.PacketConn
if tryUnprivileged {
// "udp" here means unprivileged -- not the protocol "udp".
icmpConn, err = icmp.ListenPacket("udp6", srcIP.String())
Expand All @@ -130,8 +137,12 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
return
}
}
defer icmpConn.Close()

socket = icmpConn
if err := icmpConn.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true); err != nil {
level.Debug(logger).Log("msg", "Failed to set Control Message for retrieving Hop Limit", "err", err)
hopLimitFlagSet = false
}
} else {
requestType = ipv4.ICMPTypeEcho
replyType = ipv4.ICMPTypeEchoReply
Expand All @@ -143,21 +154,25 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
if module.ICMP.DontFragment {
// 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.
icmpConn, err := net.ListenPacket("ip4:icmp", srcIP.String())
netConn, err := net.ListenPacket("ip4:icmp", srcIP.String())
if err != nil {
level.Error(logger).Log("msg", "Error listening to socket", "err", err)
return
}
defer netConn.Close()

rc, err := ipv4.NewRawConn(icmpConn)
v4RawConn, err = ipv4.NewRawConn(netConn)
if err != nil {
level.Error(logger).Log("msg", "Error creating raw connection", "err", err)
return
}
socket = &v4Conn{c: rc, df: true}
} else {
var icmpConn *icmp.PacketConn
defer v4RawConn.Close()

if err := v4RawConn.SetControlMessage(ipv4.FlagTTL, true); err != nil {
level.Debug(logger).Log("msg", "Failed to set Control Message for retrieving TTL", "err", err)
hopLimitFlagSet = false
}
} else {
if tryUnprivileged {
icmpConn, err = icmp.ListenPacket("udp4", srcIP.String())
if err != nil {
Expand All @@ -174,16 +189,18 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
return
}
}
defer icmpConn.Close()

socket = icmpConn
if err := icmpConn.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true); err != nil {
level.Debug(logger).Log("msg", "Failed to set Control Message for retrieving TTL", "err", err)
hopLimitFlagSet = false
}
}
}

defer socket.Close()

var dst net.Addr = ip
var dst net.Addr = dstIPAddr
if !privileged {
dst = &net.UDPAddr{IP: ip.IP, Zone: ip.Zone}
dst = &net.UDPAddr{IP: dstIPAddr.IP, Zone: dstIPAddr.Zone}
}

var data []byte
Expand Down Expand Up @@ -215,7 +232,26 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
durationGaugeVec.WithLabelValues("setup").Add(time.Since(setupStart).Seconds())
level.Info(logger).Log("msg", "Writing out packet")
rttStart := time.Now()
if _, err = socket.WriteTo(wb, dst); err != nil {

if icmpConn != nil {
_, err = icmpConn.WriteTo(wb, dst)
} else {
// Only for IPv4 raw. Needed for setting DontFragment flag.
header := &ipv4.Header{
luizluca marked this conversation as resolved.
Show resolved Hide resolved
Version: ipv4.Version,
Len: ipv4.HeaderLen,
Protocol: 1,
TotalLen: ipv4.HeaderLen + len(wb),
TTL: 64,
Dst: dstIPAddr.IP,
Src: srcIP,
}

header.Flags |= ipv4.DontFragment

err = v4RawConn.WriteTo(header, wb, nil)
}
if err != nil {
level.Warn(logger).Log("msg", "Error writing to socket", "err", err)
return
}
Expand Down Expand Up @@ -243,13 +279,52 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr

rb := make([]byte, 65536)
deadline, _ := ctx.Deadline()
if err := socket.SetReadDeadline(deadline); err != nil {
if icmpConn != nil {
err = icmpConn.SetReadDeadline(deadline)
} else {
err = v4RawConn.SetReadDeadline(deadline)
}
if err != nil {
level.Error(logger).Log("msg", "Error setting socket deadline", "err", err)
return
}
level.Info(logger).Log("msg", "Waiting for reply packets")
for {
n, peer, err := socket.ReadFrom(rb)
var n int
var peer net.Addr
var err error
var hopLimit float64 = -1
brian-brazil marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

This is still messy, as there's now two hasHopLimit indicators doing different things. It'd be better to name the bool what is actually, namely whether the CM was set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The real issue is that go does not tell me if hopLimit is missing or zero. I opened an issue golang/go#41820.

I renamed the bool to ipv6HopLimitFlagSet and now I use it only for IPv6. If golang fixes the issue, it can be removed in the future to adapt to whatever go uses to inform hopLimit is missing.

Another solution is to assume that hopLimit will never be 0 in a echo-reply. I assumed that hop limit could be zero when it is using the last permitted hop. However, I was wrong as a packet with hopLimit==1 will not get forwarded. The IPv6 echo reply will only be 0 if the target OS is explicitly setting the answer to be 0 (maybe a way to avoid its traffic to be routed?). As it is a very very special case, we might ignore it.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably best not to make assumptions about the TTL in a tool like this, as if that weird situation does happen you wouldn't want to lead users down the wrong path. It's probably best to use the flag for both protocols.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. Let's play safe. I reintroduced the bool check for both.


if dstIPAddr.IP.To4() == nil {
var cm *ipv6.ControlMessage
n, cm, peer, err = icmpConn.IPv6PacketConn().ReadFrom(rb)
// HopLimit == 0 is valid for IPv6, although go initialize it as 0.
if cm != nil && hopLimitFlagSet {
hopLimit = float64(cm.HopLimit)
} else {
level.Debug(logger).Log("msg", "Cannot get Hop Limit from the received packet. 'probe_icmp_reply_hop_limit' will be missing.")
}
} else {
var cm *ipv4.ControlMessage
if icmpConn != nil {
n, cm, peer, err = icmpConn.IPv4PacketConn().ReadFrom(rb)
} else {
var h *ipv4.Header
var p []byte
h, p, cm, err = v4RawConn.ReadFrom(rb)
if err == nil {
copy(rb, p)
n = len(p)
peer = &net.IPAddr{IP: h.Src}
}
}
if cm != nil && hopLimitFlagSet {
// Not really Hop Limit, but it is in practice.
hopLimit = float64(cm.TTL)
} else {
level.Debug(logger).Log("msg", "Cannot get TTL from the received packet. 'probe_icmp_reply_hop_limit' will be missing.")
}
}
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
level.Warn(logger).Log("msg", "Timeout reading from socket", "err", err)
Expand All @@ -274,72 +349,12 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
}
if bytes.Equal(rb[:n], wb) {
durationGaugeVec.WithLabelValues("rtt").Add(time.Since(rttStart).Seconds())
if hopLimit >= 0 {
hopLimitGauge.Set(hopLimit)
registry.MustRegister(hopLimitGauge)
}
level.Info(logger).Log("msg", "Found matching reply packet")
return true
}
}
}

type v4Conn struct {
c *ipv4.RawConn

df bool
src net.IP
}

func (c *v4Conn) ReadFrom(b []byte) (int, net.Addr, error) {
h, p, _, err := c.c.ReadFrom(b)
if err != nil {
return 0, nil, err
}

copy(b, p)
n := len(b)
if len(p) < len(b) {
n = len(p)
}
return n, &net.IPAddr{IP: h.Src}, nil
}

func (d *v4Conn) WriteTo(b []byte, addr net.Addr) (int, error) {
ipAddr, err := net.ResolveIPAddr(addr.Network(), addr.String())
if err != nil {
return 0, err
}

header := &ipv4.Header{
Version: ipv4.Version,
Len: ipv4.HeaderLen,
Protocol: 1,
TotalLen: ipv4.HeaderLen + len(b),
TTL: 64,
Dst: ipAddr.IP,
Src: d.src,
}

if d.df {
header.Flags |= ipv4.DontFragment
}

return len(b), d.c.WriteTo(header, b, nil)
}

func (d *v4Conn) Close() error {
return d.c.Close()
}

func (d *v4Conn) LocalAddr() net.Addr {
return nil
}

func (d *v4Conn) SetDeadline(t time.Time) error {
return d.c.SetDeadline(t)
}

func (d *v4Conn) SetReadDeadline(t time.Time) error {
return d.c.SetReadDeadline(t)
}

func (d *v4Conn) SetWriteDeadline(t time.Time) error {
return d.c.SetWriteDeadline(t)
}