package net import ( "b612.me/starlog" "b612.me/starnet" "context" "encoding/json" "fmt" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "net" "strings" "sync/atomic" "time" ) func useCustomeDNS(dns []string) { resolver := net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (conn net.Conn, err error) { for _, addr := range dns { if conn, err = net.Dial("udp", addr+":53"); err != nil { continue } else { return conn, nil } } return }, } net.DefaultResolver = &resolver } func Traceroute(address string, bindaddr string, dns string, maxHops int, timeout time.Duration, ipinfoAddr string, hideIncorrect bool) { ipinfo := net.ParseIP(address) if ipinfo == nil { { if dns != "" { useCustomeDNS([]string{dns}) starlog.Infoln("使用自定义DNS服务器:", dns) } else { starlog.Infoln("使用系统默认DNS服务器") } addr, err := net.ResolveIPAddr("ip", address) if err != nil { starlog.Errorln("IP地址解析失败:", address, err) return } starlog.Infoln("解析IP地址:", addr.String()) address = addr.String() } } traceroute(address, bindaddr, maxHops, timeout, ipinfoAddr, hideIncorrect) } func traceroute(address string, bindaddr string, maxHops int, timeout time.Duration, ipinfoAddr string, hideIncorrect bool) { ipinfo := net.ParseIP(address) if ipinfo == nil { starlog.Errorln("IP地址解析失败:", address) return } var echoType icmp.Type = ipv4.ICMPTypeEcho var exceededType icmp.Type = ipv4.ICMPTypeTimeExceeded var replyType icmp.Type = ipv4.ICMPTypeEchoReply var proto = 1 var network = "ip4:icmp" var resolveIP = "ip4" if ipinfo.To4() == nil { network = "ip6:ipv6-icmp" resolveIP = "ip6" echoType = ipv6.ICMPTypeEchoRequest exceededType = ipv6.ICMPTypeTimeExceeded replyType = ipv6.ICMPTypeEchoReply proto = 58 } if bindaddr == "" { bindaddr = "0.0.0.0" } c, err := icmp.ListenPacket(network, bindaddr) if err != nil { fmt.Println(err) return } defer c.Close() if maxHops == 0 { maxHops = 32 } firstTargetHop := int32(maxHops + 1) if timeout == 0 { timeout = time.Second * 3 } exitfor: for i := 1; i <= maxHops; i++ { retry := 0 doRetry: dst, err := net.ResolveIPAddr(resolveIP, address) if err != nil { starlog.Errorln("IP地址解析失败:", address, err) return } if atomic.LoadInt32(&firstTargetHop) <= int32(i) { return } m := icmp.Message{ Type: echoType, Code: 0, Body: &icmp.Echo{ ID: i, Seq: i, Data: []byte("B612.ME-ROUTER-TRACE"), }, } b, err := m.Marshal(nil) if err != nil { fmt.Printf("%d\tMarshal error: %v\n", i, err) continue } if network == "ip4:icmp" { if err := c.IPv4PacketConn().SetTTL(i); err != nil { fmt.Printf("%d\tSetTTL error: %v\n", i, err) continue } } else { if err := c.IPv6PacketConn().SetHopLimit(i); err != nil { fmt.Printf("%d\tSetHopLimit error: %v\n", i, err) continue } } start := time.Now() n, err := c.WriteTo(b, dst) if err != nil { fmt.Printf("%d\tWriteTo error: %v\n", i, err) continue } else if n != len(b) { fmt.Printf("%d\tWrite Short: %v Expected: %v\n", i, n, len(b)) continue } now := time.Now() exitrecheck: for { reply := make([]byte, 1500) err = c.SetReadDeadline(time.Now().Add(timeout)) if err != nil { fmt.Printf("%d\tSetReadDeadline error: %v\n", i, err) break } n, peer, err := c.ReadFrom(reply) if err != nil { fmt.Printf("%d\tReadFrom error: %v\n", i, err) break } duration := time.Since(start) rm, err := icmp.ParseMessage(proto, reply[:n]) if err != nil { fmt.Printf("%d\tParseMessage error: %v\n", i, err) break } switch rm.Type { case exceededType: fmt.Printf("%d\thops away:\t%s\t(%s) %s\n", i, peer, duration, GetIPInfo(peer.String(), ipinfoAddr)) break exitrecheck case replyType: fmt.Printf("%d\thops away:\t%s\t(%s) %s\n", i, peer, duration, GetIPInfo(peer.String(), ipinfoAddr)) if peer.String() == dst.String() { break exitfor } case ipv4.ICMPTypeEcho, ipv6.ICMPTypeEchoRequest: if time.Now().Sub(now).Seconds() > timeout.Seconds() { if retry < 1 { retry++ goto doRetry } if !hideIncorrect { fmt.Printf("%d\tInvalid Echo Request:%s (%s) %s\n", i, peer, duration, GetIPInfo(peer.String(), ipinfoAddr)) } break exitrecheck } case ipv4.ICMPTypeDestinationUnreachable, ipv6.ICMPTypeDestinationUnreachable: if time.Now().Sub(now).Seconds() > timeout.Seconds() { if retry < 1 { retry++ goto doRetry } if !hideIncorrect { fmt.Printf("%d\tInvalid DstInv Request:%s (%s) %s\n", i, peer, duration, GetIPInfo(peer.String(), ipinfoAddr)) } break exitrecheck } default: if time.Now().Sub(now).Seconds() > timeout.Seconds() { if retry < 1 { retry++ goto doRetry } if !hideIncorrect { fmt.Printf("%d\tgot %+v from %v (%s) %s\n", i, rm.Type, peer, duration, GetIPInfo(peer.String(), ipinfoAddr)) } break exitrecheck } } } } } func GetIPInfo(ip string, addr string) string { if addr == "" { return "" } uri := strings.ReplaceAll(addr, "{ip}", ip) res, err := starnet.Curl(starnet.NewRequests(uri, nil, "GET", starnet.WithTimeout(time.Second*2), starnet.WithDialTimeout(time.Second*3))) if err != nil { return "获取IP信息失败:" + err.Error() } var ipinfo IPInfo err = json.Unmarshal(res.RecvData, &ipinfo) if err != nil { return "解析IP信息失败:" + err.Error() } return fmt.Sprintf("%s %s %s %s %s", ipinfo.CountryName, ipinfo.RegionName, ipinfo.CityName, ipinfo.OwnerDomain, ipinfo.ISP) } type IPInfo struct { CountryName string `json:"country_name"` RegionName string `json:"region_name"` CityName string `json:"city_name"` OwnerDomain string `json:"owner_domain"` Ip string `json:"ip"` ISP string `json:"isp_domain"` Err string `json:"err"` }