如何使用Go实现traceroute工具

17 min read

traceroute是一种用于查找特定目标地址的网络路径的工具。它使用ICMP协议发送数据包,每个数据包带有不同的TTL(生存时间)。路由上的每个节点都会降低TTL,当TTL为0时,目标主机在回复ICMP超时消息之前就会被丢弃,这样traceroute就可以确定途经路径。以下是使用Go实现traceroute工具的示例代码:

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host\n", os.Args[0])
        os.Exit(1)
    }
    host := os.Args[1]
    addr, err := net.ResolveIPAddr("ip", host)
    if err != nil {
        fmt.Println("Unable to resolve host IP address:", err)
        os.Exit(1)
    }
    conn, err := net.Dial("ip:icmp", addr.String())
    if err != nil {
        fmt.Println("Unable to establish ICMP connection:", err)
        os.Exit(1)
    }
    defer conn.Close()
    fmt.Println("Tracing route to", addr.String())
    const maxHops = 30
    for i := 1; i <= maxHops; i++ {
        conn.SetTTL(i)
        var msg [512]byte
        msg[0] = 8  // ICMP Echo Request Type
        msg[1] = 0  // ICMP Echo Request Code
        msg[2] = 0  // ICMP Echo Request Checksum (16 bits)
        msg[3] = 0  // ICMP Echo Request Checksum (16 bits)
        msg[4] = 0  // ICMP Echo Request Identifier (16 bits)
        msg[5] = 0  // ICMP Echo Request Identifier (16 bits)
        msg[6] = byte(i)  // ICMP Echo Request Sequence Number (16 bits)
        msg[7] = 0  // ICMP Echo Request Sequence Number (16 bits)
        length := 8
        checksum := checkSum(msg[:length])
        msg[2] = byte(checksum >> 8)
        msg[3] = byte(checksum & 0xff)
        start := now()
        _, err := conn.Write(msg[:length])
        if err != nil {
            fmt.Println("Error sending ICMP ping request:", err)
            continue
        }
        reply := make([]byte, 512)
        conn.SetReadDeadline(start.add(5*time.Second))
        n, err := conn.Read(reply)
        elapsed := time.Since(start)
        if err != nil {
            if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
                fmt.Printf("%02d * * *\n", i)
                continue
            } else {
                fmt.Println("Error reading ICMP ping reply:", err)
                continue
            }
        }
        if reply[0] == 11 && reply[1] == 0 {
            fmt.Printf("%02d %v %v\n", i, addr.String(), elapsed)
            break
        } else {
            fmt.Printf("%02d %v %v\n", i, net.IP(reply[12:16]).String(), elapsed)
        }
    }
}

func checkSum(data []byte) uint16 {
    var sum uint32
    for i := 0; i < len(data)-1; i += 2 {
        sum += uint32(data[i+1])<<8 | uint32(data[i])
    }
    if len(data)%2 != 0 {
        sum += uint32(data[len(data)-1])
    }
    sum = (sum >> 16) + (sum & 0xffff)
    sum = sum + (sum >> 16)
    return uint16(^sum)
}

func now() time.Time {
    return time.Unix(0, syscall.Gettimeofday().Nsec)
}

该示例使用net包创建ICMP连接,并使用SetTTL()设置TTL值。然后,它构造了一个ICMP Echo请求消息,并使用网络连接发送它。接下来,它等待接收ICMP Echo回复消息。如果它没有收到回复或者超时,则移动到下一个TTL值。如果它收到ICMP Echo回复,并且对于该TTL值它已经到达目标主机,则它打印出目标主机的IP地址和往返时间。否则,它将打印出到达该TTL值的第一个路由器的IP地址和往返时间。