From f5d07a11d83936ad702ee46f68f041b2ff064e5f Mon Sep 17 00:00:00 2001 From: Luiz Angelo Daros de Luca Date: Fri, 25 Sep 2020 03:41:06 -0300 Subject: [PATCH] Add probe_icmp_reply_hop_limit IPv6 hop limit (IPv4 TTL) tells how many remaining hops the reply datagram has. When the target uses a well known initial hop limit, it can be used to count the number of hops from the target to the prober and detect routing problems. Signed-off-by: Luiz Angelo Daros de Luca --- prober/icmp.go | 160 ++++++++++++++++++++++++------------------------- 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/prober/icmp.go b/prober/icmp.go index 1e3410a83..4d3441153 100644 --- a/prober/icmp.go +++ b/prober/icmp.go @@ -65,14 +65,20 @@ 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 + icmpConn *icmp.PacketConn + rawConn *ipv4.RawConn durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "probe_icmp_duration_seconds", Help: "Duration of icmp request by phase", }, []string{"phase"}) + + ttlGauge = 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"} { @@ -80,8 +86,10 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr } registry.MustRegister(durationGaugeVec) + registry.MustRegister(ttlGauge) + + dstIPAddr, lookupTime, err := chooseProtocol(ctx, module.ICMP.IPProtocol, module.ICMP.IPProtocolFallback, target, registry, logger) - ip, 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 @@ -104,7 +112,7 @@ 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 @@ -112,7 +120,6 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr srcIP = net.ParseIP("::") } - var icmpConn *icmp.PacketConn if tryUnprivileged { // "udp" here means unprivileged -- not the protocol "udp". icmpConn, err = icmp.ListenPacket("udp6", srcIP.String()) @@ -131,7 +138,10 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr } } - 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) + } + defer icmpConn.Close() } else { requestType = ipv4.ICMPTypeEcho replyType = ipv4.ICMPTypeEchoReply @@ -143,21 +153,23 @@ 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 } - rc, err := ipv4.NewRawConn(icmpConn) + level.Info(logger).Log("msg", "Creating RawConn") + rawConn, 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} + if err := rawConn.SetControlMessage(ipv4.FlagTTL, true); err != nil { + level.Debug(logger).Log("msg", "Failed to set Control Message for retrieving TTL", "err", err) + } + defer rawConn.Close() } else { - var icmpConn *icmp.PacketConn - if tryUnprivileged { icmpConn, err = icmp.ListenPacket("udp4", srcIP.String()) if err != nil { @@ -174,16 +186,17 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr return } } - - 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) + } + defer icmpConn.Close() } } - 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 @@ -215,7 +228,25 @@ 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 { + header := &ipv4.Header{ + 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 = rawConn.WriteTo(header, wb, nil) + } + if err != nil { level.Warn(logger).Log("msg", "Error writing to socket", "err", err) return } @@ -243,13 +274,41 @@ 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 = rawConn.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 ttl int + + if dstIPAddr.IP.To4() == nil { + var cm *ipv6.ControlMessage + n, cm, peer, err = icmpConn.IPv6PacketConn().ReadFrom(rb) + ttl = cm.HopLimit + } else { + if icmpConn != nil { + var cm *ipv4.ControlMessage + n, cm, peer, err = icmpConn.IPv4PacketConn().ReadFrom(rb) + ttl = cm.TTL + } else { + h, p, cm, err := rawConn.ReadFrom(rb) + if err == nil { + copy(rb, p) + n = len(p) + peer = &net.IPAddr{IP: h.Src} + } + ttl = cm.TTL + } + } if err != nil { if nerr, ok := err.(net.Error); ok && nerr.Timeout() { level.Warn(logger).Log("msg", "Timeout reading from socket", "err", err) @@ -274,72 +333,9 @@ 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()) + ttlGauge.Set(float64(ttl)) 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) -}