在网络诊断中,我们常常需要了解数据包从源主机到目标主机所经过的路径,以便定位网络问题。Windows 下的 tracert
就是一种常用的工具,它通过发送一系列带有特殊 TTL(Time To Live)值的 ICMP 数据包,并分析返回的 ICMP 报文,逐步揭示数据包途径的每一个路由器或网关,以及每个节点的响应时间和丢包情况,帮助我们诊断网络延迟和连接问题。
步骤 www.cqzlsb.com
- 发送 ICMP Echo Request 数据包: tracert 程序会向目标主机发送一系列 ICMP Echo Request (ping) 数据包。
- 设置递增的 TTL 值: 对于每个数据包,tracert 会将 IP 头部的 TTL 字段设置为一个递增的值,通常从 1 开始。
- 路由器递减 TTL: 当数据包经过网络上的每个路由器时,路由器会将 TTL 值减 1。
- TTL 到达 0: 当 TTL 值达到 0 时,路由器会丢弃该数据包,并向发送方发送一个 ICMP Time Exceeded (类型 11,代码 0) 消息。
- ICMP Time Exceeded 消息包含路由器信息: 这个 ICMP Time Exceeded 消息包含了丢弃数据包的路由器的 IP 地址。
- tracert 记录路由器信息: tracert 程序会接收 ICMP Time Exceeded 消息,并记录下该路由器的 IP 地址和往返时间 (RTT)。
- 重复步骤 1-6,直到到达目标主机: tracert 会不断增加 TTL 值,并重复发送数据包,直到其中一个数据包到达目标主机。
- 目标主机回应 ICMP Echo Reply: 当数据包到达目标主机时,目标主机将回应一个 ICMP Echo Reply (类型 0,代码 0) 消息。
- tracert 结束: tracert 程序接收到 ICMP Echo Reply 消息后,就知道已经到达目标主机,并结束追踪过程。
基于 Go 的 tracert 命令简单实现
这里使用了 golang.org/x/net/icmp
扩展库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | func Tracert(addr string) error { timeout := time.Second * 10 conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { return fmt.Errorf("error listening for ICMP packets: %w", err) } defer conn.Close() destinationAddress, err := net.ResolveIPAddr("ip4", addr) if err != nil { return fmt.Errorf("error resolving hostname: %w", err) } fmt.Printf("Traceroute to '%s' [%s]\n", addr, destinationAddress.IP) message := icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: os.Getpid() & 0xffff, Data: []byte("hello"), }, } for ttl := 1; ttl <= 30; ttl++ { fmt.Printf("%d ", ttl) message.Body.(*icmp.Echo).Seq = ttl messageBytes, err := message.Marshal(nil) if err != nil { return fmt.Errorf("error marshaling ICMP message: %w", err) } if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil { return fmt.Errorf("error setting TTL: %w", err) } start := time.Now() if _, err := conn.WriteTo(messageBytes, destinationAddress); err != nil { return fmt.Errorf("error sending ICMP packet: %w", err) } responseBytes := make([]byte, 1500) conn.SetReadDeadline(time.Now().Add(timeout)) n, remoteAddress, err := conn.ReadFrom(responseBytes) if err != nil { if neterr, ok := err.(net.Error); ok && neterr.Timeout() { fmt.Println("* (Timeout)") } else { fmt.Println("* (Error)") } continue } duration := time.Since(start) responseMessage, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), responseBytes[:n]) if err != nil { return fmt.Errorf("error parsing ICMP message: %w", err) } switch responseMessage.Type { case ipv4.ICMPTypeTimeExceeded: fmt.Printf("%v %v ms\n", remoteAddress, duration.Milliseconds()) case ipv4.ICMPTypeEchoReply: fmt.Printf("%v %v ms\n", remoteAddress, duration.Milliseconds()) fmt.Println("Traceroute Complete.") return nil default: return fmt.Errorf("unexpected ICMP message type: %v, code: %v", responseMessage.Type, responseMessage.Code) } } return nil } |