diff --git a/Makefile b/Makefile index 7d54c72..d19f20a 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ OUT_DIR = ./dist all: build +fmt: + docker run --rm -v $(PWD)/src:/src golang:alpine sh -c "gofmt -w /src/*.go" + # Builds both binaries inside Docker and exports them locally build: @mkdir -p $(OUT_DIR) diff --git a/dist/livetrace-linux-amd64 b/dist/livetrace-linux-amd64 index ca1b4af..bac11f5 100755 Binary files a/dist/livetrace-linux-amd64 and b/dist/livetrace-linux-amd64 differ diff --git a/dist/livetrace-windows-amd64.exe b/dist/livetrace-windows-amd64.exe index 7048e85..e7d15d8 100755 Binary files a/dist/livetrace-windows-amd64.exe and b/dist/livetrace-windows-amd64.exe differ diff --git a/src/main.go b/src/main.go index 1e1b395..406ca32 100644 --- a/src/main.go +++ b/src/main.go @@ -32,9 +32,9 @@ import ( const ( icmpProto = 1 - histSize = 25 // history slots per hop - minPktSize = 28 // IP(20) + ICMP/UDP header(8) — zero payload - defPktSize = 56 // matches common ping/traceroute default + histSize = 25 // history slots per hop + minPktSize = 28 // IP(20) + ICMP/UDP header(8) — zero payload + defPktSize = 56 // matches common ping/traceroute default ) var barChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} @@ -57,7 +57,7 @@ type Hop struct { sent int recv int last float64 - lastTimeout bool // true if the most recent resolved probe timed out + lastTimeout bool // true if the most recent resolved probe timed out sumRTT float64 sumRTTSq float64 // Σ(rtt²) for online std-dev computation best float64 @@ -223,17 +223,17 @@ type Tracer struct { udpPC *ipv4.PacketConn // probe tracking - mu sync.Mutex - seqCtr int - probes map[int]*pending // key: ICMP seq (ICMP) or sequential int (UDP) - portToKey map[int]int // UDP only: dstPort → probe key - maxActive int - sendMu sync.Mutex // serialises SetTTL + send + mu sync.Mutex + seqCtr int + probes map[int]*pending // key: ICMP seq (ICMP) or sequential int (UDP) + portToKey map[int]int // UDP only: dstPort → probe key + maxActive int + sendMu sync.Mutex // serialises SetTTL + send // lifecycle stopCh chan struct{} started time.Time - debug bool + debug bool } func NewTracer( @@ -289,7 +289,7 @@ func NewTracer( maxActive: maxHops, stopCh: make(chan struct{}), started: time.Now(), - debug: debug, + debug: debug, }, nil } @@ -298,17 +298,18 @@ func NewTracer( func (t *Tracer) Done() <-chan struct{} { return t.stopCh } func (t *Tracer) Start() error { - if t.proto == "winapi" { - go t.probeLoop() - return nil - } + // in Start() — winping needs no raw sockets, same as winapi + if t.proto == "winapi" || t.proto == "winping" { + go t.probeLoop() + return nil + } - // ADD THIS: - if t.proto == "udp" { - udpModeWarning() - } + // ADD THIS: + if t.proto == "udp" { + udpModeWarning() + } - var err error + var err error t.icmpConn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { t.icmpConn, err = icmp.ListenPacket("udp4", "0.0.0.0:0") @@ -398,6 +399,8 @@ func (t *Tracer) makeUDPBytes() []byte { func (t *Tracer) sendProbe(ttl int) { switch t.proto { + case "winping": + t.sendProbeWinPing(ttl) case "udp": t.sendProbeUDP(ttl) case "winapi": @@ -631,30 +634,30 @@ func extractOriginalICMP(data []byte) (seq, id int, ok bool) { // extractOriginalUDPPort reads the original UDP destination port from the // embedded original IP+UDP header in a Time Exceeded or Dest Unreachable reply. func (t *Tracer) extractOriginalUDPPort(data []byte) (dstPort int, ok bool) { - if len(data) < 28 { - if t.debug { - fmt.Fprintf(os.Stderr, "[debug] extractUDP: too short (%d bytes)\n", len(data)) + if len(data) < 28 { + if t.debug { + fmt.Fprintf(os.Stderr, "[debug] extractUDP: too short (%d bytes)\n", len(data)) + } + return 0, false } - return 0, false - } - ihl := int(data[0]&0x0f) * 4 - if ihl < 20 || len(data) < ihl+8 { - if t.debug { - fmt.Fprintf(os.Stderr, "[debug] extractUDP: bad IHL (%d)\n", ihl) + ihl := int(data[0]&0x0f) * 4 + if ihl < 20 || len(data) < ihl+8 { + if t.debug { + fmt.Fprintf(os.Stderr, "[debug] extractUDP: bad IHL (%d)\n", ihl) + } + return 0, false } - return 0, false - } - if data[9] != 17 { - if t.debug { - fmt.Fprintf(os.Stderr, "[debug] extractUDP: proto=%d (not UDP)\n", data[9]) + if data[9] != 17 { + if t.debug { + fmt.Fprintf(os.Stderr, "[debug] extractUDP: proto=%d (not UDP)\n", data[9]) + } + return 0, false } - return 0, false - } - dstPort = int(binary.BigEndian.Uint16(data[ihl+2 : ihl+4])) + dstPort = int(binary.BigEndian.Uint16(data[ihl+2 : ihl+4])) if t.debug { - fmt.Fprintf(os.Stderr, "[debug] extractUDP: dstPort=%d\n", dstPort) + fmt.Fprintf(os.Stderr, "[debug] extractUDP: dstPort=%d\n", dstPort) } - return dstPort, true + return dstPort, true } // ─── Reply handler ──────────────────────────────────────────────────────────── @@ -924,15 +927,15 @@ func peerString(addr net.Addr) string { // ─── Entry point ───────────────────────────────────────────────────────────── func main() { - maxHops := flag.Int("m", 30, "maximum hops") + maxHops := flag.Int("m", 30, "maximum hops") intervalMS := flag.Int("i", 1000, "probe interval in milliseconds") - timeoutMS := flag.Int("t", 3000, "probe timeout in milliseconds") + timeoutMS := flag.Int("t", 3000, "probe timeout in milliseconds") proto := flag.String("proto", "icmp", "probe protocol: icmp, udp, or winapi (Windows ICMP API, Windows only)") - port := flag.Int("port", 33434, "base destination port (UDP mode only)") - size := flag.Int("size", defPktSize, "total IP packet size in bytes (min 28); use 1500 for MTU test") - noDNS := flag.Bool("n", false, "disable reverse DNS (show numeric IPs only)") - count := flag.Int("count", 0, "stop after N complete probe rounds (0 = run forever)") - dur := flag.Duration("duration", 0, "stop after this duration, e.g. 30s, 5m (0 = run forever)") + port := flag.Int("port", 33434, "base destination port (UDP mode only)") + size := flag.Int("size", defPktSize, "total IP packet size in bytes (min 28); use 1500 for MTU test") + noDNS := flag.Bool("n", false, "disable reverse DNS (show numeric IPs only)") + count := flag.Int("count", 0, "stop after N complete probe rounds (0 = run forever)") + dur := flag.Duration("duration", 0, "stop after this duration, e.g. 30s, 5m (0 = run forever)") debug := flag.Bool("debug", false, "enable debug output") flag.Usage = func() { @@ -961,14 +964,14 @@ Examples: p := strings.ToLower(*proto) -if p != "icmp" && p != "udp" && p != "winapi" { - fmt.Fprintln(os.Stderr, "Error: -proto must be icmp, udp, or winapi") - os.Exit(1) -} -if p == "winapi" && runtime.GOOS != "windows" { - fmt.Fprintln(os.Stderr, "Error: -proto winapi is only available on Windows") - os.Exit(1) -} + if p != "icmp" && p != "udp" && p != "winapi" && p != "winping" { + fmt.Fprintln(os.Stderr, "Error: -proto must be icmp, udp, or winapi") + os.Exit(1) + } + if p == "winapi" && runtime.GOOS != "windows" { + fmt.Fprintln(os.Stderr, "Error: -proto winapi is only available on Windows") + os.Exit(1) + } tracer, err := NewTracer( flag.Arg(0), @@ -1017,4 +1020,3 @@ if p == "winapi" && runtime.GOOS != "windows" { } } } - diff --git a/src/winapi_other.go b/src/winapi_other.go index 442c8c6..25bdb6e 100644 --- a/src/winapi_other.go +++ b/src/winapi_other.go @@ -4,9 +4,9 @@ package main import ( "fmt" + "golang.org/x/net/ipv4" "net" "time" - "golang.org/x/net/ipv4" ) func winAPIProbe(_ net.IP, _ int, _ time.Duration, _ []byte) (float64, string, uint32, error) { @@ -18,5 +18,7 @@ func (t *Tracer) sendProbeWinAPI(_ int) {} func udpModeWarning() {} func setUDPSocketTTL(pc *ipv4.PacketConn, conn net.PacketConn, ttl int) error { - return pc.SetTTL(ttl) + return pc.SetTTL(ttl) } + +func (t *Tracer) sendProbeWinPing(_ int) {} diff --git a/src/winapi_windows.go b/src/winapi_windows.go index 93d5acd..e288b7c 100644 --- a/src/winapi_windows.go +++ b/src/winapi_windows.go @@ -4,12 +4,15 @@ package main import ( "fmt" + "golang.org/x/net/ipv4" "net" + "os" + "os/exec" + "regexp" + "strconv" "syscall" "time" "unsafe" - "os" - "golang.org/x/net/ipv4" ) var ( @@ -17,6 +20,9 @@ var ( procIcmpCreate = modIphlpapi.NewProc("IcmpCreateFile") procIcmpClose = modIphlpapi.NewProc("IcmpCloseHandle") procIcmpSend2 = modIphlpapi.NewProc("IcmpSendEcho2") + rePingFrom = regexp.MustCompile(`Reply from ([\d.]+):`) + rePingTime = regexp.MustCompile(`time[=<](\d+)ms`) + rePingTimeout = regexp.MustCompile(`Request timed out|could not find`) ) const ( @@ -26,9 +32,9 @@ const ( ) func udpModeWarning() { - fmt.Fprintln(os.Stderr, - "Warning: UDP mode on Windows requires an inbound Windows Firewall rule "+ - "to allow ICMP Time Exceeded (Type 11). Consider using -proto winapi instead.") + fmt.Fprintln(os.Stderr, + "Warning: UDP mode on Windows requires an inbound Windows Firewall rule "+ + "to allow ICMP Time Exceeded (Type 11). Consider using -proto winapi instead.") } // mirrors IP_OPTION_INFORMATION from @@ -42,9 +48,9 @@ type ipOptInfo struct { // mirrors ICMP_ECHO_REPLY from type icmpEchoReply struct { - Address uint32 // source IP, bytes in network order inside a LE DWORD + Address uint32 // source IP, bytes in network order inside a LE DWORD Status uint32 - RoundTripTime uint32 // integer ms; we measure our own sub-ms time + RoundTripTime uint32 // integer ms; we measure our own sub-ms time DataSize uint16 Reserved uint16 Data uintptr // PVOID — platform-sized pointer @@ -145,18 +151,74 @@ func (t *Tracer) sendProbeWinAPI(ttl int) { } func setUDPSocketTTL(pc *ipv4.PacketConn, conn net.PacketConn, ttl int) error { - udpConn, ok := conn.(*net.UDPConn) - if !ok { - return pc.SetTTL(ttl) - } - raw, err := udpConn.SyscallConn() - if err != nil { - return pc.SetTTL(ttl) - } - var sockErr error - raw.Control(func(fd uintptr) { - sockErr = syscall.SetsockoptInt(syscall.Handle(fd), - syscall.IPPROTO_IP, syscall.IP_TTL, ttl) - }) - return sockErr + udpConn, ok := conn.(*net.UDPConn) + if !ok { + return pc.SetTTL(ttl) + } + raw, err := udpConn.SyscallConn() + if err != nil { + return pc.SetTTL(ttl) + } + var sockErr error + raw.Control(func(fd uintptr) { + sockErr = syscall.SetsockoptInt(syscall.Handle(fd), + syscall.IPPROTO_IP, syscall.IP_TTL, ttl) + }) + return sockErr +} + +func pingProbe(targetIP net.IP, ttl int, timeout time.Duration) (rtt float64, fromIP string, reached bool, timedOut bool) { + cmd := exec.Command("ping", + "-n", "1", + "-i", strconv.Itoa(ttl), + "-w", strconv.Itoa(int(timeout.Milliseconds())), + targetIP.String(), + ) + start := time.Now() + out, _ := cmd.Output() + elapsed := float64(time.Since(start).Microseconds()) / 1000.0 + text := string(out) + + if rePingTimeout.MatchString(text) { + return 0, "", false, true + } + m := rePingFrom.FindStringSubmatch(text) + if m == nil { + return 0, "", false, true + } + fromIP = m[1] + reached = fromIP == targetIP.String() + + if mt := rePingTime.FindStringSubmatch(text); mt != nil { + if v, err := strconv.ParseFloat(mt[1], 64); err == nil { + rtt = v + } + } else { + rtt = elapsed + } + return rtt, fromIP, reached, false +} + +func (t *Tracer) sendProbeWinPing(ttl int) { + t.hops[ttl-1].recordSent() + go func() { + rtt, fromIP, reached, timedOut := pingProbe(t.targetIP, ttl, t.timeout) + select { + case <-t.stopCh: + return + default: + } + if timedOut { + t.hops[ttl-1].recordTimeout() + return + } + t.hops[ttl-1].recordRecv(rtt, fromIP, reached) + if reached { + t.mu.Lock() + if t.maxActive > ttl { + t.maxActive = ttl + } + t.mu.Unlock() + } + }() }