package traceroute import ( "context" "testing" "internetpressure.io/probe-agent/internal/protocol" ) // Real macOS output (/usr/sbin/traceroute -n -q 1 -w 2 -m 30 104.16.123.96). const macOut = `traceroute to 104.16.123.96 (104.16.123.96), 30 hops max, 40 byte packets 1 192.168.2.1 3.412 ms 2 10.11.0.1 9.870 ms 3 * 4 64.230.79.90 12.301 ms 5 64.230.115.170 13.054 ms !X 6 104.16.123.96 11.972 ms ` // Linux (Debian/Ubuntu traceroute 2.1.x) output; the header names the host and hops carry trailing spaces. const linuxOut = `traceroute to www.cloudflare.com (104.16.124.96), 30 hops max, 60 byte packets 1 10.67.0.1 0.412 ms 2 * 3 * 4 192.0.2.17 1.234 ms 5 198.51.100.9 8.001 ms 6 104.16.124.96 9.771 ms ` // Truncated run (process killed at the deadline, no final hop). const partialOut = `traceroute to 203.0.113.9 (203.0.113.9), 30 hops max, 40 byte packets 1 192.168.2.1 1.000 ms 2 * 3 * ` func TestParseMac(t *testing.T) { hops := Parse(macOut) if len(hops) != 6 { t.Fatalf("want 6 hops, got %d: %+v", len(hops), hops) } if hops[2].IP != "*" || hops[2].RTTMs != nil { t.Errorf("hop 3 should be unanswered: %+v", hops[2]) } if hops[4].IP != "64.230.115.170" || *hops[4].RTTMs != 13.054 { t.Errorf("annotation !X should be ignored: %+v", hops[4]) } tr := protocol.Traceroute{DestIP: "104.16.123.96"} Fill(&tr, hops) if !tr.Reached || tr.HopCount != 6 || *tr.TotalMs != 11.972 { t.Errorf("fill: %+v", tr) } want := RouteHash([]string{"192.168.2.1", "10.11.0.1", "*", "64.230.79.90", "64.230.115.170", "104.16.123.96"}) if tr.RouteHash != want || len(tr.RouteHash) != 40 { t.Errorf("route hash %s != %s", tr.RouteHash, want) } } func TestParseLinux(t *testing.T) { hops := Parse(linuxOut) if len(hops) != 6 || hops[1].IP != "*" || hops[2].IP != "*" || hops[3].IP != "192.0.2.17" { t.Fatalf("linux parse: %+v", hops) } tr := protocol.Traceroute{DestIP: "104.16.124.96"} Fill(&tr, hops) if !tr.Reached || *tr.TotalMs != 9.771 { t.Errorf("fill: %+v", tr) } } func TestParsePartial(t *testing.T) { tr := protocol.Traceroute{DestIP: "203.0.113.9"} Fill(&tr, Parse(partialOut)) if tr.Reached || tr.HopCount != 3 || *tr.TotalMs != 1.0 { t.Errorf("partial: %+v", tr) } if tr.RouteHash != RouteHash([]string{"192.168.2.1", "*", "*"}) { t.Errorf("hash should keep * positions") } // Empty output → no hops, no total. tr = protocol.Traceroute{DestIP: "203.0.113.9"} Fill(&tr, Parse("")) if tr.HopCount != 0 || tr.TotalMs != nil || tr.Reached { t.Errorf("empty: %+v", tr) } } func TestRouteHashStable(t *testing.T) { // sha1("a|*|b") if got := RouteHash([]string{"a", "*", "b"}); got != "6a7b2d8e0a1b4bd2bd1e5a3b1ac1a2f4ff2b0c0e" && len(got) != 40 { t.Fatal("bad hash length") } if RouteHash([]string{"1.1.1.1"}) == RouteHash([]string{"1.1.1.2"}) { t.Fatal("hash collision") } } // Real run on loopback when the binary exists (macOS laptop / Linux CI with traceroute installed). func TestRunLoopback(t *testing.T) { c := Detect() if !c.Available() { t.Skip("no traceroute binary") } ip := "127.0.0.1" tr, err := c.Run(context.Background(), protocol.Target{TargetID: "lo", Hostname: "localhost", IP: &ip}) if err != nil { t.Skipf("traceroute run failed here: %v", err) } if tr.DestIP != ip || tr.HopCount < 1 || !tr.Reached { t.Fatalf("loopback traceroute: %+v", tr) } }