ttr.git


ttr

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

run.go (6416B)

      1 package net
      2 
      3 import (
      4 	"encoding/binary"
      5 	"fmt"
      6 	"net"
      7 	"os"
      8 	"strings"
      9 	"time"
     10 
     11 	"golang.org/x/net/icmp"
     12 	"golang.org/x/net/ipv4"
     13 	"golang.org/x/net/ipv6"
     14 )
     15 
     16 const (
     17 	/* Configuration */
     18 	defaultMaxHops = 30
     19 	defaultTimeout = 1 * time.Second
     20 	probesPerHop   = 3
     21 	echoPayload    = "tiny-traceroute-djnn-sh"
     22 
     23 	/* Network Protocols & Packet sizing */
     24 	protocolICMPv4   = 1
     25 	protocolICMPv6   = 58
     26 	readBufferSize   = 1500
     27 	minIPv4HeaderLen = 20
     28 	minIPv6HeaderLen = 40
     29 	icmpHeaderLen    = 8
     30 	ipv4WordSize     = 4 /* IHL is measured in 32-bit (4-byte) words */
     31 	ihlMask          = 0x0f
     32 	processIDMask    = 0xffff
     33 )
     34 
     35 func Run(target string, ipType string) {
     36 	destIP, err := net.ResolveIPAddr("ip", target)
     37 	if err != nil {
     38 		fmt.Printf("Error resolving host: %v\n", err)
     39 		os.Exit(1)
     40 	}
     41 
     42 	fmt.Printf("traceroute to %s (%s), %d hops max\n", target, destIP.String(), defaultMaxHops)
     43 
     44 	c, pc, err := setupSocket(ipType)
     45 	if err != nil {
     46 		fmt.Printf("Socket error: %v\n", err)
     47 		return
     48 	}
     49 	defer c.Close()
     50 
     51 	traceID := os.Getpid() & processIDMask
     52 
     53 	for ttl := 1; ttl <= defaultMaxHops; ttl++ {
     54 		fmt.Printf("%2d  ", ttl)
     55 
     56 		var prevAddr string
     57 		destinationReached := false
     58 
     59 		for probe := 0; probe < probesPerHop; probe++ {
     60 			seq := (ttl * 100) + probe
     61 
     62 			peerAddr, rtt, err := executeProbe(c, pc, destIP, traceID, seq, ttl, ipType)
     63 			if err != nil {
     64 				fmt.Printf("* ")
     65 			} else {
     66 				printProbeResult(peerAddr, prevAddr, rtt)
     67 				prevAddr = peerAddr
     68 
     69 				if peerAddr == destIP.String() {
     70 					destinationReached = true
     71 				}
     72 			}
     73 		}
     74 
     75 		fmt.Println()
     76 
     77 		if destinationReached {
     78 			break
     79 		}
     80 	}
     81 }
     82 
     83 func setupSocket(ipVersion string) (*icmp.PacketConn, interface{}, error) {
     84 	protocol := "ip4:icmp"
     85 	network := "0.0.0.0"
     86 	if ipVersion == "ip6" {
     87 		protocol = "ip6:ipv6-icmp"
     88 		network = "::"
     89 	}
     90 
     91 	c, err := icmp.ListenPacket(protocol, network)
     92 	if err != nil {
     93 		return nil, nil, fmt.Errorf("listen packet: %w (ensure sudo/admin)", err)
     94 	}
     95 
     96 	if ipVersion == "ip6" {
     97 		return c, c.IPv6PacketConn(), nil
     98 	}
     99 	return c, c.IPv4PacketConn(), nil
    100 }
    101 
    102 func executeProbe(c *icmp.PacketConn, pc interface{}, dest *net.IPAddr, id, seq, ttl int, ipType string) (string, time.Duration, error) {
    103 	// Set TTL based on IP version
    104 	if ipType == "ip6" {
    105 		if pc6, ok := pc.(*ipv6.PacketConn); ok {
    106 			if err := pc6.SetHopLimit(ttl); err != nil {
    107 				return "", 0, err
    108 			}
    109 		}
    110 	} else {
    111 		if pc4, ok := pc.(*ipv4.PacketConn); ok {
    112 			if err := pc4.SetTTL(ttl); err != nil {
    113 				return "", 0, err
    114 			}
    115 		}
    116 	}
    117 
    118 	start := time.Now()
    119 
    120 	if err := sendEchoRequest(c, dest, id, seq, ipType); err != nil {
    121 		return "", 0, err
    122 	}
    123 
    124 	peer, err := awaitResponse(c, pc, id, seq, ipType)
    125 	if err != nil {
    126 		return "", 0, err
    127 	}
    128 
    129 	return cleanIP(peer.String()), time.Since(start), nil
    130 }
    131 
    132 func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int, ipType string) error {
    133 	var msgType icmp.Type
    134 	if ipType == "ip6" {
    135 		msgType = ipv6.ICMPTypeEchoRequest
    136 	} else {
    137 		msgType = ipv4.ICMPTypeEcho
    138 	}
    139 
    140 	msg := icmp.Message{
    141 		Type: msgType, Code: 0,
    142 		Body: &icmp.Echo{
    143 			ID: id, Seq: seq,
    144 			Data: []byte(echoPayload),
    145 		},
    146 	}
    147 	b, err := msg.Marshal(nil)
    148 	if err != nil {
    149 		return err
    150 	}
    151 	_, err = c.WriteTo(b, dest)
    152 	return err
    153 }
    154 
    155 func awaitResponse(c *icmp.PacketConn, pc interface{}, expectedID, expectedSeq int, ipType string) (net.Addr, error) {
    156 	c.SetReadDeadline(time.Now().Add(defaultTimeout))
    157 	b := make([]byte, readBufferSize)
    158 
    159 	protocol := protocolICMPv4
    160 	if ipType == "ip6" {
    161 		protocol = protocolICMPv6
    162 	}
    163 
    164 	for {
    165 		n, peer, err := c.ReadFrom(b)
    166 		if err != nil {
    167 			return nil, err
    168 		}
    169 
    170 		rm, err := icmp.ParseMessage(protocol, b[:n])
    171 		if err != nil {
    172 			continue
    173 		}
    174 
    175 		// Handle based on IP version
    176 		if ipType == "ip6" {
    177 			switch rm.Type {
    178 			case ipv6.ICMPTypeEchoReply:
    179 				if echo, ok := rm.Body.(*icmp.Echo); ok {
    180 					if echo.ID == expectedID && echo.Seq == expectedSeq {
    181 						return peer, nil
    182 					}
    183 				}
    184 			case ipv6.ICMPTypeTimeExceeded:
    185 				if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
    186 					if matchOriginalPacketV6(te.Data, expectedID, expectedSeq) {
    187 						return peer, nil
    188 					}
    189 				}
    190 			}
    191 		} else {
    192 			switch rm.Type {
    193 			case ipv4.ICMPTypeEchoReply:
    194 				if echo, ok := rm.Body.(*icmp.Echo); ok {
    195 					if echo.ID == expectedID && echo.Seq == expectedSeq {
    196 						return peer, nil
    197 					}
    198 				}
    199 			case ipv4.ICMPTypeTimeExceeded:
    200 				if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
    201 					if matchOriginalPacket(te.Data, expectedID, expectedSeq) {
    202 						return peer, nil
    203 					}
    204 				}
    205 			}
    206 		}
    207 	}
    208 }
    209 
    210 func matchOriginalPacket(data []byte, expectedID, expectedSeq int) bool {
    211 	if len(data) < minIPv4HeaderLen {
    212 		return false
    213 	}
    214 
    215 	/* Calculate Internet Header Length (IHL) */
    216 	ihl := int(data[0]&ihlMask) * ipv4WordSize
    217 
    218 	/* We need the IP header + 8 bytes of the inner ICMP packet */
    219 	if len(data) < ihl+icmpHeaderLen {
    220 		return false
    221 	}
    222 
    223 	innerICMP := data[ihl:]
    224 	origID, origSeq := extractICMPIDAndSeq(innerICMP)
    225 	return origID == expectedID && origSeq == expectedSeq
    226 }
    227 
    228 func matchOriginalPacketV6(data []byte, expectedID, expectedSeq int) bool {
    229 	/* For IPv6, the TimeExceeded message contains the original IPv6 header +
    230 	   as much of the original packet as possible. IPv6 has a fixed 40-byte header */
    231 	if len(data) < minIPv6HeaderLen+icmpHeaderLen {
    232 		return false
    233 	}
    234 
    235 	/* Skip the IPv6 header (40 bytes) to get to the inner ICMPv6 packet */
    236 	innerICMP := data[minIPv6HeaderLen:]
    237 	origID, origSeq := extractICMPIDAndSeq(innerICMP)
    238 	return origID == expectedID && origSeq == expectedSeq
    239 }
    240 
    241 func extractICMPIDAndSeq(icmpData []byte) (id, seq int) {
    242 	/* ICMP Echo Structure: Type(1) + Code(1) + Checksum(2) + ID(2) + Seq(2) */
    243 	if len(icmpData) < icmpHeaderLen {
    244 		return 0, 0
    245 	}
    246 	origID := binary.BigEndian.Uint16(icmpData[4:6])
    247 	origSeq := binary.BigEndian.Uint16(icmpData[6:8])
    248 	return int(origID), int(origSeq)
    249 }
    250 
    251 func printProbeResult(peerIP, prevAddr string, rtt time.Duration) {
    252 	if peerIP != prevAddr {
    253 		fmt.Printf("%s ", resolveHostname(peerIP))
    254 	}
    255 	fmt.Printf("%.3f ms  ", float64(rtt.Microseconds())/1000.0)
    256 }
    257 
    258 func resolveHostname(ip string) string {
    259 	names, _ := net.LookupAddr(ip)
    260 	if len(names) > 0 {
    261 		hostname := names[0]
    262 		if strings.HasSuffix(hostname, ".") {
    263 			hostname = hostname[:len(hostname)-1]
    264 		}
    265 		return fmt.Sprintf("%s (%s)", hostname, ip)
    266 	}
    267 	return ip
    268 }
    269 
    270 func cleanIP(addr string) string {
    271 	if host, _, err := net.SplitHostPort(addr); err == nil {
    272 		return host
    273 	}
    274 	return addr
    275 }