SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
5.7 KB · 210 lines go
Raw Blame History
1// Package traceroute runs the system traceroute binary (default UDP probes, which works unprivileged on macOS)2// and parses its output into the protocol's Traceroute record.3//4//	traceroute -n -q 1 -w 2 -m 30 <ip>      (60 s process timeout)5//6// route_hash = sha1 hex over the "|"-joined hop IPs, unanswered hops kept as "*".7package traceroute89import (10	"bytes"11	"context"12	"crypto/sha1"13	"encoding/hex"14	"errors"15	"net"16	"os"17	"os/exec"18	"regexp"19	"runtime"20	"strconv"21	"strings"22	"time"2324	"internetpressure.io/probe-agent/internal/protocol"25)2627// ProcessTimeout bounds one traceroute run.28const ProcessTimeout = 60 * time.Second2930// MaxHops passed as -m.31const MaxHops = 303233// Checker runs traceroutes with the detected binary. Binary is "" when unavailable.34type Checker struct {35	Binary  string36	Binary6 string // traceroute6 on macOS (optional)37}3839// Detect locates the traceroute binary for this OS. Returns a Checker with Binary "" when none is found.40func Detect() *Checker {41	c := &Checker{}42	var candidates []string43	switch runtime.GOOS {44	case "darwin":45		candidates = []string{"/usr/sbin/traceroute"}46	default:47		candidates = []string{"/usr/bin/traceroute", "/usr/sbin/traceroute", "/bin/traceroute"}48	}49	for _, p := range candidates {50		if fi, err := os.Stat(p); err == nil && !fi.IsDir() {51			c.Binary = p52			break53		}54	}55	if c.Binary == "" {56		if p, err := exec.LookPath("traceroute"); err == nil {57			c.Binary = p58		}59	}60	if runtime.GOOS == "darwin" {61		if fi, err := os.Stat("/usr/sbin/traceroute6"); err == nil && !fi.IsDir() {62			c.Binary6 = "/usr/sbin/traceroute6"63		}64	}65	return c66}6768// Available reports whether traceroute can run.69func (c *Checker) Available() bool { return c != nil && c.Binary != "" }7071// Run resolves the target (pinned IP or first IPv4 of the hostname) and runs one traceroute.72func (c *Checker) Run(ctx context.Context, t protocol.Target) (protocol.Traceroute, error) {73	tr := protocol.Traceroute{TS: protocol.FormatTime(time.Now()), TargetID: t.TargetID, Hops: []protocol.Hop{}}74	if !c.Available() {75		return tr, errors.New("traceroute binary not available")76	}77	ip, err := resolve(ctx, t)78	if err != nil {79		return tr, err80	}81	tr.DestIP = ip.String()8283	ctx, cancel := context.WithTimeout(ctx, ProcessTimeout)84	defer cancel()85	bin, args := c.command(ip)86	cmd := exec.CommandContext(ctx, bin, args...)87	cmd.WaitDelay = 2 * time.Second88	var out, stderr bytes.Buffer89	cmd.Stdout = &out90	cmd.Stderr = &stderr91	runErr := cmd.Run()92	// Partial output is still useful when the process was killed at the deadline.93	if out.Len() == 0 && runErr != nil {94		msg := strings.TrimSpace(stderr.String())95		if msg == "" {96			msg = runErr.Error()97		}98		return tr, errors.New("traceroute: " + msg)99	}100	hops := Parse(out.String())101	Fill(&tr, hops)102	return tr, nil103}104105func (c *Checker) command(ip net.IP) (string, []string) {106	args := []string{"-n", "-q", "1", "-w", "2", "-m", strconv.Itoa(MaxHops)}107	if ip.To4() == nil {108		if c.Binary6 != "" {109			return c.Binary6, append(args, ip.String())110		}111		return c.Binary, append([]string{"-6"}, append(args, ip.String())...)112	}113	return c.Binary, append(args, ip.String())114}115116func resolve(ctx context.Context, t protocol.Target) (net.IP, error) {117	if s := t.FixedIP(); s != "" {118		if ip := net.ParseIP(s); ip != nil {119			return ip, nil120		}121		return nil, errors.New("invalid pinned ip")122	}123	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)124	defer cancel()125	addrs, err := net.DefaultResolver.LookupIPAddr(ctx, t.Hostname)126	if err != nil {127		return nil, err128	}129	for _, a := range addrs {130		if a.IP.To4() != nil {131			return a.IP, nil132		}133	}134	if len(addrs) > 0 {135		return addrs[0].IP, nil136	}137	return nil, errors.New("no address")138}139140// hopLine matches " 3  10.0.0.1  12.345 ms" and " 2  *" (with -q 1 there is one probe per line). Annotations141// such as " !H" or " !X" after the RTT are ignored. On Linux an unanswered hop may print as " 2  * " too.142var hopLine = regexp.MustCompile(`^\s*(\d+)\s+(\*|[0-9a-fA-F.:]+)(?:\s+(\d+(?:\.\d+)?)\s*ms)?`)143144// Parse converts raw traceroute stdout into hops, ordered by hop number. Lines that are not hops (the header145// line, warnings) are skipped. Missing hop numbers are filled as unanswered so the route hash is positional.146func Parse(out string) []protocol.Hop {147	byN := map[int]protocol.Hop{}148	maxN := 0149	for _, line := range strings.Split(out, "\n") {150		m := hopLine.FindStringSubmatch(line)151		if m == nil {152			continue153		}154		n, _ := strconv.Atoi(m[1])155		if n <= 0 || n > MaxHops {156			continue157		}158		h := protocol.Hop{N: n, IP: m[2]}159		if m[2] == "*" {160			h.RTTMs = nil161		} else if m[3] != "" {162			if v, err := strconv.ParseFloat(m[3], 64); err == nil {163				h.RTTMs = protocol.F(v)164			}165		}166		// If the same hop number appears twice (unusual with -q 1), keep the answered one.167		if prev, ok := byN[n]; ok && prev.IP != "*" && h.IP == "*" {168			continue169		}170		byN[n] = h171		if n > maxN {172			maxN = n173		}174	}175	hops := make([]protocol.Hop, 0, maxN)176	for n := 1; n <= maxN; n++ {177		if h, ok := byN[n]; ok {178			hops = append(hops, h)179		} else {180			hops = append(hops, protocol.Hop{N: n, IP: "*"})181		}182	}183	return hops184}185186// Fill computes hop_count, reached, total_ms and route_hash from hops.187func Fill(tr *protocol.Traceroute, hops []protocol.Hop) {188	tr.Hops = hops189	tr.HopCount = len(hops)190	ips := make([]string, len(hops))191	for i, h := range hops {192		ips[i] = h.IP193	}194	tr.RouteHash = RouteHash(ips)195	tr.Reached = len(hops) > 0 && hops[len(hops)-1].IP == tr.DestIP196	tr.TotalMs = nil197	for i := len(hops) - 1; i >= 0; i-- {198		if hops[i].RTTMs != nil {199			tr.TotalMs = protocol.F(*hops[i].RTTMs)200			break201		}202	}203}204205// RouteHash is sha1 hex over "ip1|ip2|*|ip4…".206func RouteHash(ips []string) string {207	sum := sha1.Sum([]byte(strings.Join(ips, "|")))208	return hex.EncodeToString(sum[:])209}210