Compare commits

...

87 Commits

Author SHA1 Message Date
zhshch2002
870d1f3b5a update: get version from git 2022-05-25 12:56:47 +08:00
sjlleo
ebd435db53 refactor: report 支持多线程和rDNS反查 2022-05-25 10:48:47 +08:00
sjlleo
53b2249ce5 fix: 尝试性修复部分 CentOS 6、Debian8 下 wget 安装失败 2022-05-25 08:13:57 +08:00
sjlleo
7215a1e2b7 update: 更新 NextTrace 最新的 CLI 参数 2022-05-24 21:33:18 +08:00
sjlleo
24b06d2fd7 fix: strange error 2022-05-24 21:10:28 +08:00
sjlleo
f1f95dff29 fix: route-path 显示不全的问题 2022-05-24 21:05:59 +08:00
zhshch2002
0d9b8c8861 fix: typo 2022-05-24 18:27:22 +08:00
zhshch2002
290524b502 add: Version print 2022-05-24 18:14:31 +08:00
sjlleo
905ef267f2 improve 2022-05-24 11:57:50 +08:00
sjlleo
9720c19153 add: route report 2022-05-24 11:57:10 +08:00
sjlleo
f2c494441b Add files via upload 2022-05-24 11:45:02 +08:00
sjlleo
44aba64505 Delete screenshot2.png 2022-05-24 11:44:30 +08:00
sjlleo
c0be6774c1 Delete screenshot3.png 2022-05-24 11:44:19 +08:00
zhshch2002
23693895e4 update: GitHub actions build.yml 2022-05-24 11:22:13 +08:00
zhshch2002
7edebf938b remove: test for TestIPSB api 2022-05-24 11:20:23 +08:00
zhshch2002
d4a176f864 add: test github actions 2022-05-24 11:17:43 +08:00
zhshch2002
69388c956e remove: listener_channe, signal, taskgroup 2022-05-24 11:17:43 +08:00
sjlleo
738ff949b1 Special Version for my friends - missuo 2022-05-24 10:59:38 +08:00
sjlleo
b59f0eb9da update: rdns 功能介绍 2022-05-24 09:21:30 +08:00
sjlleo
c7df49ca8e improve: #4 case insensitive 2022-05-24 09:05:56 +08:00
sjlleo
4d3a52bb28 Add: IP Database Copyright 2022-05-24 08:55:41 +08:00
sjlleo
fbf7335bc8 update: "./" is no longer need here 2022-05-24 05:37:19 +08:00
sjlleo
d60c80e1ff Add files via upload 2022-05-23 21:28:10 +08:00
sjlleo
4f622b7afb Add files via upload 2022-05-23 21:27:50 +08:00
sjlleo
f51b07d388 Add files via upload 2022-05-23 21:17:43 +08:00
sjlleo
679dbbf01a Delete screenshot2.png 2022-05-23 21:17:18 +08:00
sjlleo
2a5e5572b4 update: fix command not found bug 2022-05-23 21:03:44 +08:00
sjlleo
974655a1b3 update: todo update version 2022-05-23 20:55:11 +08:00
sjlleo
eb9844f924 Add: screenshots 2022-05-23 20:41:48 +08:00
sjlleo
b8a51b3c44 add: screenshots 2022-05-23 20:40:37 +08:00
sjlleo
5d984fc3ac Merge pull request #3 from tsosunchia/main
对安装脚本一点小改善
2022-05-23 20:33:30 +08:00
tsosunchia
bcea9aa2cb 安装脚本增加checkVersion为之后增加自动更新功能做准备;优化了部分逻辑结构;在部分软件包管理器增加了安装依赖包之前更新软件源的步骤 2022-05-23 20:26:53 +08:00
sjlleo
ffc2c33e63 Merge pull request #2 from tsosunchia/main
修正安装脚本在macOS运行中出现的BUG
2022-05-23 19:55:39 +08:00
tsosunchia
919335133e 修正脚本在macOS运行中出现的BUG 2022-05-23 19:29:38 +08:00
sjlleo
4d0e3eb93d update: 修改用法 2022-05-23 18:10:40 +08:00
sjlleo
56d2f0554f update: sed is uncompatible in macOS, with 1:-1 instead (Thanks to nsnnns) 2022-05-23 18:07:48 +08:00
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
sjlleo
14730bb489 Update: reporter 适配新的数据结构 2022-05-18 20:57:31 +08:00
sjlleo
b688a1ae99 Merge branch 'main' of https://github.com/xgadget-lab/nexttrace 2022-05-18 20:08:11 +08:00
sjlleo
3548c47411 refactor: tcp trace 2022-05-18 18:53:02 +08:00
sjlleo
460e8ba78d Add: add special thanks 2022-05-17 19:56:36 +08:00
zhshch2002
abe4b685c4 update: printer 2022-05-15 20:19:21 +08:00
zhshch2002
9758a93382 Merge branch 'dev/refactor-methods'
# Conflicts:
#	main.go
#	util/printer/printer.go
2022-05-15 20:16:26 +08:00
zhshch2002
6fffe31506 refactor: udp trace 2022-05-15 20:09:17 +08:00
sjlleo
b7b966b93e update: 更新使用指南 2022-05-15 14:19:30 +08:00
sjlleo
433c8656a1 update: 新增 route-path 模块 2022-05-15 11:12:13 +08:00
sjlleo
1542cb4b07 update: 骨干网判断优化 2022-05-14 16:55:50 +08:00
sjlleo
06ee8f7373 add: 新增路由报告模块接口 2022-05-14 16:55:28 +08:00
sjlleo
6792bafb02 update: 整理 2022-05-14 08:48:49 +08:00
sjlleo
70305caa1c update: 改Switch判断 (Git还我头像qwq) 2022-05-14 08:38:02 +08:00
sjlleo
671ad82780 update: 完善阿里云、腾讯云的内网识别范围 2022-05-14 08:28:30 +08:00
sjlleo
e62575beba update: 完善table显示,对阿里云、腾讯云内网的识别 2022-05-14 08:28:00 +08:00
sjlleo
d92a1e10d3 update: 完善参数提醒 2022-05-14 08:27:13 +08:00
sjlleo
971d68f93f update: 架构整理 2022-05-13 21:44:43 +08:00
sjlleo
f765dbafae update: go mod 2022-05-13 20:44:04 +08:00
zhshch2002
ea7feab2f9 update: build.yml trigger condition 2022-05-13 16:55:36 +08:00
zhshch2002
e941eaa167 update: build.yml Token 2022-05-13 16:43:25 +08:00
zhshch2002
46e32d697d fix: "lab 实验室"语意重复 2022-05-13 16:41:57 +08:00
sjlleo
97578e40f7 update: 修复一个作者名称的异常 2022-05-13 16:37:31 +08:00
43 changed files with 2629 additions and 1185 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}
@@ -21,15 +21,15 @@ for pl in ${PLATFORMS}; do
echo "build => ${TARGET}"
if [ "${DEBUG_MODE}" == "debug" ]; then
go build -trimpath -gcflags "all=-N -l" -o ${TARGET} \
-ldflags "-X 'main.version=${BUILD_VERSION}' \
-X 'main.buildDate=${BUILD_DATE}' \
-X 'main.commitID=${COMMIT_SHA1}'\
-ldflags "-X 'github.com/xgadget-lab/nexttrace/printer.version=${BUILD_VERSION}' \
-X 'github.com/xgadget-lab/nexttrace/printer.buildDate=${BUILD_DATE}' \
-X 'github.com/xgadget-lab/nexttrace/printer.commitID=${COMMIT_SHA1}'\
-w -s"
else
go build -trimpath -o ${TARGET} \
-ldflags "-X 'main.version=${BUILD_VERSION}' \
-X 'main.buildDate=${BUILD_DATE}' \
-X 'main.commitID=${COMMIT_SHA1}'\
-ldflags "-X 'github.com/xgadget-lab/nexttrace/printer.version=${BUILD_VERSION}' \
-X 'github.com/xgadget-lab/nexttrace/printer.buildDate=${BUILD_DATE}' \
-X 'github.com/xgadget-lab/nexttrace/printer.commitID=${COMMIT_SHA1}'\
-w -s"
fi
done

View File

@@ -1,16 +1,31 @@
on:
push: # 每次 push 的时候触发
push: # 每次带有 tag 的 push 候触发
tags:
- 'v*'
name: Build Release
jobs:
release:
if: startsWith(github.ref, 'refs/tags/') # 只有这次 Commit 是 创建 Tag 时,才进行后续发布操作
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master # checkout 代码
- uses: actions/setup-go@v2 # 配置 Go 环境
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: "1.18" # 改成自己的版本
go-version: "1.18"
- name: Test
run: go test -v -coverprofile='coverage.out' -covermode=count ./...
release:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: "1.18"
- run: bash .cross_compile.sh
@@ -23,5 +38,6 @@ jobs:
dist/nexttrace_darwin_arm64
dist/nexttrace_linux_amd64
dist/nexttrace_linux_arm64
dist/nexttrace_linux_mips
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GT_Token }}

17
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
on:
push:
pull_request:
name: Test
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: "1.18"
- name: Test
run: go test -v -coverprofile='coverage.out' -covermode=count ./...

148
README.md
View File

@@ -1,2 +1,146 @@
# traceroute
可视化路由跟踪工具
<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
nexttrace 1.0.0.1
# 表格打印一次性输出全部跳数需等待20-40秒
nexttrace -table 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
```
`NextTrace`也同样支持一些进阶功能如IP反向解析、并发数控制、模式切换等
```bash
# 无并发,每次只发送一个探测包
nexttrace -r 1 www.hkix.net
# 打开IP反向解析功能在IPv6的骨干网定位辅助有较大帮助
nexttrace -rdns www.bbix.net
# 联合使用
nexttrace -r 1 -q 1 -report www.time.com.my
```
### IP数据库
目前使用的IP数据库默认为我们自己搭建的API服务如果后期遇到滥用我们可能会选择关闭。
我们也会在后期开放服务端源代码您也可以根据该项目的源码自行搭建属于您的API服务器。
NextTrace所有的的IP地理位置`API DEMO`可以参考[这里](https://github.com/xgadget-lab/nexttrace/blob/main/ipgeo/)
### 全部用法详见Usage菜单
```shell
Usage of nexttrace:
-T Use TCP SYN for tracerouting (default port is 80)
-U Use UDP Package for tracerouting (default port is 53 in UDP)
-V Check Version
-d string
Choose IP Geograph Data Provider [LeoMoeAPI, IP.SB, IPInfo, IPInsight] (default "LeoMoeAPI")
-m int
Set the max number of hops (max TTL to be reached). (default 30)
-p int
Set SYN Traceroute Port (default 80)
-q int
Set the number of probes per each hop. (default 3)
-r int
Set ParallelRequests number. It should be 1 when there is a multi-routing. (default 18)
-rdns
Set whether rDNS will be display
-realtime
Output trace results in runtime
-report
Route Path
-table
Output trace results as table
```
## 项目截图
![](asset/screenshot_special.png)
![](asset/screenshot.png)
<!--
Leo注描述可能不合适建议再加以斟酌已经修改
## History
- v0.0.6.alpha - Now
- https://github.com/xgadget-lab/nexttrace
- 因为项目计划调整更名并转移到当前仓库。重构了部分代码提高了效率增加了ICMP(IPv4 & IPv6)支持,并规划了更多功能。
- 最初版本 - v0.0.5.alpha
- https://github.com/OwO-Network/traceroute
- 感谢 Leo (leo.moe) & Vincent (vincent.moe) 发起了这个项目,并完成了最初的工作。
-->
## Thanks
[Vincent Young](https://github.com/missuo) (i@yyt.moe)
[Sam Sam](https://github.com/samleong123) (samsam123@samsam123.name.my)
[waiting4new](https://github.com/waiting4new)、[FFEE_CO](https://github.com/fkx4-p)、[nsnnns](https://github.com/tsosunchia)
## IP Database Copyright
### IPv4 Database
#### China MainLand
* 项目组自行维护 ~ 御三家骨干网数据 ~ 5%
* 埃文科技 Paid Database ~ 95%
#### WorldWide
* 埃文科技 Paid Database ~ 15%
* IpInfo Free ~ 15%
* IPInSight Free ~ 70%
### IPv6 Database
This product includes IP2Location LITE data available from <a href="https://lite.ip2location.com">https://lite.ip2location.com</a>.
### Others
其他第三方API尽管集成在本项目内但是具体的TOS以及AUP请详见第三方API官网。如遇到IP数据错误也请直接联系他们纠错。

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

BIN
asset/screenshot_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

12
go.mod
View File

@@ -5,22 +5,24 @@ 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
)
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rodaine/table v1.0.1 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/rodaine/table v1.0.1
github.com/stretchr/testify v1.7.1
github.com/tidwall/gjson v1.14.1
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

12
go.sum
View File

@@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
@@ -9,7 +11,10 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/panjf2000/ants/v2 v2.5.0 h1:1rWGWSnxCsQBga+nQbA4/iY6VMeNoOIAM0ZWh9u3q2Q=
github.com/panjf2000/ants/v2 v2.5.0/go.mod h1:cU93usDlihJZ5CfRGNDYsiBYvoilLvBF5Qp/BT2GNRE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
@@ -33,18 +38,21 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,5 +1,7 @@
package ipgeo
import "strings"
type IPGeoData struct {
Asnumber string
Country string
@@ -11,3 +13,16 @@ type IPGeoData struct {
}
type Source = func(ip string) (*IPGeoData, error)
func GetSource(s string) Source {
switch strings.ToUpper(s) {
case "LEOMOEAPI":
return LeoIP
case "IP.SB":
return IPSB
case "IPINSIGHT":
return IPInSight
default:
return nil
}
}

View File

@@ -15,11 +15,12 @@ func TestLeoIP(t *testing.T) {
}
func TestIPSB(t *testing.T) {
res, err := IPSB("1.1.1.1")
assert.Nil(t, err)
assert.NotNil(t, res)
assert.NotEmpty(t, res.Asnumber)
assert.NotEmpty(t, res.Isp)
// Not available
//res, err := IPSB("1.1.1.1")
//assert.Nil(t, err)
//assert.NotNil(t, res)
//assert.NotEmpty(t, res.Asnumber)
//assert.NotEmpty(t, res.Isp)
}
func TestIPInfo(t *testing.T) {

View File

@@ -3,21 +3,25 @@ package ipgeo
import (
"io/ioutil"
"net/http"
"time"
"github.com/tidwall/gjson"
)
func IPSB(ip string) (*IPGeoData, error) {
resp, err := http.Get("https://api.ip.sb/geoip/" + ip)
url := "https://api.ip.sb/geoip/" + ip
client := &http.Client{
// 2秒超时
Timeout: 2 * time.Second,
}
req, _ := http.NewRequest("GET", url, nil)
// 设置 UAip.sb 默认禁止 go-client User-Agent 的 api 请求
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0")
content, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
body, _ := ioutil.ReadAll(content.Body)
res := gjson.ParseBytes(body)
return &IPGeoData{

View File

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

145
main.go
View File

@@ -3,85 +3,106 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"time"
"github.com/xgadget-lab/nexttrace/methods"
"github.com/xgadget-lab/nexttrace/methods/tcp"
"github.com/xgadget-lab/nexttrace/methods/udp"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/printer"
"github.com/xgadget-lab/nexttrace/reporter"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/xgadget-lab/nexttrace/util"
"github.com/xgadget-lab/nexttrace/util/printer"
)
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, Besttrace]")
func main() {
printer.PrintCopyRight()
domain := flagApply()
ip := util.DomainLookUp(domain)
printer.PrintTraceRouteNav(ip, domain, *dataOrigin)
if *tcpSYNFlag {
tcpTraceroute := tcp.New(ip, methods.TracerouteConfig{
MaxHops: uint16(*maxHops),
NumMeasurements: uint16(*numMeasurements),
ParallelRequests: uint16(*parallelRequests),
Port: *port,
Timeout: time.Second / 2,
})
res, err := tcpTraceroute.Start()
if err != nil {
fmt.Println("请赋予 sudo (root) 权限运行本程序")
} else {
if *displayMode == "Besttrace" {
printer.TraceroutePrinter(ip, *res, *dataOrigin)
} else if *displayMode == "table" {
printer.TracerouteTablePrinter(ip, *res, *dataOrigin)
} else {
printer.TracerouteTablePrinter(ip, *res, *dataOrigin)
}
}
} else {
if *port == 80 {
*port = 53
}
udpTraceroute := udp.New(ip, true, methods.TracerouteConfig{
MaxHops: uint16(*maxHops),
NumMeasurements: uint16(*numMeasurements),
ParallelRequests: uint16(*parallelRequests),
Port: *port,
Timeout: 2 * time.Second,
})
res, err := udpTraceroute.Start()
if err != nil {
fmt.Println("请赋予 sudo (root) 权限运行本程序")
} else {
if *displayMode == "Besttrace" {
printer.TraceroutePrinter(ip, *res, *dataOrigin)
} else if *displayMode == "table" {
printer.TracerouteTablePrinter(ip, *res, *dataOrigin)
} else {
printer.TracerouteTablePrinter(ip, *res, *dataOrigin)
}
}
}
}
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")
var ver = flag.Bool("V", false, "Check Version")
func flagApply() string {
flag.Parse()
printer.Version()
if *ver {
os.Exit(0)
}
ipArg := flag.Args()
if flag.NArg() != 1 {
fmt.Println("Args Error\nUsage : ./bettertrace [-T] [-d <dataOrigin> ] [ -m <hops> ] [ -p <port> ] [ -q <probes> ] [ -r <parallelrequests> ] <hostname>")
fmt.Println("Args Error\nUsage : ./nexttrace [-T] [-rdns] [-displayMode <displayMode>] [-d <dataOrigin> ] [ -m <hops> ] [ -p <port> ] [ -q <probes> ] [ -r <parallelrequests> ] <hostname>")
os.Exit(2)
}
return ipArg[0]
}
func main() {
domain := flagApply()
if os.Getuid() != 0 {
log.Fatalln("Traceroute requires root/sudo privileges.")
}
ip := util.DomainLookUp(domain)
printer.PrintTraceRouteNav(ip, domain, *dataOrigin)
var m trace.Method = ""
switch {
case *tcpSYNFlag:
m = trace.TCPTrace
case *udpPackageFlag:
m = trace.UDPTrace
default:
m = trace.ICMPTrace
}
if !*tcpSYNFlag && *port == 80 {
*port = 53
}
var conf = trace.Config{
DestIP: ip,
DestPort: *port,
MaxHops: *maxHops,
NumMeasurements: *numMeasurements,
ParallelRequests: *parallelRequests,
RDns: *rdnsenable,
IPGeoSource: ipgeo.GetSource(*dataOrigin),
Timeout: 2 * time.Second,
}
if m == trace.ICMPTrace && !*tablePrint {
conf.RealtimePrinter = printer.RealtimePrinter
}
res, err := trace.Traceroute(m, conf)
if err != nil {
log.Fatalln(err)
}
if *routePath {
r := reporter.New(res, ip.String())
r.Print()
return
}
if m == trace.ICMPTrace && *tablePrint {
printer.TracerouteTablePrinter(res)
}
if m == trace.TCPTrace || m == trace.UDPTrace {
if *realtimePrint {
printer.TraceroutePrinter(res)
} else {
printer.TracerouteTablePrinter(res)
}
}
}

View File

@@ -1,79 +0,0 @@
package methods
import (
"encoding/binary"
"errors"
"net"
"time"
)
// TracerouteHop type
type TracerouteHop struct {
Success bool
Address net.Addr
TTL uint16
RTT *time.Duration
}
type TracerouteConfig struct {
MaxHops uint16
NumMeasurements uint16
ParallelRequests uint16
Port int
Timeout time.Duration
}
func GetIPHeaderLength(data []byte) (int, error) {
if len(data) < 1 {
return 0, errors.New("received invalid IP header")
}
return int((data[0] & 0x0F) * 4), nil
}
func GetICMPResponsePayload(data []byte) ([]byte, error) {
length, err := GetIPHeaderLength(data)
if err != nil {
return nil, err
}
if len(data) < length {
return nil, errors.New("length of packet too short")
}
return data[length:], nil
}
func GetUDPSrcPort(data []byte) uint16 {
srcPortBytes := data[:2]
srcPort := binary.BigEndian.Uint16(srcPortBytes)
return srcPort
}
func GetTCPSeq(data []byte) uint32 {
seqBytes := data[4:8]
return binary.BigEndian.Uint32(seqBytes)
}
func ReduceFinalResult(preliminary map[uint16][]TracerouteHop, maxHops uint16, destIP net.IP) map[uint16][]TracerouteHop {
// reduce the results to remove all hops after the first encounter to final destination
finalResults := map[uint16][]TracerouteHop{}
for i := uint16(1); i < maxHops; i++ {
foundFinal := false
probes := preliminary[i]
if probes == nil {
break
}
finalResults[i] = []TracerouteHop{}
for _, probe := range probes {
if probe.Success && probe.Address.String() == destIP.String() {
foundFinal = true
}
finalResults[i] = append(finalResults[i], probe)
}
if foundFinal {
break
}
}
return finalResults
}

View File

@@ -1,329 +0,0 @@
package tcp
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/methods"
"github.com/xgadget-lab/nexttrace/parallel_limiter"
"github.com/xgadget-lab/nexttrace/signal"
"github.com/xgadget-lab/nexttrace/util"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
type inflightData struct {
start time.Time
ttl uint16
}
type results struct {
inflightRequests sync.Map
results map[uint16][]methods.TracerouteHop
resultsMu sync.Mutex
err error
concurrentRequests *parallel_limiter.ParallelLimiter
reachedFinalHop *signal.Signal
}
type Traceroute struct {
opConfig opConfig
trcrtConfig methods.TracerouteConfig
results results
}
type opConfig struct {
icmpConn net.PacketConn
tcpConn net.PacketConn
tcpMu sync.Mutex
destIP net.IP
srcIP net.IP
wg *sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func New(destIP net.IP, config methods.TracerouteConfig) *Traceroute {
return &Traceroute{
opConfig: opConfig{
destIP: destIP,
},
trcrtConfig: config,
}
}
func (tr *Traceroute) Start() (*map[uint16][]methods.TracerouteHop, error) {
tr.opConfig.ctx, tr.opConfig.cancel = context.WithCancel(context.Background())
tr.opConfig.srcIP, _ = util.LocalIPPort(tr.opConfig.destIP)
var err error
tr.opConfig.tcpConn, err = net.ListenPacket("ip4:tcp", tr.opConfig.srcIP.String())
if err != nil {
return nil, err
}
tr.opConfig.icmpConn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return nil, err
}
var wg sync.WaitGroup
tr.opConfig.wg = &wg
tr.results = results{
inflightRequests: sync.Map{},
concurrentRequests: parallel_limiter.New(int(tr.trcrtConfig.ParallelRequests)),
reachedFinalHop: signal.New(),
results: map[uint16][]methods.TracerouteHop{},
}
return tr.start()
}
func (tr *Traceroute) timeoutLoop() {
ticker := time.NewTicker(tr.trcrtConfig.Timeout / 4)
go func() {
for range ticker.C {
tr.results.inflightRequests.Range(func(key, value interface{}) bool {
request := value.(inflightData)
expired := time.Since(request.start) > tr.trcrtConfig.Timeout
if !expired {
return true
}
tr.results.inflightRequests.Delete(key)
tr.addToResult(request.ttl, methods.TracerouteHop{
Success: false,
TTL: request.ttl,
})
tr.results.concurrentRequests.Finished()
tr.opConfig.wg.Done()
return true
})
}
}()
select {
case <-tr.opConfig.ctx.Done():
ticker.Stop()
}
}
func (tr *Traceroute) addToResult(ttl uint16, hop methods.TracerouteHop) {
tr.results.resultsMu.Lock()
defer tr.results.resultsMu.Unlock()
if tr.results.results[ttl] == nil {
tr.results.results[ttl] = []methods.TracerouteHop{}
}
tr.results.results[ttl] = append(tr.results.results[ttl], hop)
}
func (tr *Traceroute) handleICMPMessage(msg listener_channel.ReceivedMessage, data []byte) {
header, err := methods.GetICMPResponsePayload(data)
if err != nil {
return
}
sequenceNumber := methods.GetTCPSeq(header)
val, ok := tr.results.inflightRequests.LoadAndDelete(sequenceNumber)
if !ok {
return
}
request := val.(inflightData)
elapsed := time.Since(request.start)
if msg.Peer.String() == tr.opConfig.destIP.String() {
tr.results.reachedFinalHop.Signal()
}
tr.addToResult(request.ttl, methods.TracerouteHop{
Success: true,
Address: msg.Peer,
TTL: request.ttl,
RTT: &elapsed,
})
tr.results.concurrentRequests.Finished()
tr.opConfig.wg.Done()
}
func (tr *Traceroute) icmpListener() {
lc := listener_channel.New(tr.opConfig.icmpConn)
defer lc.Stop()
go lc.Start()
for {
select {
case <-tr.opConfig.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:
body := rm.Body.(*icmp.TimeExceeded).Data
tr.handleICMPMessage(msg, body)
case ipv4.ICMPTypeDestinationUnreachable:
body := rm.Body.(*icmp.DstUnreach).Data
tr.handleICMPMessage(msg, body)
default:
log.Println("received icmp message of unknown type")
}
}
}
}
func (tr *Traceroute) tcpListener() {
lc := listener_channel.New(tr.opConfig.tcpConn)
defer lc.Stop()
go lc.Start()
for {
select {
case <-tr.opConfig.ctx.Done():
return
case msg := <-lc.Messages:
if msg.N == nil {
continue
}
if msg.Peer.String() != tr.opConfig.destIP.String() {
continue
}
// Decode a packet
packet := gopacket.NewPacket(msg.Msg[:*msg.N], layers.LayerTypeTCP, gopacket.Default)
// Get the TCP layer from this packet
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
tcp, _ := tcpLayer.(*layers.TCP)
val, ok := tr.results.inflightRequests.LoadAndDelete(tcp.Ack - 1)
if !ok {
continue
}
request := val.(inflightData)
tr.results.concurrentRequests.Finished()
elapsed := time.Since(request.start)
if msg.Peer.String() == tr.opConfig.destIP.String() {
tr.results.reachedFinalHop.Signal()
}
tr.addToResult(request.ttl, methods.TracerouteHop{
Success: true,
Address: msg.Peer,
TTL: request.ttl,
RTT: &elapsed,
})
tr.opConfig.wg.Done()
}
}
}
}
func (tr *Traceroute) sendMessage(ttl uint16) {
_, srcPort := util.LocalIPPort(tr.opConfig.destIP)
ipHeader := &layers.IPv4{
SrcIP: tr.opConfig.srcIP,
DstIP: tr.opConfig.destIP,
Protocol: layers.IPProtocolTCP,
TTL: uint8(ttl),
}
sequenceNumber := uint32(rand.Intn(math.MaxUint32))
tcpHeader := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(tr.trcrtConfig.Port),
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 {
tr.results.err = err
tr.opConfig.cancel()
return
}
tr.opConfig.tcpMu.Lock()
defer tr.opConfig.tcpMu.Unlock()
err := ipv4.NewPacketConn(tr.opConfig.tcpConn).SetTTL(int(ttl))
if err != nil {
tr.results.err = err
tr.opConfig.cancel()
return
}
start := time.Now()
if _, err := tr.opConfig.tcpConn.WriteTo(buf.Bytes(), &net.IPAddr{IP: tr.opConfig.destIP}); err != nil {
tr.results.err = err
tr.opConfig.cancel()
return
}
tr.results.inflightRequests.Store(sequenceNumber, inflightData{start: start, ttl: ttl})
}
func (tr *Traceroute) sendLoop() {
rand.Seed(time.Now().UTC().UnixNano())
defer tr.opConfig.wg.Done()
for ttl := uint16(1); ttl <= tr.trcrtConfig.MaxHops; ttl++ {
select {
case <-tr.results.reachedFinalHop.Chan():
return
default:
}
for i := 0; i < int(tr.trcrtConfig.NumMeasurements); i++ {
select {
case <-tr.opConfig.ctx.Done():
return
case <-tr.results.concurrentRequests.Start():
tr.opConfig.wg.Add(1)
go tr.sendMessage(ttl)
}
}
}
}
func (tr *Traceroute) start() (*map[uint16][]methods.TracerouteHop, error) {
go tr.timeoutLoop()
go tr.icmpListener()
go tr.tcpListener()
tr.opConfig.wg.Add(1)
go tr.sendLoop()
tr.opConfig.wg.Wait()
tr.opConfig.cancel()
if tr.results.err != nil {
return nil, tr.results.err
}
result := methods.ReduceFinalResult(tr.results.results, tr.trcrtConfig.MaxHops, tr.opConfig.destIP)
return &result, tr.results.err
}

View File

@@ -1,311 +0,0 @@
package udp
import (
"log"
"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/methods"
"github.com/xgadget-lab/nexttrace/methods/quic"
"github.com/xgadget-lab/nexttrace/parallel_limiter"
"github.com/xgadget-lab/nexttrace/signal"
"github.com/xgadget-lab/nexttrace/taskgroup"
"github.com/xgadget-lab/nexttrace/util"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
type inflightData struct {
icmpMsg chan<- net.Addr
}
type opConfig struct {
quic bool
destIP net.IP
wg *taskgroup.TaskGroup
icmpConn net.PacketConn
ctx context.Context
cancel context.CancelFunc
}
type results struct {
inflightRequests sync.Map
results map[uint16][]methods.TracerouteHop
resultsMu sync.Mutex
err error
concurrentRequests *parallel_limiter.ParallelLimiter
reachedFinalHop *signal.Signal
}
type Traceroute struct {
trcrtConfig methods.TracerouteConfig
opConfig opConfig
results results
}
func New(destIP net.IP, quic bool, config methods.TracerouteConfig) *Traceroute {
return &Traceroute{
opConfig: opConfig{
quic: quic,
destIP: destIP,
},
trcrtConfig: config,
}
}
func (tr *Traceroute) Start() (*map[uint16][]methods.TracerouteHop, error) {
tr.opConfig.ctx, tr.opConfig.cancel = context.WithCancel(context.Background())
tr.results = results{
inflightRequests: sync.Map{},
concurrentRequests: parallel_limiter.New(int(tr.trcrtConfig.ParallelRequests)),
results: map[uint16][]methods.TracerouteHop{},
reachedFinalHop: signal.New(),
}
var err error
tr.opConfig.icmpConn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return nil, err
}
return tr.start()
}
func (tr *Traceroute) addToResult(ttl uint16, hop methods.TracerouteHop) {
tr.results.resultsMu.Lock()
defer tr.results.resultsMu.Unlock()
if tr.results.results[ttl] == nil {
tr.results.results[ttl] = []methods.TracerouteHop{}
}
tr.results.results[ttl] = append(tr.results.results[ttl], hop)
}
func (tr *Traceroute) getUDPConn(try int) (net.IP, int, net.PacketConn) {
srcIP, _ := util.LocalIPPort(tr.opConfig.destIP)
var ipString string
if srcIP == nil {
ipString = ""
} else {
ipString = srcIP.String()
}
udpConn, err := net.ListenPacket("udp", ipString+":0")
if err != nil {
if try > 3 {
log.Fatal(err)
}
return tr.getUDPConn(try + 1)
}
return srcIP, udpConn.LocalAddr().(*net.UDPAddr).Port, udpConn
}
func (tr *Traceroute) sendMessage(ttl uint16) {
srcIP, srcPort, udpConn := tr.getUDPConn(0)
var payload []byte
if tr.opConfig.quic {
payload = quic.GenerateWithRandomIds()
} else {
ipHeader := &layers.IPv4{
SrcIP: srcIP,
DstIP: tr.opConfig.destIP,
Protocol: layers.IPProtocolTCP,
TTL: uint8(ttl),
}
udpHeader := &layers.UDP{
SrcPort: layers.UDPPort(srcPort),
DstPort: layers.UDPPort(tr.trcrtConfig.Port),
}
_ = udpHeader.SetNetworkLayerForChecksum(ipHeader)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err := gopacket.SerializeLayers(buf, opts, udpHeader, gopacket.Payload("HAJSFJHKAJSHFKJHAJKFHKASHKFHHKAFKHFAHSJK")); err != nil {
tr.results.err = err
tr.opConfig.cancel()
return
}
payload = buf.Bytes()
}
err := ipv4.NewPacketConn(udpConn).SetTTL(int(ttl))
if err != nil {
tr.results.err = err
tr.opConfig.cancel()
return
}
icmpMsg := make(chan net.Addr, 1)
udpMsg := make(chan net.Addr, 1)
start := time.Now()
if _, err := udpConn.WriteTo(payload, &net.UDPAddr{IP: tr.opConfig.destIP, Port: tr.trcrtConfig.Port}); err != nil {
tr.results.err = err
tr.opConfig.cancel()
return
}
inflight := inflightData{
icmpMsg: icmpMsg,
}
tr.results.inflightRequests.Store(uint16(srcPort), inflight)
go func() {
reply := make([]byte, 1500)
_, peer, err := udpConn.ReadFrom(reply)
if err != nil {
// probably because we closed the connection
return
}
udpMsg <- peer
}()
select {
case peer := <-icmpMsg:
rtt := time.Since(start)
if peer.(*net.IPAddr).IP.Equal(tr.opConfig.destIP) {
tr.results.reachedFinalHop.Signal()
}
tr.addToResult(ttl, methods.TracerouteHop{
Success: true,
Address: peer,
TTL: ttl,
RTT: &rtt,
})
case peer := <-udpMsg:
rtt := time.Since(start)
ip := peer.(*net.UDPAddr).IP
if ip.Equal(tr.opConfig.destIP) {
tr.results.reachedFinalHop.Signal()
}
tr.addToResult(ttl, methods.TracerouteHop{
Success: true,
Address: &net.IPAddr{IP: ip},
TTL: ttl,
RTT: &rtt,
})
case <-time.After(tr.trcrtConfig.Timeout):
tr.addToResult(ttl, methods.TracerouteHop{
Success: false,
Address: nil,
TTL: ttl,
RTT: nil,
})
}
tr.results.inflightRequests.Delete(uint16(srcPort))
udpConn.Close()
tr.results.concurrentRequests.Finished()
tr.opConfig.wg.Done()
}
func (tr *Traceroute) handleICMPMessage(msg listener_channel.ReceivedMessage, data []byte) {
header, err := methods.GetICMPResponsePayload(data)
if err != nil {
return
}
srcPort := methods.GetUDPSrcPort(header)
val, ok := tr.results.inflightRequests.LoadAndDelete(srcPort)
if !ok {
return
}
request := val.(inflightData)
request.icmpMsg <- msg.Peer
}
func (tr *Traceroute) icmpListener() {
lc := listener_channel.New(tr.opConfig.icmpConn)
defer lc.Stop()
go lc.Start()
for {
select {
case <-tr.opConfig.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:
body := rm.Body.(*icmp.TimeExceeded).Data
tr.handleICMPMessage(msg, body)
case ipv4.ICMPTypeDestinationUnreachable:
body := rm.Body.(*icmp.DstUnreach).Data
tr.handleICMPMessage(msg, body)
default:
log.Println("received icmp message of unknown type", rm.Type)
}
}
}
}
func (tr *Traceroute) sendLoop() {
rand.Seed(time.Now().UTC().UnixNano())
for ttl := uint16(1); ttl <= tr.trcrtConfig.MaxHops; ttl++ {
select {
case <-tr.results.reachedFinalHop.Chan():
return
default:
}
for i := 0; i < int(tr.trcrtConfig.NumMeasurements); i++ {
select {
case <-tr.opConfig.ctx.Done():
return
case <-tr.results.concurrentRequests.Start():
tr.opConfig.wg.Add()
go tr.sendMessage(ttl)
}
}
}
}
func (tr *Traceroute) start() (*map[uint16][]methods.TracerouteHop, error) {
go tr.icmpListener()
wg := taskgroup.New()
tr.opConfig.wg = wg
tr.sendLoop()
wg.Wait()
tr.opConfig.cancel()
tr.opConfig.icmpConn.Close()
if tr.results.err != nil {
return nil, tr.results.err
}
result := methods.ReduceFinalResult(tr.results.results, tr.trcrtConfig.MaxHops, tr.opConfig.destIP)
return &result, tr.results.err
}

268
nt_install.sh Normal file
View File

@@ -0,0 +1,268 @@
#!/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
if [[ $arch == "arm64" ]]; then
archParam="arm64"
fi
if [[ $archParam == "" ]]; then
echo "未知的系统架构,请联系作者"
exit 1
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 来管理包了
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 -y update &>/dev/null
yum install wget -y &>/dev/null
fi
# try dnf
dnf -h &>/dev/null
if [ $? -eq 0 ]; then
dnf check-update &>/dev/null
dnf install wget -y &>/dev/null
fi
# try pacman
pacman -h &>/dev/null
if [ $? -eq 0 ]; then
pacman -Sy &>/dev/null
pacman -S wget &>/dev/null
fi
# 有的发行版自带的wget只有 --help 参数
wget --help &>/dev/null
if [ $? -ne 0 ]; then
echo "wget 安装失败"
exit 1
fi
}
installJqPackage() {
# macOS should install wget originally. Nothing to do
echo "jq 正在安装中..."
# try apt
apt-get -h &>/dev/null
if [ $? -eq 0 ]; then
# 先更新一下数据源有些机器数据源比较老可能会404
apt-get update -y &>/dev/null
apt-get install jq -y &>/dev/null
fi
# try yum
yum -h &>/dev/null
if [ $? -eq 0 ]; then
yum -y update &>/dev/null
yum install jq -y &>/dev/null
fi
# try dnf
dnf -h &>/dev/null
if [ $? -eq 0 ]; then
dnf check-update &>/dev/null
dnf install jq -y &>/dev/null
fi
# try pacman
pacman -h &>/dev/null
if [ $? -eq 0 ]; then
pacman -Sy &>/dev/null
pacman -S jq &>/dev/null
fi
jq -h &>/dev/null
if [ $? -ne 0 ]; then
echo "jq 安装失败"
exit 1
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
}
checkVersion() {
echo "正在检查版本..."
version=$(curl -sL https://api.github.com/repos/xgadget-lab/nexttrace/releases/latest | jq -r '.tag_name')
if [[ $version == "" ]]; then
echo "获取版本失败,请检查网络连接"
exit 1
fi
echo 当前最新release版本${version}
read -r -p "是否安装/更新软件? (y/n)" input
case $input in
[yY][eE][sS] | [yY])
break
;;
[nN][oO] | [nN])
echo "您选择了取消安装/更新,脚本即将退出"
exit 1
;;
*)
break
;;
esac
}
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")
latestURL=${latestURL:1:-1}
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}
if [[ ${osDistribution} == "macOS" ]]; then
xattr -r -d com.apple.quarantine ${usrPath}/nexttrace
fi
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
# TODO: 检查版本并更新
#checkVersion
# Download Procedure
getLocation
downloadBinrayFile
# Run Procedure
runBinrayFileHelp

View File

@@ -1,52 +0,0 @@
package parallel_limiter
import (
"sync"
)
type ParallelLimiter struct {
maxCount int
mu sync.Mutex
currentRunning int
waiting []chan struct{}
}
func New(count int) *ParallelLimiter {
return &ParallelLimiter{
maxCount: count,
currentRunning: 0,
waiting: []chan struct{}{},
}
}
func (p *ParallelLimiter) Start() chan struct{} {
p.mu.Lock()
if p.currentRunning+1 > p.maxCount {
waitChan := make(chan struct{})
p.waiting = append(p.waiting, waitChan)
p.mu.Unlock()
return waitChan
}
p.currentRunning++
p.mu.Unlock()
instantResolveChan := make(chan struct{})
go func() {
instantResolveChan <- struct{}{}
}()
return instantResolveChan
}
func (p *ParallelLimiter) Finished() {
p.mu.Lock()
if len(p.waiting) > 0 {
first := p.waiting[0]
p.waiting = p.waiting[1:]
first <- struct{}{}
p.currentRunning++
}
p.currentRunning--
p.mu.Unlock()
}

View File

@@ -5,8 +5,13 @@ import (
"net"
)
func PrintCopyRight() {
fmt.Println("NextTrace v0.1.0 Alpha \nxgadget-lab 实验室 zhsh (xzhsh.ch) & leo (leo.moe)")
var version = "v0.0.0.alpha"
var buildDate = ""
var commitID = ""
func Version() {
fmt.Println("NextTrace ", version, buildDate, commitID)
fmt.Println("XGadget-lab Leo (leo.moe) & Vincent (vincent.moe) & zhshch (xzhsh.ch)")
}
func PrintTraceRouteNav(ip net.IP, domain string, dataOrigin string) {

89
printer/printer.go Normal file
View File

@@ -0,0 +1,89 @@
package printer
import (
"fmt"
"strings"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/xgadget-lab/nexttrace/ipgeo"
)
var dataOrigin string
func TraceroutePrinter(res *trace.Result) {
for i, hop := range res.Hops {
fmt.Print(i + 1)
for _, h := range hop {
HopPrinter(h)
}
}
}
func HopPrinter(h trace.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, ", ")
}

104
printer/printer_test.go Normal file
View File

@@ -0,0 +1,104 @@
package printer
import (
"errors"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/xgadget-lab/nexttrace/util"
"net"
"testing"
"time"
)
func TestPrintTraceRouteNav(t *testing.T) {
PrintTraceRouteNav(util.DomainLookUp("1.1.1.1"), "1.1.1.1", "dataOrigin")
}
var testGeo = &ipgeo.IPGeoData{
Asnumber: "TestAsnumber",
Country: "TestCountry",
Prov: "TestProv",
City: "TestCity",
District: "TestDistrict",
Owner: "TestOwner",
Isp: "TestIsp",
}
var testResult = &trace.Result{
Hops: [][]trace.Hop{
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: testGeo,
},
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: testGeo,
},
},
{
{
Success: false,
Address: nil,
Hostname: "",
TTL: 0,
RTT: 0,
Error: errors.New("test error"),
Geo: nil,
},
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: nil,
},
},
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "test",
TTL: 0,
RTT: 0,
Error: nil,
Geo: &ipgeo.IPGeoData{},
},
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: testGeo,
},
},
},
}
func TestTraceroutePrinter(t *testing.T) {
TraceroutePrinter(testResult)
}
func TestTracerouteTablePrinter(t *testing.T) {
TracerouteTablePrinter(testResult)
}
func TestRealtimePrinter(t *testing.T) {
RealtimePrinter(testResult, 0)
RealtimePrinter(testResult, 1)
RealtimePrinter(testResult, 2)
}

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])
}
}

124
printer/tableprinter.go Normal file
View File

@@ -0,0 +1,124 @@
package printer
import (
"fmt"
"github.com/xgadget-lab/nexttrace/ipgeo"
"strings"
"github.com/xgadget-lab/nexttrace/trace"
"github.com/fatih/color"
"github.com/rodaine/table"
)
type rowData struct {
Hop string
IP string
Latency string
Asnumber string
Country string
Prov string
City string
District string
Owner string
}
func TracerouteTablePrinter(res *trace.Result) {
// 初始化表格
tbl := New()
for _, hop := range res.Hops {
for k, h := range hop {
data := tableDataGenerator(h)
if k > 0 {
data.Hop = ""
}
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)
}
}
}
}
// 打印表格
tbl.Print()
}
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", "Location", "Owner")
tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
return tbl
}
func tableDataGenerator(h trace.Hop) *rowData {
if h.Address == nil {
return &rowData{
Hop: fmt.Sprint(h.TTL),
IP: "*",
}
} else {
lantency := fmt.Sprintf("%.2fms", h.RTT.Seconds()*1000)
IP := h.Address.String()
if strings.HasPrefix(IP, "9.") {
return &rowData{
Hop: fmt.Sprint(h.TTL),
IP: IP,
Latency: lantency,
Country: "局域网",
Owner: "腾讯云",
}
} else if strings.HasPrefix(IP, "11.") {
return &rowData{
Hop: fmt.Sprint(h.TTL),
IP: IP,
Latency: lantency,
Country: "局域网",
Owner: "阿里云",
}
}
if h.Hostname != "" {
IP = fmt.Sprint(h.Hostname, " (", IP, ") ")
}
if h.Geo == nil {
h.Geo = &ipgeo.IPGeoData{}
}
r := &rowData{
Hop: fmt.Sprint(h.TTL),
IP: IP,
Latency: lantency,
Asnumber: h.Geo.Asnumber,
Country: h.Geo.Country,
Prov: h.Geo.Prov,
City: h.Geo.City,
District: h.Geo.District,
Owner: h.Geo.Owner,
}
if h.Geo == nil {
return r
}
if h.Geo.Owner == "" {
h.Geo.Owner = h.Geo.Isp
}
r.Asnumber = h.Geo.Asnumber
r.Country = h.Geo.Country
r.Prov = h.Geo.Prov
r.City = h.Geo.City
r.District = h.Geo.District
r.Owner = h.Geo.Owner
return r
}
}

175
reporter/reporter.go Normal file
View File

@@ -0,0 +1,175 @@
package reporter
import (
"fmt"
"net"
"strings"
"sync"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/trace"
)
type Reporter interface {
Print()
}
func New(rs *trace.Result, ip string) Reporter {
experimentTag()
r := reporter{
routeResult: rs,
targetIP: ip,
}
return &r
}
type reporter struct {
targetTTL uint16
targetIP string
routeReport map[uint16][]routeReportNode
routeReportLock sync.Mutex
routeResult *trace.Result
wg sync.WaitGroup
}
type routeReportNode struct {
asn string
isp string
geo []string
ix bool
}
func experimentTag() {
fmt.Println("Route-Path 功能实验室")
}
func (r *reporter) generateRouteReportNode(ip string, ipGeoData ipgeo.IPGeoData, ttl uint16) {
var success bool = true
defer r.wg.Done()
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
}
}
// TODO: 这种写法不好,后面再重构一下
// 判断反向解析的域名中又或者是IP地理位置数据库中是否出现了 IX
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
}
// TODO: 正则判断POP并且提取带宽大小等信息
// CN2 需要特殊处理因为他们很多没有ASN
// 但是目前这种写法是不规范的属于凭空标记4809的IP
// TODO: 用更好的方式显示 CN2 骨干网的路由 Path
if strings.HasPrefix(ip, "59.43") {
rpn.asn = "4809"
} else {
rpn.asn = ipGeoData.Asnumber
}
// 无论最后一跳是否为存在地理位置信息AnyCast都应该给予显示
if (ipGeoData.Country == "" || ipGeoData.Country == "LAN Address" || ipGeoData.Country == "-") && ip != r.targetIP {
success = false
} else {
if ipGeoData.City == "" {
rpn.geo = []string{ipGeoData.Country, ipGeoData.Prov}
} else {
rpn.geo = []string{ipGeoData.Country, ipGeoData.City}
}
}
if ipGeoData.Asnumber == "" {
rpn.asn = "*"
}
if ipGeoData.Isp == "" {
rpn.isp = ipGeoData.Owner
} else {
rpn.isp = ipGeoData.Isp
}
// 有效记录
if success {
// 锁住资源防止同时写panic
r.routeReportLock.Lock()
// 添加到MAP中
r.routeReport[ttl] = append(r.routeReport[ttl], rpn)
// 写入完成,解锁释放资源给其他协程
r.routeReportLock.Unlock()
}
}
func (r *reporter) InitialBaseData() Reporter {
reportNodes := map[uint16][]routeReportNode{}
r.routeReport = reportNodes
r.targetTTL = uint16(len(r.routeResult.Hops))
for i := uint16(0); i < r.targetTTL; i++ {
traceHop := r.routeResult.Hops[i][0]
if traceHop.Success {
currentIP := traceHop.Address.String()
r.wg.Add(1)
go r.generateRouteReportNode(currentIP, *traceHop.Geo, i)
}
}
// 等待所有的子协程运行完毕
r.wg.Wait()
return r
}
func (r *reporter) Print() {
var beforeActiveTTL uint16 = 1
r.InitialBaseData()
for i := uint16(1); i < r.targetTTL; i++ {
// 计算该TTL内的数据长度如果为0则代表没有有效数据
if len(r.routeReport[i]) == 0 {
// 跳过改跃点的数据整理
continue
}
nodeReport := r.routeReport[i][0]
if i == 1 {
fmt.Printf("AS%s %s「%s『%s", nodeReport.asn, nodeReport.isp, nodeReport.geo[0], nodeReport.geo[1])
} else {
nodeReportBefore := r.routeReport[beforeActiveTTL][0]
// ASN 相同,同个 ISP 内部的数据传递
if nodeReportBefore.asn == nodeReport.asn {
// Same ASN but Coutry or City Changed
if nodeReportBefore.geo[0] != nodeReport.geo[0] {
fmt.Printf("』→ %s『%s", nodeReport.geo[0], nodeReport.geo[1])
} else {
if nodeReportBefore.geo[1] != nodeReport.geo[1] {
fmt.Printf(" → %s", nodeReport.geo[1])
}
}
} else {
// ASN 不同,跨 ISP 的数据传递,这里可能会出现 POP、IP Transit、Peer、Exchange
fmt.Printf("』」")
if int(i) != len(r.routeReport)+1 {
// 部分 Shell 客户端可能无法很好的展示这个特殊字符
// TODO: 寻找其他替代字符
fmt.Printf("\n ╭╯\n ╰")
}
if nodeReport.ix {
fmt.Printf("AS%s \033[42;37mIXP\033[0m %s「%s『%s", nodeReport.asn, nodeReport.isp, nodeReport.geo[0], nodeReport.geo[1])
} else {
fmt.Printf("AS%s %s「%s『%s", nodeReport.asn, nodeReport.isp, nodeReport.geo[0], nodeReport.geo[1])
}
}
}
// 标记为最新的一个有效跃点
beforeActiveTTL = i
}
fmt.Println("』」")
}

115
reporter/reporter_test.go Normal file
View File

@@ -0,0 +1,115 @@
package reporter
import (
"net"
"testing"
"time"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/trace"
)
var testResult = &trace.Result{
Hops: [][]trace.Hop{
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("192.168.3.1")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: &ipgeo.IPGeoData{
Asnumber: "4808",
Country: "中国",
Prov: "北京市",
City: "北京市",
District: "北京市",
Owner: "",
Isp: "中国联通",
},
},
},
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("114.249.16.1")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: &ipgeo.IPGeoData{
Asnumber: "4808",
Country: "中国",
Prov: "北京市",
City: "北京市",
District: "北京市",
Owner: "",
Isp: "中国联通",
},
},
},
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("219.158.5.150")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: &ipgeo.IPGeoData{
Asnumber: "4837",
Country: "中国",
Prov: "",
City: "",
District: "",
Owner: "",
Isp: "中国联通",
},
},
},
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("62.115.125.160")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: &ipgeo.IPGeoData{
Asnumber: "1299",
Country: "Sweden",
Prov: "Stockholm County",
City: "Stockholm",
District: "",
Owner: "",
Isp: "Telia Company AB",
},
},
},
{
{
Success: true,
Address: &net.IPAddr{IP: net.ParseIP("213.226.68.73")},
Hostname: "test",
TTL: 0,
RTT: 10 * time.Millisecond,
Error: nil,
Geo: &ipgeo.IPGeoData{
Asnumber: "56630",
Country: "Germany",
Prov: "Hesse, Frankfurt",
City: "",
District: "",
Owner: "",
Isp: "Melbikomas UAB",
},
},
},
},
}
func TestPrint(t *testing.T) {
r := New(testResult, "213.226.68.73")
r.Print()
}

View File

@@ -1,19 +0,0 @@
package signal
type Signal struct {
sigChan chan struct{}
}
func New() *Signal {
return &Signal{sigChan: make(chan struct{}, 1)}
}
func (s *Signal) Signal() {
if len(s.sigChan) == 0 {
s.sigChan <- struct{}{}
}
}
func (s *Signal) Chan() chan struct{} {
return s.sigChan
}

View File

@@ -1,45 +0,0 @@
package taskgroup
import (
"sync"
)
type TaskGroup struct {
count int
mu sync.Mutex
done []chan struct{}
}
func New() *TaskGroup {
return &TaskGroup{
count: 0,
mu: sync.Mutex{},
done: []chan struct{}{},
}
}
func (t *TaskGroup) Add() {
t.mu.Lock()
defer t.mu.Unlock()
t.count++
}
func (t *TaskGroup) Done() {
t.mu.Lock()
defer t.mu.Unlock()
if t.count-1 == 0 {
for _, doneChannel := range t.done {
doneChannel <- struct{}{}
}
t.done = []chan struct{}{}
}
t.count--
}
func (t *TaskGroup) Wait() {
doneChannel := make(chan struct{})
t.mu.Lock()
t.done = append(t.done, doneChannel)
t.mu.Unlock()
<-doneChannel
}

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

@@ -1,4 +1,4 @@
package listener_channel
package trace
import (
"golang.org/x/net/context"
@@ -13,21 +13,19 @@ type ReceivedMessage struct {
Err error
}
type ListenerChannel struct {
type PacketListener struct {
ctx context.Context
cancel context.CancelFunc
Conn net.PacketConn
Messages chan ReceivedMessage
}
func New(conn net.PacketConn) *ListenerChannel {
ctx, cancel := context.WithCancel(context.Background())
func NewPacketListener(conn net.PacketConn, ctx context.Context) *PacketListener {
results := make(chan ReceivedMessage, 50)
return &ListenerChannel{Conn: conn, ctx: ctx, cancel: cancel, Messages: results}
return &PacketListener{Conn: conn, ctx: ctx, Messages: results}
}
func (l *ListenerChannel) Start() {
func (l *PacketListener) Start() {
for {
select {
case <-l.ctx.Done():
@@ -55,7 +53,3 @@ func (l *ListenerChannel) Start() {
}
}
}
func (l *ListenerChannel) Stop() {
l.cancel()
}

File diff suppressed because one or more lines are too long

272
trace/tcp_ipv4.go Normal file
View File

@@ -0,0 +1,272 @@
package trace
import (
"log"
"math"
"math/rand"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/xgadget-lab/nexttrace/util"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/sync/semaphore"
)
type TCPTracer 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 *TCPTracer) Execute() (*Result, error) {
if len(t.res.Hops) > 0 {
return &t.res, ErrTracerouteExecuted
}
t.SrcIP, _ = util.LocalIPPort(t.DestIP)
var err error
t.tcp, err = net.ListenPacket("ip4:tcp", t.SrcIP.String())
if err != nil {
return nil, err
}
t.icmp, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
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 *TCPTracer) 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(1, msg.Msg[:*msg.N])
if err != nil {
log.Println(err)
continue
}
switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
t.handleICMPMessage(msg, rm.Body.(*icmp.TimeExceeded).Data)
case ipv4.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 *TCPTracer) listenTCP() {
lc := NewPacketListener(t.tcp, t.ctx)
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 *TCPTracer) 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 *TCPTracer) 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.IPv4{
SrcIP: t.SrcIP,
DstIP: t.DestIP,
Protocol: layers.IPProtocolTCP,
TTL: 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
}
err = ipv4.NewPacketConn(t.tcp).SetTTL(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()
/*
// 这里属于 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
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
}

265
trace/tcp_ipv6.go Normal file
View File

@@ -0,0 +1,265 @@
package trace
import (
"log"
"math"
"math/rand"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"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 := NewPacketListener(t.tcp, t.ctx)
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, ", ")
}

126
trace/trace.go Normal file
View File

@@ -0,0 +1,126 @@
package trace
import (
"errors"
"net"
"sync"
"time"
"github.com/xgadget-lab/nexttrace/ipgeo"
)
var (
ErrInvalidMethod = errors.New("invalid method")
ErrTracerouteExecuted = errors.New("traceroute already executed")
ErrHopLimitTimeout = errors.New("hop timeout")
)
type Config struct {
MaxHops int
NumMeasurements int
ParallelRequests int
Timeout time.Duration
DestIP net.IP
DestPort int
Quic bool
IPGeoSource ipgeo.Source
RDns bool
RealtimePrinter func(res *Result, ttl int)
}
type Method string
const (
ICMPTrace Method = "icmp"
UDPTrace Method = "udp"
TCPTrace Method = "tcp"
)
type Tracer interface {
Execute() (*Result, error)
}
func Traceroute(method Method, config Config) (*Result, error) {
var tracer Tracer
if config.MaxHops == 0 {
config.MaxHops = 30
}
if config.NumMeasurements == 0 {
config.NumMeasurements = 3
}
if config.ParallelRequests == 0 {
config.ParallelRequests = config.NumMeasurements * 5
}
switch method {
case ICMPTrace:
if config.DestIP.To4() != nil {
tracer = &ICMPTracer{Config: config}
} else {
tracer = &ICMPTracerv6{Config: config}
}
case UDPTrace:
if config.DestIP.To4() != nil {
tracer = &UDPTracer{Config: config}
} else {
return nil, errors.New("IPv6 UDP Traceroute is not supported")
}
case TCPTrace:
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
}
return tracer.Execute()
}
type Result struct {
Hops [][]Hop
lock sync.Mutex
}
func (s *Result) add(hop Hop) {
s.lock.Lock()
defer s.lock.Unlock()
k := hop.TTL - 1
for len(s.Hops) < hop.TTL {
s.Hops = append(s.Hops, make([]Hop, 0))
}
s.Hops[k] = append(s.Hops[k], hop)
}
func (s *Result) reduce(final int) {
if final > 0 && final < len(s.Hops) {
s.Hops = s.Hops[:final]
}
}
type Hop struct {
Success bool
Address net.Addr
Hostname string
TTL int
RTT time.Duration
Error error
Geo *ipgeo.IPGeoData
}
func (h *Hop) fetchIPData(c Config) (err error) {
if c.RDns && h.Hostname == "" {
ptr, err := net.LookupAddr(h.Address.String())
if err == nil && len(ptr) > 0 {
h.Hostname = ptr[0]
}
}
if c.IPGeoSource != nil && h.Geo == nil {
h.Geo, err = c.IPGeoSource(h.Address.String())
}
return
}

257
trace/udp.go Normal file
View File

@@ -0,0 +1,257 @@
package trace
import (
"log"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/xgadget-lab/nexttrace/util"
"golang.org/x/net/context"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/sync/semaphore"
)
type UDPTracer struct {
Config
wg sync.WaitGroup
res Result
ctx context.Context
inflightRequest map[int]chan Hop
inflightRequestLock sync.Mutex
icmp net.PacketConn
final int
finalLock sync.Mutex
sem *semaphore.Weighted
}
func (t *UDPTracer) Execute() (*Result, error) {
if len(t.res.Hops) > 0 {
return &t.res, ErrTracerouteExecuted
}
var err error
t.icmp, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
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()
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)
}
}
t.wg.Wait()
t.res.reduce(t.final)
return &t.res, nil
}
func (t *UDPTracer) 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(1, msg.Msg[:*msg.N])
if err != nil {
log.Println(err)
continue
}
switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
t.handleICMPMessage(msg, rm.Body.(*icmp.TimeExceeded).Data)
case ipv4.ICMPTypeDestinationUnreachable:
t.handleICMPMessage(msg, rm.Body.(*icmp.DstUnreach).Data)
default:
log.Println("received icmp message of unknown type", rm.Type)
}
}
}
}
func (t *UDPTracer) handleICMPMessage(msg ReceivedMessage, data []byte) {
header, err := util.GetICMPResponsePayload(data)
if err != nil {
return
}
srcPort := util.GetUDPSrcPort(header)
//t.inflightRequestLock.Lock()
//defer t.inflightRequestLock.Unlock()
ch, ok := t.inflightRequest[int(srcPort)]
if !ok {
return
}
ch <- Hop{
Success: true,
Address: msg.Peer,
}
}
func (t *UDPTracer) getUDPConn(try int) (net.IP, int, net.PacketConn) {
srcIP, _ := util.LocalIPPort(t.DestIP)
var ipString string
if srcIP == nil {
ipString = ""
} else {
ipString = srcIP.String()
}
udpConn, err := net.ListenPacket("udp", ipString+":0")
if err != nil {
if try > 3 {
log.Fatal(err)
}
return t.getUDPConn(try + 1)
}
return srcIP, udpConn.LocalAddr().(*net.UDPAddr).Port, udpConn
}
func (t *UDPTracer) 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
}
srcIP, srcPort, udpConn := t.getUDPConn(0)
var payload []byte
if t.Quic {
payload = GenerateQuicPayloadWithRandomIds()
} else {
ipHeader := &layers.IPv4{
SrcIP: srcIP,
DstIP: t.DestIP,
Protocol: layers.IPProtocolTCP,
TTL: uint8(ttl),
}
udpHeader := &layers.UDP{
SrcPort: layers.UDPPort(srcPort),
DstPort: layers.UDPPort(t.DestPort),
}
_ = udpHeader.SetNetworkLayerForChecksum(ipHeader)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err := gopacket.SerializeLayers(buf, opts, udpHeader, gopacket.Payload("HAJSFJHKAJSHFKJHAJKFHKASHKFHHKAFKHFAHSJK")); err != nil {
return err
}
payload = buf.Bytes()
}
err = ipv4.NewPacketConn(udpConn).SetTTL(ttl)
if err != nil {
return err
}
start := time.Now()
if _, err := udpConn.WriteTo(payload, &net.UDPAddr{IP: t.DestIP, Port: t.DestPort}); err != nil {
return err
}
// 在对inflightRequest进行写操作的时候应该加锁保护以免多个goroutine协程试图同时写入造成panic
t.inflightRequestLock.Lock()
hopCh := make(chan Hop)
t.inflightRequest[srcPort] = hopCh
t.inflightRequestLock.Unlock()
defer func() {
t.inflightRequestLock.Lock()
close(hopCh)
delete(t.inflightRequest, srcPort)
t.inflightRequestLock.Unlock()
}()
go func() {
reply := make([]byte, 1500)
_, peer, err := udpConn.ReadFrom(reply)
if err != nil {
// probably because we closed the connection
return
}
hopCh <- Hop{
Success: true,
Address: peer,
}
}()
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.UDPAddr); 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

@@ -1,11 +0,0 @@
package util
type IPGeoData struct {
Asnumber string `json:"asnumber"`
Country string `json:"country"`
Prov string `json:"prov"`
City string `json:"city"`
District string `json:"district"`
Owner string `json:"owner"`
Isp string `json:"isp"`
}

View File

@@ -1,103 +0,0 @@
package printer
import (
"fmt"
"net"
"strings"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/methods"
)
var dataOrigin string
func TraceroutePrinter(ip net.IP, res map[uint16][]methods.TracerouteHop, dataOrigin string) {
for hi := uint16(1); hi < 30; hi++ {
fmt.Print(hi)
for _, v := range res[hi] {
hopPrinter(v)
if v.Address != nil && ip.String() == v.Address.String() {
hi = 31
}
}
}
}
func hopPrinter(v2 methods.TracerouteHop) {
if v2.Address == nil {
fmt.Println("\t*")
} else {
var iPGeoData *ipgeo.IPGeoData
var err error
ipStr := v2.Address.String()
// TODO: 判断 err 返回并且在CLI终端提示错误
if dataOrigin == "LeoMoeAPI" {
iPGeoData, err = ipgeo.LeoIP(ipStr)
} else if dataOrigin == "IP.SB" {
iPGeoData, err = ipgeo.IPSB(ipStr)
} else if dataOrigin == "IPInfo" {
iPGeoData, err = ipgeo.IPInfo(ipStr)
} else if dataOrigin == "IPInsight" {
iPGeoData, err = ipgeo.IPInSight(ipStr)
} else {
iPGeoData, err = ipgeo.LeoIP(ipStr)
}
geo := ""
if err != nil {
geo = fmt.Sprint("Error: ", err)
} else {
geo = formatIpGeoData(ipStr, iPGeoData)
}
ptr, err := net.LookupAddr(ipStr)
txt := "\t"
if err != nil {
txt += fmt.Sprint(ipStr, " ", fmt.Sprintf("%.2f", v2.RTT.Seconds()*1000), "ms ", geo)
} else {
txt += fmt.Sprint(ptr[0], " (", ipStr, ") ", fmt.Sprintf("%.2f", v2.RTT.Seconds()*1000), "ms ", 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: 判断阿里云和腾讯云内网,数据不足,有待进一步完善
if strings.HasPrefix(ip, "9.31.") || strings.HasPrefix(ip, "11.72.") {
res = append(res, "局域网", "腾讯云")
} else if strings.HasPrefix(ip, "11.13.") {
res = append(res, "局域网", "阿里云")
} else if data.Country == "" {
res = append(res, "局域网")
} else {
if data.Owner == "" {
data.Owner = data.Isp
}
if data.District != "" {
data.City = data.City + ", " + data.District
}
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

@@ -1,114 +0,0 @@
package printer
import (
"fmt"
"net"
"github.com/fatih/color"
"github.com/rodaine/table"
"github.com/xgadget-lab/nexttrace/ipgeo"
"github.com/xgadget-lab/nexttrace/methods"
)
type rowData struct {
Hop int64
IP string
Latency string
Asnumber string
Country string
Prov string
City string
District string
Owner string
}
func TracerouteTablePrinter(ip net.IP, res map[uint16][]methods.TracerouteHop, dataOrigin string) {
// 初始化表格
tbl := New()
for hi := uint16(1); hi < 30; hi++ {
for _, v := range res[hi] {
data := tableDataGenerator(v)
tbl.AddRow(data.Hop, data.IP, data.Latency, data.Asnumber, data.Country, data.Prov, data.City, data.Owner)
if v.Address != nil && ip.String() == v.Address.String() {
hi = 31
}
}
}
// 打印表格
tbl.Print()
}
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.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
return tbl
}
func tableDataGenerator(v2 methods.TracerouteHop) *rowData {
if v2.Address == nil {
return &rowData{
Hop: int64(v2.TTL),
IP: "*",
}
} else {
// 初始化变量
var iPGeoData *ipgeo.IPGeoData
var err error
var lantency, IP string
ipStr := v2.Address.String()
// TODO: 判断 err 返回并且在CLI终端提示错误
if dataOrigin == "LeoMoeAPI" {
iPGeoData, err = ipgeo.LeoIP(ipStr)
} else if dataOrigin == "IP.SB" {
iPGeoData, err = ipgeo.IPSB(ipStr)
} else if dataOrigin == "IPInfo" {
iPGeoData, err = ipgeo.IPInfo(ipStr)
} else if dataOrigin == "IPInsight" {
iPGeoData, err = ipgeo.IPInSight(ipStr)
} else {
iPGeoData, err = ipgeo.LeoIP(ipStr)
}
ptr, err_LookupAddr := net.LookupAddr(ipStr)
lantency = fmt.Sprintf("%.2fms", v2.RTT.Seconds()*1000)
if err_LookupAddr != nil {
IP = fmt.Sprint(ipStr)
} else {
IP = fmt.Sprint(ptr[0], " (", ipStr, ") ")
}
if iPGeoData.Owner == "" {
iPGeoData.Owner = iPGeoData.Isp
}
if err != nil {
fmt.Print("Error: ", err)
return &rowData{
Hop: int64(v2.TTL),
IP: IP,
Latency: lantency,
}
} else {
return &rowData{
Hop: int64(v2.TTL),
IP: IP,
Latency: lantency,
Asnumber: iPGeoData.Asnumber,
Country: iPGeoData.Country,
Prov: iPGeoData.Prov,
City: iPGeoData.City,
District: iPGeoData.District,
Owner: iPGeoData.Owner,
}
}
}
}

37
util/trace.go Normal file
View File

@@ -0,0 +1,37 @@
package util
import (
"encoding/binary"
"errors"
)
func GetIPHeaderLength(data []byte) (int, error) {
if len(data) < 1 {
return 0, errors.New("received invalid IP header")
}
return int((data[0] & 0x0F) * 4), nil
}
func GetICMPResponsePayload(data []byte) ([]byte, error) {
length, err := GetIPHeaderLength(data)
if err != nil {
return nil, err
}
if len(data) < length {
return nil, errors.New("length of packet too short")
}
return data[length:], nil
}
func GetUDPSrcPort(data []byte) uint16 {
srcPortBytes := data[:2]
srcPort := binary.BigEndian.Uint16(srcPortBytes)
return srcPort
}
func GetTCPSeq(data []byte) uint32 {
seqBytes := data[4:8]
return binary.BigEndian.Uint32(seqBytes)
}

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 {