This commit is contained in:
Eliezer Croitoru 2026-09-09 17:10:45 +03:00
parent ae5e8975c3
commit 9621a23c79
6 changed files with 148 additions and 79 deletions

View File

@ -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)

Binary file not shown.

Binary file not shown.

View File

@ -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" {
}
}
}

View File

@ -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) {}

View File

@ -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 <ipexport.h>
@ -42,9 +48,9 @@ type ipOptInfo struct {
// mirrors ICMP_ECHO_REPLY from <ipexport.h>
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()
}
}()
}