V3.3.2
This commit is contained in:
parent
ae5e8975c3
commit
9621a23c79
3
Makefile
3
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)
|
||||
|
||||
BIN
dist/livetrace-linux-amd64
vendored
BIN
dist/livetrace-linux-amd64
vendored
Binary file not shown.
BIN
dist/livetrace-windows-amd64.exe
vendored
BIN
dist/livetrace-windows-amd64.exe
vendored
Binary file not shown.
114
src/main.go
114
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" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user