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 }