ttr.git


ttr

~ tiny traceroute in golang
git clone https://github.com/djnnvx/ttr.git
log | files | refs | archive | README | LICENSE

commit 7f43539558b4e6b090e0e2c314277f3247a01af3
parent c35b7c769e02ec710d7d1decd86af51cf6563e4e
Author: djnn <[email protected]>
Date: Mon, 5 Jan 2026 18:19:22 +0900

fix(net): ipv6 support

Diffstat:
Mnet/run.go | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------

1 file changed, 94 insertions(+), 27 deletions(-)

diff --git a/net/run.go b/net/run.go
@@ -10,6 +10,7 @@ import (
 
 	"golang.org/x/net/icmp"
 	"golang.org/x/net/ipv4"
+	"golang.org/x/net/ipv6"
 )
 
 const (
@@ -20,9 +21,11 @@ const (
 	echoPayload    = "tiny-traceroute-djnn-sh"
 
 	/* Network Protocols & Packet sizing */
-	protocolICMP     = 1
+	protocolICMPv4   = 1
+	protocolICMPv6   = 58
 	readBufferSize   = 1500
 	minIPv4HeaderLen = 20
+	minIPv6HeaderLen = 40
 	icmpHeaderLen    = 8
 	ipv4WordSize     = 4 /* IHL is measured in 32-bit (4-byte) words */
 	ihlMask          = 0x0f
@@ -56,7 +59,7 @@ func Run(target string, ipType string) {
 		for probe := 0; probe < probesPerHop; probe++ {
 			seq := (ttl * 100) + probe
 
-			peerAddr, rtt, err := executeProbe(c, pc, destIP, traceID, seq, ttl)
+			peerAddr, rtt, err := executeProbe(c, pc, destIP, traceID, seq, ttl, ipType)
 			if err != nil {
 				fmt.Printf("* ")
 			} else {
@@ -77,7 +80,7 @@ func Run(target string, ipType string) {
 	}
 }
 
-func setupSocket(ipVersion string) (*icmp.PacketConn, *ipv4.PacketConn, error) {
+func setupSocket(ipVersion string) (*icmp.PacketConn, interface{}, error) {
 	protocol := "ip4:icmp"
 	network := "0.0.0.0"
 	if ipVersion == "ip6" {
@@ -89,21 +92,36 @@ func setupSocket(ipVersion string) (*icmp.PacketConn, *ipv4.PacketConn, error) {
 	if err != nil {
 		return nil, nil, fmt.Errorf("listen packet: %w (ensure sudo/admin)", err)
 	}
+
+	if ipVersion == "ip6" {
+		return c, c.IPv6PacketConn(), nil
+	}
 	return c, c.IPv4PacketConn(), nil
 }
 
-func executeProbe(c *icmp.PacketConn, pc *ipv4.PacketConn, dest *net.IPAddr, id, seq, ttl int) (string, time.Duration, error) {
-	if err := pc.SetTTL(ttl); err != nil {
-		return "", 0, err
+func executeProbe(c *icmp.PacketConn, pc interface{}, dest *net.IPAddr, id, seq, ttl int, ipType string) (string, time.Duration, error) {
+	// Set TTL based on IP version
+	if ipType == "ip6" {
+		if pc6, ok := pc.(*ipv6.PacketConn); ok {
+			if err := pc6.SetHopLimit(ttl); err != nil {
+				return "", 0, err
+			}
+		}
+	} else {
+		if pc4, ok := pc.(*ipv4.PacketConn); ok {
+			if err := pc4.SetTTL(ttl); err != nil {
+				return "", 0, err
+			}
+		}
 	}
 
 	start := time.Now()
 
-	if err := sendEchoRequest(c, dest, id, seq); err != nil {
+	if err := sendEchoRequest(c, dest, id, seq, ipType); err != nil {
 		return "", 0, err
 	}
 
-	peer, err := awaitResponse(pc, id, seq)
+	peer, err := awaitResponse(c, pc, id, seq, ipType)
 	if err != nil {
 		return "", 0, err
 	}
@@ -111,9 +129,16 @@ func executeProbe(c *icmp.PacketConn, pc *ipv4.PacketConn, dest *net.IPAddr, id,
 	return cleanIP(peer.String()), time.Since(start), nil
 }
 
-func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int) error {
+func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int, ipType string) error {
+	var msgType icmp.Type
+	if ipType == "ip6" {
+		msgType = ipv6.ICMPTypeEchoRequest
+	} else {
+		msgType = ipv4.ICMPTypeEcho
+	}
+
 	msg := icmp.Message{
-		Type: ipv4.ICMPTypeEcho, Code: 0,
+		Type: msgType, Code: 0,
 		Body: &icmp.Echo{
 			ID: id, Seq: seq,
 			Data: []byte(echoPayload),
@@ -127,32 +152,55 @@ func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int) error {
 	return err
 }
 
-func awaitResponse(pc *ipv4.PacketConn, expectedID, expectedSeq int) (net.Addr, error) {
-	pc.SetReadDeadline(time.Now().Add(defaultTimeout))
+func awaitResponse(c *icmp.PacketConn, pc interface{}, expectedID, expectedSeq int, ipType string) (net.Addr, error) {
+	c.SetReadDeadline(time.Now().Add(defaultTimeout))
 	b := make([]byte, readBufferSize)
 
+	protocol := protocolICMPv4
+	if ipType == "ip6" {
+		protocol = protocolICMPv6
+	}
+
 	for {
-		n, _, peer, err := pc.ReadFrom(b)
+		n, peer, err := c.ReadFrom(b)
 		if err != nil {
 			return nil, err
 		}
 
-		rm, err := icmp.ParseMessage(protocolICMP, b[:n])
+		rm, err := icmp.ParseMessage(protocol, b[:n])
 		if err != nil {
 			continue
 		}
 
-		switch rm.Type {
-		case ipv4.ICMPTypeEchoReply:
-			if echo, ok := rm.Body.(*icmp.Echo); ok {
-				if echo.ID == expectedID && echo.Seq == expectedSeq {
-					return peer, nil
+		// Handle based on IP version
+		if ipType == "ip6" {
+			switch rm.Type {
+			case ipv6.ICMPTypeEchoReply:
+				if echo, ok := rm.Body.(*icmp.Echo); ok {
+					if echo.ID == expectedID && echo.Seq == expectedSeq {
+						return peer, nil
+					}
+				}
+			case ipv6.ICMPTypeTimeExceeded:
+				if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
+					if matchOriginalPacketV6(te.Data, expectedID, expectedSeq) {
+						return peer, nil
+					}
 				}
 			}
-		case ipv4.ICMPTypeTimeExceeded:
-			if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
-				if matchOriginalPacket(te.Data, expectedID, expectedSeq) {
-					return peer, nil
+		} else {
+			switch rm.Type {
+			case ipv4.ICMPTypeEchoReply:
+				if echo, ok := rm.Body.(*icmp.Echo); ok {
+					if echo.ID == expectedID && echo.Seq == expectedSeq {
+						return peer, nil
+					}
+				}
+			case ipv4.ICMPTypeTimeExceeded:
+				if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
+					if matchOriginalPacket(te.Data, expectedID, expectedSeq) {
+						return peer, nil
+					}
 				}
 			}
 		}
@@ -173,12 +221,31 @@ func matchOriginalPacket(data []byte, expectedID, expectedSeq int) bool {
 	}
 
 	innerICMP := data[ihl:]
+	origID, origSeq := extractICMPIDAndSeq(innerICMP)
+	return origID == expectedID && origSeq == expectedSeq
+}
 
-	/* Inner ICMP Structure: Type(1) + Code(1) + Checksum(2) + ID(2) + Seq(2) */
-	origID := binary.BigEndian.Uint16(innerICMP[4:6])
-	origSeq := binary.BigEndian.Uint16(innerICMP[6:8])
+func matchOriginalPacketV6(data []byte, expectedID, expectedSeq int) bool {
+	/* For IPv6, the TimeExceeded message contains the original IPv6 header +
+	   as much of the original packet as possible. IPv6 has a fixed 40-byte header */
+	if len(data) < minIPv6HeaderLen+icmpHeaderLen {
+		return false
+	}
 
-	return int(origID) == expectedID && int(origSeq) == expectedSeq
+	/* Skip the IPv6 header (40 bytes) to get to the inner ICMPv6 packet */
+	innerICMP := data[minIPv6HeaderLen:]
+	origID, origSeq := extractICMPIDAndSeq(innerICMP)
+	return origID == expectedID && origSeq == expectedSeq
+}
+
+func extractICMPIDAndSeq(icmpData []byte) (id, seq int) {
+	/* ICMP Echo Structure: Type(1) + Code(1) + Checksum(2) + ID(2) + Seq(2) */
+	if len(icmpData) < icmpHeaderLen {
+		return 0, 0
+	}
+	origID := binary.BigEndian.Uint16(icmpData[4:6])
+	origSeq := binary.BigEndian.Uint16(icmpData[6:8])
+	return int(origID), int(origSeq)
 }
 
 func printProbeResult(peerIP, prevAddr string, rtt time.Duration) {