Compare commits

..

29 Commits

Author SHA1 Message Date
zhshch2002
e728b6b063 refactor: icmp 2022-05-23 17:35:40 +08:00
zhshch2002
8b2d2f3990 Merge branch 'main' of https://github.com/xgadget-lab/nexttrace 2022-05-23 17:01:15 +08:00
zhshch2002
4c51b2fbbe add: realtime and table output mode
Co-authored-by: sjlleo <sjlleo@users.noreply.github.com>
2022-05-23 17:00:24 +08:00
sjlleo
9a6586f27a update: import jq package 2022-05-23 16:01:16 +08:00
sjlleo
cc8e3e4838 update: 修复命令在直接使用sudo的情况下不可用 2022-05-23 10:32:26 +08:00
sjlleo
d69b7b9acb update: 新增国内镜像加速 2022-05-23 10:18:45 +08:00
sjlleo
483a90848d update: fix a bash problem 2022-05-23 10:05:48 +08:00
sjlleo
131a9e2e8a update: add some description 2022-05-23 09:47:06 +08:00
sjlleo
982e1064c2 update: sudo check first 2022-05-23 09:45:59 +08:00
sjlleo
5ff461af42 update: apt update first 2022-05-23 09:43:45 +08:00
sjlleo
8adc98a753 update: One-Key Install Script 2022-05-23 09:41:42 +08:00
sjlleo
937113ca33 add: 一键安装、升级脚本 2022-05-23 09:39:23 +08:00
zhshch2002
9f0c62506e update: README.md 快速安装脚本 2022-05-22 21:47:56 +08:00
sjlleo
cad5f944cb 完善 2022-05-22 21:23:39 +08:00
sjlleo
0f0fb91fb6 Add: Thank Member 2022-05-22 20:26:48 +08:00
sjlleo
e1c6f1ccf6 add: Thanks Member 2022-05-22 20:23:05 +08:00
sjlleo
5ac811bbae update: 完善ReadMe描述 2022-05-22 20:18:28 +08:00
zhshch2002
dbd8ae573c Merge branch 'main' of https://github.com/xgadget-lab/nexttrace 2022-05-22 20:03:43 +08:00
sjlleo
8db4c5e7b8 Add: logo.svg file 2022-05-22 20:02:54 +08:00
zhshch2002
7db2a717a4 Merge branch 'main' of https://github.com/xgadget-lab/nexttrace 2022-05-22 20:02:47 +08:00
zhshch2002
b0ba116c91 update: README.md 2022-05-22 20:01:48 +08:00
sjlleo
5f993961ed update: fix channel closed, printer imrove 2022-05-22 19:55:27 +08:00
sjlleo
ead46decf6 Add: IPv6 Traceroute 2022-05-22 15:38:05 +08:00
sjlleo
7712ebf953 Add mips 2022-05-19 14:58:09 +08:00
sjlleo
14bbc62358 Add linux mips release 2022-05-19 14:48:25 +08:00
sjlleo
4323021f96 Change Token 2022-05-19 14:31:26 +08:00
sjlleo
cbfb37f37b Update build.yml 2022-05-19 14:00:50 +08:00
sjlleo
50d594e4df Add: icmp trace 2022-05-19 13:35:50 +08:00
sjlleo
6b08727993 update: 暂不关闭hopCh管道,原因待查 2022-05-18 21:19:19 +08:00
22 changed files with 1096 additions and 58 deletions

View File

@@ -5,7 +5,7 @@ set -e
DIST_PREFIX="nexttrace"
DEBUG_MODE=${2}
TARGET_DIR="dist"
PLATFORMS="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64"
PLATFORMS="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/mips"
rm -rf ${TARGET_DIR}
mkdir ${TARGET_DIR}

View File

@@ -24,5 +24,6 @@ jobs:
dist/nexttrace_darwin_arm64
dist/nexttrace_linux_amd64
dist/nexttrace_linux_arm64
dist/nexttrace_linux_mips
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GT_Token }}

View File

@@ -1,14 +1,65 @@
<div align="center">
<img src="asset/logo.png" height="200px"/>
</div>
# NextTrace
可视化路由跟踪工具
一款开源的可视化路由跟踪工具使用Golang开发。
## How To Use
### Install
```bash
bash -c "$(curl -Ls https://raw.githubusercontent.com/xgadget-lab/nexttrace/main/nt_install.sh)"
```
### Get Started
`NextTrace`默认使用`icmp`协议发起`TraceRoute`请求,该协议同时支持`IPv4``IPv6`
```bash
# IPv4 ICMP Trace
# Tips: 非实时数据一次性出结果需要等待30s左右
./nexttrace 1.0.0.1
# Tips: 如果您需要ICMP的实时结果请用如下命令
./nexttrace -report 1.0.0.1
# IPv6 ICMP Trace
./nexttrace 2606:4700:4700::1111
```
`NextTrace`也可以使用`TCP``UDP`协议发起`Traceroute`请求,不过目前只支持`IPv4`
```bash
# TCP SYN Trace
./nexttrace -T www.bing.com
# 可以自行指定端口[此处为443]默认80端口
./nexttrace -T -p 443 1.0.0.1
# UDP Trace
./nexttrace -U 1.0.0.1
./nexttrace -U -p 53 1.0.0.1
```
### IP数据库
目前使用的IP数据库默认为我们自己搭建的API服务如果后期遇到滥用我们可能会选择关闭。
我们也会在后期开放服务端源代码您也可以根据该项目的源码自行搭建属于您的API服务器。
NextTrace所有的的IP地理位置`API DEMO`可以参考[这里](https://github.com/xgadget-lab/nexttrace/blob/main/ipgeo/)
### 全部用法详见Usage菜单
```shell
NextTrace v0.1.0 Alpha
xgadget-lab zhshch (xzhsh.ch) & leo (leo.moe)
Usage of nexttrace:
-T Use TCP SYN for tracerouting (default port is 80 in TCP, 53 in UDP)
-T Use TCP SYN for tracerouting (default port is 80)
-U Use UDP Package for tracerouting (default port is 53 in UDP)
-d string
Choose IP Geograph Data Provider [LeoMoeAPI, IP.SB, IPInfo, IPInsight] (default "LeoMoeAPI")
-displayMode string
@@ -24,10 +75,20 @@ Usage of nexttrace:
-rdns
Set whether rDNS will be display
-report
Auto-Generate a Route-Path Report by Traceroute
Route Path
```
## 项目截图
![](asset/screenshot.png)
## Thanks
Vincent Young (i@yyt.moe)
[Sam Sam](https://github.com/samleong123) (samsam123@samsam123.name.my)
[Vincent Young](https://github.com/missuo) (i@yyt.moe)
[waiting4new](https://github.com/waiting4new)
FFEE_CO
nsnnns

BIN
asset/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
asset/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

2
go.mod
View File

@@ -5,13 +5,13 @@ go 1.18
require (
github.com/google/gopacket v1.1.19
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
)
require (
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/panjf2000/ants/v2 v2.5.0 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
)
require (

View File

@@ -8,6 +8,6 @@ type tokenData struct {
var token = tokenData{
ipinsight: "",
ipinfo: "42764a944dabd0",
ipinfo: "",
ipleo: "NextTraceDemo",
}

30
main.go
View File

@@ -14,15 +14,17 @@ import (
"github.com/xgadget-lab/nexttrace/util"
)
var tcpSYNFlag = flag.Bool("T", false, "Use TCP SYN for tracerouting (default port is 80 in TCP, 53 in UDP)")
var tcpSYNFlag = flag.Bool("T", false, "Use TCP SYN for tracerouting (default port is 80)")
var udpPackageFlag = flag.Bool("U", false, "Use UDP Package for tracerouting (default port is 53 in UDP)")
var port = flag.Int("p", 80, "Set SYN Traceroute Port")
var numMeasurements = flag.Int("q", 3, "Set the number of probes per each hop.")
var parallelRequests = flag.Int("r", 18, "Set ParallelRequests number. It should be 1 when there is a multi-routing.")
var maxHops = flag.Int("m", 30, "Set the max number of hops (max TTL to be reached).")
var dataOrigin = flag.String("d", "LeoMoeAPI", "Choose IP Geograph Data Provider [LeoMoeAPI, IP.SB, IPInfo, IPInsight]")
var displayMode = flag.String("displayMode", "table", "Choose The Display Mode [table, classic]")
var rdnsenable = flag.Bool("rdns", false, "Set whether rDNS will be display")
var routePath = flag.Bool("report", false, "Route Path")
var realtimePrint = flag.Bool("realtime", false, "Output trace results in runtime")
var tablePrint = flag.Bool("table", false, "Output trace results as table")
func flagApply() string {
flag.Parse()
@@ -44,10 +46,14 @@ func main() {
printer.PrintTraceRouteNav(ip, domain, *dataOrigin)
var m trace.Method = ""
if *tcpSYNFlag {
switch {
case *tcpSYNFlag:
m = trace.TCPTrace
} else {
case *udpPackageFlag:
m = trace.UDPTrace
default:
m = trace.ICMPTrace
}
if !*tcpSYNFlag && *port == 80 {
@@ -63,8 +69,10 @@ func main() {
RDns: *rdnsenable,
IPGeoSource: ipgeo.GetSource(*dataOrigin),
Timeout: 2 * time.Second,
}
//Quic: false,
if m == trace.ICMPTrace && !*tablePrint {
conf.RealtimePrinter = printer.RealtimePrinter
}
res, err := trace.Traceroute(m, conf)
@@ -79,9 +87,15 @@ func main() {
return
}
if *displayMode == "table" {
if m == trace.ICMPTrace && *tablePrint {
printer.TracerouteTablePrinter(res)
} else {
printer.TraceroutePrinter(res)
}
if m == trace.TCPTrace || m == trace.UDPTrace {
if *realtimePrint {
printer.TraceroutePrinter(res)
} else {
printer.TracerouteTablePrinter(res)
}
}
}

215
nt_install.sh Normal file
View File

@@ -0,0 +1,215 @@
#!/bin/bash
usrPath="/usr/local/bin"
checkRootPermit() {
[[ $EUID -ne 0 ]] && echo "请使用sudo/root权限运行本脚本" && exit 1
}
checkSystemArch() {
arch=$(uname -m)
if [[ $arch == "x86_64" ]]; then
archParam="amd64"
fi
if [[ $arch == "aarch64" ]]; then
archParam="arm64"
fi
}
checkSystemDistribution() {
case "$OSTYPE" in
darwin*)
osDistribution="darwin"
downPath="/var/tmp/nexttrace"
;;
linux*)
osDistribution="linux"
downPath="/var/tmp/nexttrace"
;;
*)
echo "unknown: $OSTYPE"
exit 1
;;
esac
}
getLocation() {
echo "正在获取地理位置信息..."
countryCode=$(curl -s "http://ip-api.com/line/?fields=countryCode")
}
installWgetPackage() {
# macOS should install wget originally. Nothing to do
echo "wget 正在安装中..."
# try apt
apt -h &> /dev/null
if [ $? -eq 0 ]; then
# 先更新一下数据源有些机器数据源比较老可能会404
apt update -y &> /dev/null
apt install wget -y &> /dev/null
fi
# try yum
yum -h &> /dev/null
if [ $? -eq 0 ]; then
yum install wget -y &> /dev/null
fi
# try dnf
dnf -h &> /dev/null
if [ $? -eq 0 ]; then
dnf install wget -y &> /dev/null
fi
# try pacman
pacman -h &> /dev/null
if [ $? -eq 0 ]; then
pacman -Sy
pacman -S wget
fi
}
installJqPackage() {
# macOS should install wget originally. Nothing to do
echo "jq 正在安装中..."
# try apt
apt -h &> /dev/null
if [ $? -eq 0 ]; then
# 先更新一下数据源有些机器数据源比较老可能会404
apt update -y &> /dev/null
apt install jq -y &> /dev/null
fi
# try yum
yum -h &> /dev/null
if [ $? -eq 0 ]; then
yum install jq -y &> /dev/null
fi
# try dnf
dnf -h &> /dev/null
if [ $? -eq 0 ]; then
dnf install jq -y &> /dev/null
fi
# try pacman
pacman -h &> /dev/null
if [ $? -eq 0 ]; then
pacman -Sy
pacman -S jq
fi
}
checkWgetPackage() {
wget -h &> /dev/null
if [ $? -ne 0 ]; then
read -r -p "您还没有安装wget是否安装? (y/n)" input
case $input in
[yY][eE][sS]|[yY])
installWgetPackage
;;
[nN][oO]|[nN])
echo "您选择了取消安装,脚本即将退出"
exit 1
;;
*)
installWgetPackage
;;
esac
fi
}
checkJqPackage() {
jq -h &> /dev/null
if [ $? -ne 0 ]; then
echo "您还没有安装jq 当您取消安装我们会使用awk获取当前版本号。"
read -r -p "但是如遇Github变更API这可能会存在问题是否安装? (y/n)" input
case $input in
[yY][eE][sS]|[yY])
installJqPackage
;;
[nN][oO]|[nN])
echo "您选择了取消安装"
return 0
;;
*)
installJqPackage
;;
esac
fi
return 1
}
downloadBinrayFile() {
echo "正在获取最新版的 NextTrace 发行版文件信息..."
checkJqPackage
# 简单说明一下Github提供了一个API可以获取最新发行版本的二进制文件下载地址对应的是browser_download_url根据刚刚测得的osDistribution、archParam获取对应的下载地址
if [[ $? -eq 1 ]]; then
# 支持 jq 不回退
# echo nexttrace_${osDistribution}_${archParam}
latestURL=$(curl -s https://api.github.com/repos/xgadget-lab/nexttrace/releases/latest | jq ".assets[] | select(.name == \"nexttrace_${osDistribution}_${archParam}\") | .browser_download_url" | sed 's/\"//g')
else
# 不支持 jq用户拒绝安装回退 awk
latestURL=$(curl -s https://api.github.com/repos/xgadget-lab/nexttrace/releases/latest | grep -i "browser_download_url.*${osDistribution}.*${archParam}" | awk -F '"' '{print $4}')
fi
if [ "$countryCode" == "CN" ]; then
read -r -p "检测到国内网络环境,是否使用镜像下载以加速(y/n)" input
case $input in
[yY][eE][sS]|[yY])
latestURL="https://ghproxy.com/"$latestURL
;;
[nN][oO]|[nN])
echo "您选择了不使用镜像,下载可能会变得异常缓慢,或者失败"
;;
*)
latestURL="https://ghproxy.com/"$latestURL
;;
esac
fi
echo "正在下载 NextTrace 二进制文件..."
wget -O ${downPath} ${latestURL} &> /dev/null
if [ $? -eq 0 ];
then
echo "NextTrace 现在已经在您的系统中可用"
changeMode
mv ${downPath} ${usrPath}
else
echo "NextTrace 下载失败,请检查您的网络是否正常"
exit 1
fi
}
changeMode() {
chmod +x ${downPath} &> /dev/null
}
runBinrayFileHelp() {
if [ -e ${usrPath} ]; then
${usrPath}/nexttrace -h
fi
}
# Check Procedure
checkRootPermit
checkSystemDistribution
checkSystemArch
checkWgetPackage
# Download Procedure
getLocation
downloadBinrayFile
# Run Procedure
runBinrayFileHelp

View File

@@ -5,10 +5,6 @@ import (
"net"
)
func PrintCopyRight() {
fmt.Println("NextTrace v0.1.0 Alpha \nxgadget-lab zhshch (xzhsh.ch) & leo (leo.moe)")
}
func PrintTraceRouteNav(ip net.IP, domain string, dataOrigin string) {
fmt.Println("IP Geo Data Provider: " + dataOrigin)

View File

@@ -2,9 +2,10 @@ package printer
import (
"fmt"
"github.com/xgadget-lab/nexttrace/trace"
"strings"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/xgadget-lab/nexttrace/ipgeo"
)
@@ -14,12 +15,12 @@ func TraceroutePrinter(res *trace.Result) {
for i, hop := range res.Hops {
fmt.Print(i + 1)
for _, h := range hop {
hopPrinter(h)
HopPrinter(h)
}
}
}
func hopPrinter(h trace.Hop) {
func HopPrinter(h trace.Hop) {
if h.Address == nil {
fmt.Println("\t*")
} else {

View File

@@ -0,0 +1,15 @@
package printer
import (
"fmt"
"github.com/xgadget-lab/nexttrace/trace"
)
func RealtimePrinter(res *trace.Result, ttl int) {
fmt.Print(ttl)
for i := range res.Hops[ttl] {
HopPrinter(res.Hops[ttl][i])
}
}

View File

@@ -2,9 +2,10 @@ package printer
import (
"fmt"
"github.com/xgadget-lab/nexttrace/trace"
"strings"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/fatih/color"
"github.com/rodaine/table"
)
@@ -30,7 +31,16 @@ func TracerouteTablePrinter(res *trace.Result) {
if k > 0 {
data.Hop = ""
}
tbl.AddRow(data.Hop, data.IP, data.Latency, data.Asnumber, data.Country, data.Prov, data.City, data.Owner)
if data.Country == "" && data.Prov == "" && data.City == "" {
tbl.AddRow(data.Hop, data.IP, data.Latency, data.Asnumber, "", data.Owner)
} else {
if data.City != "" {
tbl.AddRow(data.Hop, data.IP, data.Latency, data.Asnumber, data.Country+", "+data.Prov+", "+data.City, data.Owner)
} else {
tbl.AddRow(data.Hop, data.IP, data.Latency, data.Asnumber, data.Country, data.Owner)
}
}
}
}
// 打印表格
@@ -42,7 +52,7 @@ func New() table.Table {
headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
columnFmt := color.New(color.FgYellow).SprintfFunc()
tbl := table.New("Hop", "IP", "Lantency", "ASN", "Country", "Province", "City", "Owner")
tbl := table.New("Hop", "IP", "Lantency", "ASN", "Location", "Owner")
tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
return tbl
}

View File

@@ -42,14 +42,16 @@ func experimentTag() {
func (r *reporter) generateRouteReportNode(ip string, ipGeoData ipgeo.IPGeoData) (routeReportNode, error) {
rpn := routeReportNode{}
ptr, err := net.LookupAddr(ip)
if err == nil {
if strings.Contains(strings.ToLower(ptr[0]), "ix") {
rpn.ix = true
} else {
rpn.ix = false
go func() {
ptr, err := net.LookupAddr(ip)
if err == nil {
if strings.Contains(strings.ToLower(ptr[0]), "ix") {
rpn.ix = true
} else {
rpn.ix = false
}
}
}
}()
if strings.Contains(strings.ToLower(ipGeoData.Isp), "exchange") || strings.Contains(strings.ToLower(ipGeoData.Isp), "ix") || strings.Contains(strings.ToLower(ipGeoData.Owner), "exchange") || strings.Contains(strings.ToLower(ipGeoData.Owner), "ix") {
rpn.ix = true
@@ -60,7 +62,7 @@ func (r *reporter) generateRouteReportNode(ip string, ipGeoData ipgeo.IPGeoData)
rpn.asn = ipGeoData.Asnumber
}
// 无论最后一跳是否为存在地理位置信息AnyCast都应该给予显示
if ipGeoData.Country == "" || ipGeoData.City == "" && ip != r.targetIP {
if ipGeoData.Country == "" || ipGeoData.City == "" || ipGeoData.City == "-" && ip != r.targetIP {
return rpn, errors.New("GeoData Search Failed")
} else {
if ipGeoData.City == "" {

175
trace/icmp_ipv4.go Normal file
View File

@@ -0,0 +1,175 @@
package trace
import (
"log"
"net"
"os"
"sync"
"time"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
type ICMPTracer struct {
Config
wg sync.WaitGroup
res Result
ctx context.Context
resCh chan Hop
icmpListen net.PacketConn
final int
finalLock sync.Mutex
}
func (t *ICMPTracer) Execute() (*Result, error) {
if len(t.res.Hops) > 0 {
return &t.res, ErrTracerouteExecuted
}
var err error
t.icmpListen, err = net.ListenPacket("ip4:1", "0.0.0.0")
if err != nil {
return &t.res, err
}
defer t.icmpListen.Close()
var cancel context.CancelFunc
t.ctx, cancel = context.WithCancel(context.Background())
defer cancel()
t.resCh = make(chan Hop)
t.final = -1
go t.listenICMP()
for ttl := 1; ttl <= t.MaxHops; ttl++ {
if t.final != -1 && ttl > t.final {
break
}
for i := 0; i < t.NumMeasurements; i++ {
t.wg.Add(1)
go t.send(ttl)
}
// 一组TTL全部退出收到应答或者超时终止以后再进行下一个TTL的包发送
t.wg.Wait()
if t.RealtimePrinter != nil {
t.RealtimePrinter(&t.res, ttl-1)
}
}
t.res.reduce(t.final)
return &t.res, nil
}
func (t *ICMPTracer) listenICMP() {
lc := NewPacketListener(t.icmpListen, t.ctx)
go lc.Start()
for {
select {
case <-t.ctx.Done():
return
case msg := <-lc.Messages:
if msg.N == nil {
continue
}
rm, err := icmp.ParseMessage(1, msg.Msg[:*msg.N])
if err != nil {
log.Println(err)
continue
}
switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
t.handleICMPMessage(msg, 0, rm.Body.(*icmp.TimeExceeded).Data)
case ipv4.ICMPTypeEchoReply:
t.handleICMPMessage(msg, 1, rm.Body.(*icmp.Echo).Data)
default:
// log.Println("received icmp message of unknown type", rm.Type)
}
}
}
}
func (t *ICMPTracer) handleICMPMessage(msg ReceivedMessage, icmpType int8, data []byte) {
t.resCh <- Hop{
Success: true,
Address: msg.Peer,
}
}
func (t *ICMPTracer) send(ttl int) error {
defer t.wg.Done()
if t.final != -1 && ttl > t.final {
return nil
}
icmpHeader := icmp.Message{
Type: ipv4.ICMPTypeEcho, Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Data: []byte("HELLO-R-U-THERE"),
},
}
ipv4.NewPacketConn(t.icmpListen).SetTTL(ttl)
wb, err := icmpHeader.Marshal(nil)
if err != nil {
log.Fatal(err)
}
start := time.Now()
if _, err := t.icmpListen.WriteTo(wb, &net.IPAddr{IP: t.DestIP}); err != nil {
log.Fatal(err)
}
if err := t.icmpListen.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
log.Fatal(err)
}
select {
case <-t.ctx.Done():
return nil
case h := <-t.resCh:
rtt := time.Since(start)
if t.final != -1 && ttl > t.final {
return nil
}
if addr, ok := h.Address.(*net.IPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
} else if addr, ok := h.Address.(*net.TCPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
}
h.TTL = ttl
h.RTT = rtt
h.fetchIPData(t.Config)
t.res.add(h)
case <-time.After(t.Timeout):
if t.final != -1 && ttl > t.final {
return nil
}
t.res.add(Hop{
Success: false,
Address: nil,
TTL: ttl,
RTT: 0,
Error: ErrHopLimitTimeout,
})
}
return nil
}

179
trace/icmp_ipv6.go Normal file
View File

@@ -0,0 +1,179 @@
package trace
import (
"log"
"net"
"os"
"sync"
"time"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv6"
)
type ICMPTracerv6 struct {
Config
wg sync.WaitGroup
res Result
ctx context.Context
resCh chan Hop
icmpListen net.PacketConn
final int
finalLock sync.Mutex
}
func (t *ICMPTracerv6) Execute() (*Result, error) {
if len(t.res.Hops) > 0 {
return &t.res, ErrTracerouteExecuted
}
var err error
t.icmpListen, err = net.ListenPacket("ip6:58", "::")
if err != nil {
return &t.res, err
}
defer t.icmpListen.Close()
var cancel context.CancelFunc
t.ctx, cancel = context.WithCancel(context.Background())
defer cancel()
t.resCh = make(chan Hop)
t.final = -1
go t.listenICMP()
for ttl := 1; ttl <= t.MaxHops; ttl++ {
if t.final != -1 && ttl > t.final {
break
}
for i := 0; i < t.NumMeasurements; i++ {
t.wg.Add(1)
go t.send(ttl)
}
// 一组TTL全部退出收到应答或者超时终止以后再进行下一个TTL的包发送
t.wg.Wait()
if t.RealtimePrinter != nil {
t.RealtimePrinter(&t.res, ttl-1)
}
}
t.res.reduce(t.final)
return &t.res, nil
}
func (t *ICMPTracerv6) listenICMP() {
lc := NewPacketListener(t.icmpListen, t.ctx)
go lc.Start()
for {
select {
case <-t.ctx.Done():
return
case msg := <-lc.Messages:
if msg.N == nil {
continue
}
rm, err := icmp.ParseMessage(58, msg.Msg[:*msg.N])
if err != nil {
log.Println(err)
continue
}
// log.Println(msg.Peer)
switch rm.Type {
case ipv6.ICMPTypeTimeExceeded:
t.handleICMPMessage(msg, 0, rm.Body.(*icmp.TimeExceeded).Data)
case ipv6.ICMPTypeEchoReply:
t.handleICMPMessage(msg, 1, rm.Body.(*icmp.Echo).Data)
default:
// log.Println("received icmp message of unknown type", rm.Type)
}
}
}
}
func (t *ICMPTracerv6) handleICMPMessage(msg ReceivedMessage, icmpType int8, data []byte) {
t.resCh <- Hop{
Success: true,
Address: msg.Peer,
}
}
func (t *ICMPTracerv6) send(ttl int) error {
defer t.wg.Done()
if t.final != -1 && ttl > t.final {
return nil
}
icmpHeader := icmp.Message{
Type: ipv6.ICMPTypeEchoRequest, Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Data: []byte("HELLO-R-U-THERE"),
},
}
p := ipv6.NewPacketConn(t.icmpListen)
icmpHeader.Body.(*icmp.Echo).Seq = ttl
p.SetHopLimit(ttl)
wb, err := icmpHeader.Marshal(nil)
if err != nil {
log.Fatal(err)
}
start := time.Now()
if _, err := t.icmpListen.WriteTo(wb, &net.IPAddr{IP: t.DestIP}); err != nil {
log.Fatal(err)
}
if err := t.icmpListen.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
log.Fatal(err)
}
select {
case <-t.ctx.Done():
return nil
case h := <-t.resCh:
rtt := time.Since(start)
if t.final != -1 && ttl > t.final {
return nil
}
if addr, ok := h.Address.(*net.IPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
} else if addr, ok := h.Address.(*net.TCPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
}
h.TTL = ttl
h.RTT = rtt
h.fetchIPData(t.Config)
t.res.add(h)
case <-time.After(t.Timeout):
if t.final != -1 && ttl > t.final {
return nil
}
t.res.add(Hop{
Success: false,
Address: nil,
TTL: ttl,
RTT: 0,
Error: ErrHopLimitTimeout,
})
}
return nil
}

View File

@@ -163,6 +163,7 @@ func (t *TCPTracer) handleICMPMessage(msg ReceivedMessage, data []byte) {
Success: true,
Address: msg.Peer,
}
}
func (t *TCPTracer) send(ttl int) error {
@@ -218,12 +219,15 @@ func (t *TCPTracer) send(ttl int) error {
hopCh := make(chan Hop)
t.inflightRequest[int(sequenceNumber)] = hopCh
t.inflightRequestLock.Unlock()
defer func() {
t.inflightRequestLock.Lock()
close(hopCh)
delete(t.inflightRequest, srcPort)
t.inflightRequestLock.Unlock()
}()
/*
// 这里属于 2个SenderN个Reciever的情况在哪里关闭Channel都容易导致Panic
defer func() {
t.inflightRequestLock.Lock()
close(hopCh)
delete(t.inflightRequest, srcPort)
t.inflightRequestLock.Unlock()
}()
*/
select {
case <-t.ctx.Done():
return nil

269
trace/tcp_ipv6.go Normal file
View File

@@ -0,0 +1,269 @@
package trace
import (
"log"
"math"
"math/rand"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/xgadget-lab/nexttrace/listener_channel"
"github.com/xgadget-lab/nexttrace/util"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv6"
"golang.org/x/sync/semaphore"
)
type TCPTracerv6 struct {
Config
wg sync.WaitGroup
res Result
ctx context.Context
inflightRequest map[int]chan Hop
inflightRequestLock sync.Mutex
SrcIP net.IP
icmp net.PacketConn
tcp net.PacketConn
final int
finalLock sync.Mutex
sem *semaphore.Weighted
}
func (t *TCPTracerv6) Execute() (*Result, error) {
if len(t.res.Hops) > 0 {
return &t.res, ErrTracerouteExecuted
}
t.SrcIP, _ = util.LocalIPPort(t.DestIP)
log.Println(util.LocalIPPort(t.DestIP))
var err error
t.tcp, err = net.ListenPacket("ip6:tcp", t.SrcIP.String())
if err != nil {
return nil, err
}
t.icmp, err = icmp.ListenPacket("ip6:53", "::")
if err != nil {
return &t.res, err
}
defer t.icmp.Close()
var cancel context.CancelFunc
t.ctx, cancel = context.WithCancel(context.Background())
defer cancel()
t.inflightRequest = make(map[int]chan Hop)
t.final = -1
go t.listenICMP()
go t.listenTCP()
t.sem = semaphore.NewWeighted(int64(t.ParallelRequests))
for ttl := 1; ttl <= t.MaxHops; ttl++ {
for i := 0; i < t.NumMeasurements; i++ {
t.wg.Add(1)
go t.send(ttl)
}
time.Sleep(1 * time.Millisecond)
}
t.wg.Wait()
t.res.reduce(t.final)
return &t.res, nil
}
func (t *TCPTracerv6) listenICMP() {
lc := NewPacketListener(t.icmp, t.ctx)
go lc.Start()
for {
select {
case <-t.ctx.Done():
return
case msg := <-lc.Messages:
if msg.N == nil {
continue
}
rm, err := icmp.ParseMessage(53, msg.Msg[:*msg.N])
if err != nil {
log.Println(err)
continue
}
log.Println(msg.Peer)
switch rm.Type {
case ipv6.ICMPTypeTimeExceeded:
t.handleICMPMessage(msg, rm.Body.(*icmp.TimeExceeded).Data)
case ipv6.ICMPTypeDestinationUnreachable:
t.handleICMPMessage(msg, rm.Body.(*icmp.DstUnreach).Data)
default:
//log.Println("received icmp message of unknown type", rm.Type)
}
}
}
}
// @title listenTCP
// @description 监听TCP的响应数据包
func (t *TCPTracerv6) listenTCP() {
lc := listener_channel.New(t.tcp)
defer lc.Stop()
go lc.Start()
for {
select {
case <-t.ctx.Done():
return
case msg := <-lc.Messages:
if msg.N == nil {
continue
}
if msg.Peer.String() != t.DestIP.String() {
continue
}
// 解包
packet := gopacket.NewPacket(msg.Msg[:*msg.N], layers.LayerTypeTCP, gopacket.Default)
// 从包中获取TCP layer信息
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
tcp, _ := tcpLayer.(*layers.TCP)
// 取得目标主机的Sequence Number
if ch, ok := t.inflightRequest[int(tcp.Ack-1)]; ok {
// 最后一跳
ch <- Hop{
Success: true,
Address: msg.Peer,
}
}
}
}
}
}
func (t *TCPTracerv6) handleICMPMessage(msg ReceivedMessage, data []byte) {
header, err := util.GetICMPResponsePayload(data)
if err != nil {
return
}
sequenceNumber := util.GetTCPSeq(header)
t.inflightRequestLock.Lock()
defer t.inflightRequestLock.Unlock()
ch, ok := t.inflightRequest[int(sequenceNumber)]
if !ok {
return
}
ch <- Hop{
Success: true,
Address: msg.Peer,
}
}
func (t *TCPTracerv6) send(ttl int) error {
err := t.sem.Acquire(context.Background(), 1)
if err != nil {
return err
}
defer t.sem.Release(1)
defer t.wg.Done()
if t.final != -1 && ttl > t.final {
return nil
}
// 随机种子
r := rand.New(rand.NewSource(time.Now().UnixNano()))
_, srcPort := util.LocalIPPort(t.DestIP)
ipHeader := &layers.IPv6{
SrcIP: t.SrcIP,
DstIP: t.DestIP,
NextHeader: layers.IPProtocolTCP,
HopLimit: uint8(ttl),
}
// 使用Uint16兼容32位系统防止在rand的时候因使用int32而溢出
sequenceNumber := uint32(r.Intn(math.MaxUint16))
tcpHeader := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(t.DestPort),
Seq: sequenceNumber,
SYN: true,
Window: 14600,
}
_ = tcpHeader.SetNetworkLayerForChecksum(ipHeader)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err := gopacket.SerializeLayers(buf, opts, tcpHeader); err != nil {
return err
}
ipv6.NewPacketConn(t.tcp).SetHopLimit(ttl)
if err != nil {
return err
}
start := time.Now()
if _, err := t.tcp.WriteTo(buf.Bytes(), &net.IPAddr{IP: t.DestIP}); err != nil {
return err
}
t.inflightRequestLock.Lock()
hopCh := make(chan Hop)
t.inflightRequest[int(sequenceNumber)] = hopCh
t.inflightRequestLock.Unlock()
select {
case <-t.ctx.Done():
return nil
case h := <-hopCh:
rtt := time.Since(start)
if t.final != -1 && ttl > t.final {
return nil
}
if addr, ok := h.Address.(*net.IPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
} else if addr, ok := h.Address.(*net.TCPAddr); ok && addr.IP.Equal(t.DestIP) {
t.finalLock.Lock()
if t.final == -1 || ttl < t.final {
t.final = ttl
}
t.finalLock.Unlock()
}
h.TTL = ttl
h.RTT = rtt
h.fetchIPData(t.Config)
t.res.add(h)
case <-time.After(t.Timeout):
if t.final != -1 && ttl > t.final {
return nil
}
t.res.add(Hop{
Success: false,
Address: nil,
TTL: ttl,
RTT: 0,
Error: ErrHopLimitTimeout,
})
}
return nil
}

76
trace/temp_printer.go Normal file
View File

@@ -0,0 +1,76 @@
package trace
import (
"fmt"
"strings"
"github.com/xgadget-lab/nexttrace/ipgeo"
)
func HopPrinter(h Hop) {
if h.Address == nil {
fmt.Println("\t*")
} else {
txt := "\t"
if h.Hostname == "" {
txt += fmt.Sprint(h.Address, " ", fmt.Sprintf("%.2f", h.RTT.Seconds()*1000), "ms")
} else {
txt += fmt.Sprint(h.Hostname, " (", h.Address, ") ", fmt.Sprintf("%.2f", h.RTT.Seconds()*1000), "ms")
}
if h.Geo != nil {
txt += " " + formatIpGeoData(h.Address.String(), h.Geo)
}
fmt.Println(txt)
}
}
func formatIpGeoData(ip string, data *ipgeo.IPGeoData) string {
var res = make([]string, 0, 10)
if data.Asnumber == "" {
res = append(res, "*")
} else {
res = append(res, "AS"+data.Asnumber)
}
// TODO: 判断阿里云和腾讯云内网,数据不足,有待进一步完善
// TODO: 移动IDC判断到Hop.fetchIPData函数减少API调用
if strings.HasPrefix(ip, "9.") {
res = append(res, "局域网", "腾讯云")
} else if strings.HasPrefix(ip, "11.") {
res = append(res, "局域网", "阿里云")
} else if data.Country == "" {
res = append(res, "局域网")
} else {
// 有些IP的归属信息为空这个时候将ISP的信息填入
if data.Owner == "" {
data.Owner = data.Isp
}
if data.District != "" {
data.City = data.City + ", " + data.District
}
if data.Prov == "" && data.City == "" {
// anyCast或是骨干网数据不应该有国家信息
data.Owner = data.Owner + ", " + data.Owner
} else {
// 非骨干网正常填入IP的国家信息数据
res = append(res, data.Country)
}
if data.Prov != "" {
res = append(res, data.Prov)
}
if data.City != "" {
res = append(res, data.City)
}
if data.Owner != "" {
res = append(res, data.Owner)
}
}
return strings.Join(res, ", ")
}

View File

@@ -25,13 +25,15 @@ type Config struct {
Quic bool
IPGeoSource ipgeo.Source
RDns bool
RealtimePrinter func(res *Result, ttl int)
}
type Method string
const (
UDPTrace Method = "udp"
TCPTrace Method = "tcp"
ICMPTrace Method = "icmp"
UDPTrace Method = "udp"
TCPTrace Method = "tcp"
)
type Tracer interface {
@@ -52,10 +54,26 @@ func Traceroute(method Method, config Config) (*Result, error) {
}
switch method {
case ICMPTrace:
if config.DestIP.To4() != nil {
tracer = &ICMPTracer{Config: config}
} else {
tracer = &ICMPTracerv6{Config: config}
}
case UDPTrace:
tracer = &UDPTracer{Config: config}
if config.DestIP.To4() != nil {
tracer = &UDPTracer{Config: config}
} else {
return nil, errors.New("IPv6 UDP Traceroute is not supported")
}
case TCPTrace:
tracer = &TCPTracer{Config: config}
if config.DestIP.To4() != nil {
tracer = &TCPTracer{Config: config}
} else {
// tracer = &TCPTracerv6{Config: config}
return nil, errors.New("IPv6 TCP Traceroute is not supported")
}
default:
return &Result{}, ErrInvalidMethod
}

View File

@@ -1,6 +1,11 @@
package trace
import (
"log"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/xgadget-lab/nexttrace/util"
@@ -8,10 +13,6 @@ import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/sync/semaphore"
"log"
"net"
"sync"
"time"
)
type UDPTracer struct {
@@ -99,8 +100,8 @@ func (t *UDPTracer) handleICMPMessage(msg ReceivedMessage, data []byte) {
return
}
srcPort := util.GetUDPSrcPort(header)
t.inflightRequestLock.Lock()
defer t.inflightRequestLock.Unlock()
//t.inflightRequestLock.Lock()
//defer t.inflightRequestLock.Unlock()
ch, ok := t.inflightRequest[int(srcPort)]
if !ok {
return
@@ -128,7 +129,6 @@ func (t *UDPTracer) getUDPConn(try int) (net.IP, int, net.PacketConn) {
}
return t.getUDPConn(try + 1)
}
return srcIP, udpConn.LocalAddr().(*net.UDPAddr).Port, udpConn
}
@@ -184,6 +184,7 @@ func (t *UDPTracer) send(ttl int) error {
return err
}
// 在对inflightRequest进行写操作的时候应该加锁保护以免多个goroutine协程试图同时写入造成panic
t.inflightRequestLock.Lock()
hopCh := make(chan Hop)
t.inflightRequest[srcPort] = hopCh

View File

@@ -36,12 +36,13 @@ func DomainLookUp(host string) net.IP {
var ipv6Flag = false
for _, ip := range ips {
ipSlice = append(ipSlice, ip)
// 仅返回ipv4的ip
if ip.To4() != nil {
ipSlice = append(ipSlice, ip)
} else {
ipv6Flag = true
}
// if ip.To4() != nil {
// ipSlice = append(ipSlice, ip)
// } else {
// ipv6Flag = true
// }
}
if ipv6Flag {