spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
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