diff --git a/dist/livetrace-linux-amd64 b/dist/livetrace-linux-amd64 index d646c6c..580fc5e 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 925a1bc..c0add3d 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 5d2ef5a..2d22d59 100644 --- a/src/main.go +++ b/src/main.go @@ -457,12 +457,6 @@ func (t *Tracer) sendProbeUDP(ttl int) { } } -func winAPIProbe(_ net.IP, _ int, _ time.Duration, _ []byte) (float64, string, uint32, error) { - return 0, "", 0, fmt.Errorf("winapi probe not supported on this platform") -} - -func (t *Tracer) sendProbeWinAPI(_ int) {} - // ─── Probe loop ─────────────────────────────────────────────────────────────── func (t *Tracer) probeLoop() { diff --git a/src/winapi_other.go b/src/winapi_other.go new file mode 100644 index 0000000..0f7cb19 --- /dev/null +++ b/src/winapi_other.go @@ -0,0 +1,15 @@ +//go:build !windows + +package main + +import ( + "fmt" + "net" + "time" +) + +func winAPIProbe(_ net.IP, _ int, _ time.Duration, _ []byte) (float64, string, uint32, error) { + return 0, "", 0, fmt.Errorf("winapi probe not supported on this platform") +} + +func (t *Tracer) sendProbeWinAPI(_ int) {} diff --git a/src/winapi_windows.go b/src/winapi_windows.go new file mode 100644 index 0000000..d9d95ed --- /dev/null +++ b/src/winapi_windows.go @@ -0,0 +1,137 @@ +//go:build windows + +package main + +import ( + "fmt" + "net" + "syscall" + "time" + "unsafe" +) + +var ( + modIphlpapi = syscall.NewLazyDLL("iphlpapi.dll") + procIcmpCreate = modIphlpapi.NewProc("IcmpCreateFile") + procIcmpClose = modIphlpapi.NewProc("IcmpCloseHandle") + procIcmpSend2 = modIphlpapi.NewProc("IcmpSendEcho2") +) + +const ( + ipSuccess uint32 = 0 + ipTTLExpiredTransit uint32 = 11013 + ipReqTimedOut uint32 = 11010 +) + +// mirrors IP_OPTION_INFORMATION from +type ipOptInfo struct { + Ttl uint8 + Tos uint8 + Flags uint8 + OptionsSize uint8 + OptionsData uintptr // PUCHAR — platform-sized pointer +} + +// mirrors ICMP_ECHO_REPLY from +type icmpEchoReply struct { + 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 + DataSize uint16 + Reserved uint16 + Data uintptr // PVOID — platform-sized pointer + Options ipOptInfo +} + +// winAPIProbe sends one ICMP echo with the given TTL via the Windows ICMP +// helper API (iphlpapi.dll) and blocks until a reply arrives or timeout +// elapses. Returns (rtt ms, responder IP, IP status code, error). +func winAPIProbe(targetIP net.IP, ttl int, timeout time.Duration, payload []byte) (float64, string, uint32, error) { + handle, _, e := procIcmpCreate.Call() + if handle == 0 { + return 0, "", 0, fmt.Errorf("IcmpCreateFile: %w", e) + } + defer procIcmpClose.Call(handle) //nolint:errcheck + + // Pack the destination IP into a DWORD with the bytes in network order. + // On LE x86/x64: storing [ip[0]..ip[3]] means ip[0] is the LSB of the uint32. + ip4 := targetIP.To4() + dst := uint32(ip4[0]) | uint32(ip4[1])<<8 | uint32(ip4[2])<<16 | uint32(ip4[3])<<24 + + opts := ipOptInfo{Ttl: uint8(ttl)} + + replyBufSz := uintptr(unsafe.Sizeof(icmpEchoReply{})) + uintptr(len(payload)) + 8 + replyBuf := make([]byte, replyBufSz) + + var dataPtr uintptr + if len(payload) > 0 { + dataPtr = uintptr(unsafe.Pointer(&payload[0])) + } + + start := time.Now() + ret, _, _ := procIcmpSend2.Call( + handle, + 0, 0, 0, // Event, ApcRoutine, ApcContext — synchronous mode + uintptr(dst), + dataPtr, uintptr(len(payload)), + uintptr(unsafe.Pointer(&opts)), + uintptr(unsafe.Pointer(&replyBuf[0])), + uintptr(replyBufSz), + uintptr(timeout.Milliseconds()), + ) + rtt := float64(time.Since(start).Microseconds()) / 1000.0 + + if ret == 0 { + // Timeout or hard error — treat as timeout. + return 0, "", ipReqTimedOut, nil + } + + reply := (*icmpEchoReply)(unsafe.Pointer(&replyBuf[0])) + + // Unpack the source IP: Address bytes are in network order inside the DWORD. + fromIP := net.IP{ + byte(reply.Address), + byte(reply.Address >> 8), + byte(reply.Address >> 16), + byte(reply.Address >> 24), + }.String() + + return rtt, fromIP, reply.Status, nil +} + +// sendProbeWinAPI sends one probe for the given TTL using the Windows ICMP +// API. It returns immediately; the blocking IcmpSendEcho2 call runs in its +// own goroutine and records the result directly into the hop. +func (t *Tracer) sendProbeWinAPI(ttl int) { + t.hops[ttl-1].recordSent() + + go func() { + payload := make([]byte, t.payloadSize()) + copy(payload, "livetrace") + + rtt, fromIP, status, err := winAPIProbe(t.targetIP, ttl, t.timeout, payload) + + // Discard the result if the tracer was stopped while we were blocked. + select { + case <-t.stopCh: + return + default: + } + + if err != nil || status == ipReqTimedOut { + t.hops[ttl-1].recordTimeout() + return + } + + reached := status == ipSuccess + t.hops[ttl-1].recordRecv(rtt, fromIP, reached) + + if reached { + t.mu.Lock() + if t.maxActive > ttl { + t.maxActive = ttl + } + t.mu.Unlock() + } + }() +}