Skip to content

Commit a387254

Browse files
authored
Merge pull request #371 from nxtrace/main
Enhance NextTrace API v4 GeoIP support and improve MTR handling
2 parents a0a8fa9 + efc9464 commit a387254

28 files changed

Lines changed: 4372 additions & 179 deletions

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323
接口),并在 `ReadFrom` 中调用 `stripIPv4Header` 剥离 macOS DGRAM ICMP socket
2424
返回的外层 IP 头。
2525

26+
## Git 与提交信息规范(必须遵守)
27+
28+
- Commit message 必须使用 Conventional Commits 风格:`type(scope): 具体行为`
29+
- subject 必须描述该提交实际改变的行为或约束,不能只写“处理 review 问题”“修复剩余问题”“调整代码”等泛化内容。
30+
- review finding 修复提交也必须写明具体修复点,例如 timeout、fallback、权限校验、stdout/stderr 分离;若一个提交覆盖多个 finding,用 body bullet 简短列出。
31+
- 一个提交包含多个互不相关主题时,优先拆分提交;确实需要放在一起时,subject 写主影响,body 写清每个具体改动。
32+
- 提交或 push PR 前必须检查 `git log --oneline main..HEAD`,发现泛化、误导或与 diff 不匹配的 commit message,应先 reword/amend。
33+
- 改写已推送 PR 分支历史后,只使用 `git push --force-with-lease`,不要用无保护的强推覆盖远端新提交。
34+
2635
## 当前 CLI 语义(重点)
2736

2837
### 常规 traceroute 路径

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,19 @@ export NEXTTRACE_CHUNZHENURL=http://127.0.0.1:2060
639639
export NEXTTRACE_DATAPROVIDER=ipinfo
640640
```
641641

642+
LeoMoeAPI keeps the old v3 WebSocket API as the default when no NextTrace API v4 token is available. To use the NextTrace API v4 HTTP GeoIP endpoint for the current shell session, run the setup command and paste your token:
643+
644+
```bash
645+
# Token page:
646+
# GET https://api.nxtrace.org/v4/api-tokens
647+
648+
nexttrace -x
649+
```
650+
651+
`nexttrace -x` stores the token in temporary files: one scoped to the parent process ID, which is normally your current shell, and one same-user fallback file for wrapper commands such as `go run`. Later `nexttrace` commands first read the real `NEXTTRACE_API_V4_TOKEN`, then the parent-PID file, then the fallback file, and load the value into the process-local environment. The command does not write shell profiles, permanent environment variables, or `nt_config.yaml`.
652+
653+
With `NEXTTRACE_API_V4_TOKEN` set and the active provider still `LeoMoeAPI`, NextTrace queries `GET https://api.nxtrace.org/v4/ipGeo?ip=<ip>` with `X-NextTrace-Token: <token>`. The request has no JSON body. Successful responses are direct GeoIP JSON mapped to the normal output fields; quota metadata is exposed only in headers (`X-NextTrace-Quota-Remaining`, `X-NextTrace-Quota-Expires-At`, `X-NextTrace-Quota-Cost`, `X-NextTrace-Quota-Source`) and does not change the default output format. Error responses prefer `{"error":{"message":"..."}}`; known statuses include `400` for empty/illegal IP, `401` unauthorized, `429` quota exhausted, and `500` internal server error. NextTrace API v4 token failures do not fall back to the old v3 WebSocket API.
654+
642655
#### `NextTrace` supports mixed parameters and shortened parameters
643656

644657
```bash
@@ -708,6 +721,7 @@ NextTrace currently reads the following environment variables. For boolean switc
708721
| --- | --- | --- |
709722
| `NEXTTRACE_HOSTPORT` | `api.nxtrace.org` | Override the backend host or `host:port` used by LeoMoeAPI, tracemap, and FastIP flows. |
710723
| `NEXTTRACE_TOKEN` | unset | Pre-supplied LeoMoeAPI bearer token; when present, token fetching via PoW is skipped. |
724+
| `NEXTTRACE_API_V4_TOKEN` | unset | LeoMoeAPI NextTrace API v4 HTTP GeoIP token. When unset, NextTrace also checks the temporary token files written by `nexttrace -x`; if neither exists, LeoMoeAPI keeps using the old v3 WebSocket / PoW flow. |
711725
| `NEXTTRACE_POWPROVIDER` | `api.nxtrace.org` | Select the PoW provider. The built-in non-default alias is `sakura`. |
712726
| `NEXTTRACE_DEPLOY_ADDR` | unset | Default listen address for `--deploy` when `--listen` is not provided. |
713727
| `NEXTTRACE_DEPLOY_TOKEN` | unset | Token for `--deploy` WebUI/API/WebSocket/MCP access. CLI `--deploy-token` takes precedence. |
@@ -746,7 +760,7 @@ Usage: nexttrace [-h|--help] [--init] [-4|--ipv4] [-6|--ipv6] [-T|--tcp]
746760
[-a|--always-rdns] [-P|--route-path] [--dn42] [-o|--output
747761
"<value>"] [-O|--output-default] [--table] [--raw]
748762
[-j|--json] [-c|--classic] [-f|--first <integer>] [-M|--map]
749-
[-e|--disable-mpls] [-V|--version]
763+
[-e|--disable-mpls] [-V|--version] [-x|--setup-api-v4-token]
750764
[-s|--source "<value>"] [--source-port <integer>] [-D|--dev
751765
"<value>"] [--listen "<value>"] [--deploy-token "<value>"]
752766
[--mcp] [--deploy] [-z|--send-time <integer>]
@@ -822,6 +836,8 @@ Arguments:
822836
-M --map Disable Print Trace Map
823837
-e --disable-mpls Disable MPLS
824838
-V --version Print version info and exit
839+
-x --setup-api-v4-token Store a session-only NextTrace API v4
840+
token in a temporary file and exit
825841
-s --source Use source address src_addr for outgoing
826842
packets
827843
--source-port Use source port src_port for outgoing

README_zh_CN.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,19 @@ export NEXTTRACE_CHUNZHENURL=http://127.0.0.1:2060
630630
export NEXTTRACE_DATAPROVIDER=ipinfo
631631
```
632632

633+
没有可用的 NextTrace API v4 token 时,LeoMoeAPI 默认仍使用旧 v3 WebSocket API。只想在当前 shell 会话启用 NextTrace API v4 HTTP GeoIP 接口时,运行设置命令并粘贴 token:
634+
635+
```bash
636+
# Token 页面:
637+
# GET https://api.nxtrace.org/v4/api-tokens
638+
639+
nexttrace -x
640+
```
641+
642+
`nexttrace -x` 会把 token 写入临时文件:一个按父进程 PID 区分(父进程通常就是当前 shell),另一个是同用户 fallback 文件,用于 `go run` 这类 wrapper 命令。之后启动的 `nexttrace` 会按真实 `NEXTTRACE_API_V4_TOKEN`、父 PID 文件、fallback 文件的顺序读取,并把值加载到当前进程环境。该命令不会写 shell profile、永久环境变量或 `nt_config.yaml`
643+
644+
设置 `NEXTTRACE_API_V4_TOKEN` 且当前数据源仍为 `LeoMoeAPI` 时,NextTrace 会请求 `GET https://api.nxtrace.org/v4/ipGeo?ip=<ip>`,并只通过 `X-NextTrace-Token: <token>` 请求头传 token;请求没有 JSON body。成功响应是直接映射到现有输出字段的 GeoIP JSON;配额信息只解析响应头(`X-NextTrace-Quota-Remaining``X-NextTrace-Quota-Expires-At``X-NextTrace-Quota-Cost``X-NextTrace-Quota-Source`),不改变默认输出格式。错误响应优先解析 `{"error":{"message":"..."}}`;已知状态包括 `400` 空/非法 IP、`401` unauthorized、`429` quota exhausted、`500` internal server error。NextTrace API v4 token 模式下的错误不会 fallback 到旧 v3 WebSocket API。
645+
633646
#### `NextTrace`支持使用混合参数和简略参数
634647

635648
```bash
@@ -684,6 +697,7 @@ NextTrace 当前会读取下列环境变量。对于布尔开关,只识别 `1`
684697
| --- | --- | --- |
685698
| `NEXTTRACE_HOSTPORT` | `api.nxtrace.org` | 覆盖 LeoMoeAPI、tracemap、FastIP 等使用的后端地址,支持 `host``host:port`|
686699
| `NEXTTRACE_TOKEN` | 未设置 | 预置 LeoMoeAPI Bearer Token;设置后将跳过 PoW 取 token 流程。 |
700+
| `NEXTTRACE_API_V4_TOKEN` | 未设置 | LeoMoeAPI NextTrace API v4 HTTP GeoIP token。未设置时,NextTrace 还会检查 `nexttrace -x` 写入的临时 token 文件;两者都不存在时,LeoMoeAPI 仍使用旧 v3 WebSocket / PoW 流程。 |
687701
| `NEXTTRACE_POWPROVIDER` | `api.nxtrace.org` | 指定 PoW 服务提供方;当前内置的非默认别名为 `sakura`|
688702
| `NEXTTRACE_DEPLOY_ADDR` | 未设置 | `--deploy` 模式下,当未传 `--listen` 时使用的默认监听地址。 |
689703
| `NEXTTRACE_DEPLOY_TOKEN` | 未设置 | `--deploy` WebUI/API/WebSocket/MCP 访问 token。CLI `--deploy-token` 优先级更高。 |
@@ -722,7 +736,7 @@ Usage: nexttrace [-h|--help] [--init] [-4|--ipv4] [-6|--ipv6] [-T|--tcp]
722736
[-a|--always-rdns] [-P|--route-path] [--dn42] [-o|--output
723737
"<value>"] [-O|--output-default] [--table] [--raw]
724738
[-j|--json] [-c|--classic] [-f|--first <integer>] [-M|--map]
725-
[-e|--disable-mpls] [-V|--version]
739+
[-e|--disable-mpls] [-V|--version] [-x|--setup-api-v4-token]
726740
[-s|--source "<value>"] [--source-port <integer>] [-D|--dev
727741
"<value>"] [--listen "<value>"] [--deploy-token "<value>"]
728742
[--mcp] [--deploy] [-z|--send-time <integer>]
@@ -798,6 +812,8 @@ Arguments:
798812
-M --map Disable Print Trace Map
799813
-e --disable-mpls Disable MPLS
800814
-V --version Print version info and exit
815+
-x --setup-api-v4-token Store a session-only NextTrace API v4
816+
token in a temporary file and exit
801817
-s --source Use source address src_addr for outgoing
802818
packets
803819
--source-port Use source port src_port for outgoing

cmd/cmd.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ const (
4949
)
5050

5151
var (
52-
domainLookupFn = util.DomainLookUpWithContext
52+
domainLookupFn = util.DomainLookUpWithContext
53+
prepareNextTraceAPIV4FastIPFn = ipgeo.PrepareNextTraceAPIV4FastIP
54+
newLeoWebsocketFn = wshandle.NewWithContext
55+
newLeoWebsocketAsyncFn = wshandle.NewWithContextAsync
5356
)
5457

5558
func normalizeListenAddr(addr string) string {
@@ -884,12 +887,17 @@ func initLeoWebsocket(ctx context.Context, dataOrigin, powProvider *string, asyn
884887
if !strings.EqualFold(*dataOrigin, "LEOMOEAPI") {
885888
return nil
886889
}
890+
if ipgeo.NextTraceAPIV4TokenConfigured() {
891+
if err := prepareNextTraceAPIV4FastIPFn(ctx, true); err == nil {
892+
return nil
893+
}
894+
}
887895

888896
var leoWs *wshandle.WsConn
889897
if async {
890-
leoWs = wshandle.NewWithContextAsync(ctx)
898+
leoWs = newLeoWebsocketAsyncFn(ctx)
891899
} else {
892-
leoWs = wshandle.NewWithContext(ctx)
900+
leoWs = newLeoWebsocketFn(ctx)
893901
}
894902
return leoWs
895903
}
@@ -1208,6 +1216,7 @@ func Execute() {
12081216
disableMaptrace := registerDisableMaptraceFlag(parser)
12091217
disableMPLS := parser.Flag("e", "disable-mpls", &argparse.Options{Help: "Disable MPLS"})
12101218
ver := parser.Flag("V", "version", &argparse.Options{Help: "Print version info and exit"})
1219+
setupNextTraceAPIV4Token := parser.Flag("x", "setup-api-v4-token", &argparse.Options{Help: "Store a session-only NextTrace API v4 token in a temporary file"})
12111220
speedMode := registerSpeedFlag(parser)
12121221
naliMode := registerNaliFlag(parser)
12131222
srcAddr := parser.String("s", "source", &argparse.Options{Help: "Use source address src_addr for outgoing packets"})
@@ -1255,6 +1264,16 @@ func Execute() {
12551264
fmt.Print(sanitizeUsagePositionalArgs(parser.Usage(err)))
12561265
return
12571266
}
1267+
if *setupNextTraceAPIV4Token {
1268+
if err := runNextTraceAPIV4TokenSetup(nextTraceAPIV4TokenSetupOptions{
1269+
stdin: os.Stdin,
1270+
stdout: os.Stdout,
1271+
stderr: os.Stderr,
1272+
}); err != nil {
1273+
os.Exit(handleNextTraceAPIV4TokenSetupError(os.Stderr, err))
1274+
}
1275+
return
1276+
}
12581277
rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
12591278
defer stop()
12601279
util.SrcDev = ""

cmd/cmd_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/nxtrace/NTrace-core/trace"
1313
"github.com/nxtrace/NTrace-core/tracelog"
1414
"github.com/nxtrace/NTrace-core/util"
15+
"github.com/nxtrace/NTrace-core/wshandle"
1516
)
1617

1718
func TestLookupTargetIPHonorsContextCancellation(t *testing.T) {
@@ -67,6 +68,74 @@ func TestLookupTargetIPOrExitReturnsFalseOnContextDeadline(t *testing.T) {
6768
}
6869
}
6970

71+
func TestInitLeoWebsocketSkipsV3WhenNextTraceAPIV4TokenConfigured(t *testing.T) {
72+
t.Setenv(util.EnvNextTraceAPIV4TokenKey, "v4-token")
73+
oldPrepare := prepareNextTraceAPIV4FastIPFn
74+
oldNewLeo := newLeoWebsocketFn
75+
var prepareCalls int
76+
var wsCalls int
77+
prepareNextTraceAPIV4FastIPFn = func(ctx context.Context, enableOutput bool) error {
78+
prepareCalls++
79+
if ctx == nil {
80+
t.Fatal("PrepareNextTraceAPIV4FastIP context = nil")
81+
}
82+
if !enableOutput {
83+
t.Fatal("PrepareNextTraceAPIV4FastIP enableOutput = false, want true")
84+
}
85+
return nil
86+
}
87+
newLeoWebsocketFn = func(context.Context) *wshandle.WsConn {
88+
wsCalls++
89+
return nil
90+
}
91+
t.Cleanup(func() {
92+
prepareNextTraceAPIV4FastIPFn = oldPrepare
93+
newLeoWebsocketFn = oldNewLeo
94+
})
95+
dataProvider := "LeoMoeAPI"
96+
powProvider := "api.nxtrace.org"
97+
98+
if got := initLeoWebsocket(context.Background(), &dataProvider, &powProvider, false); got != nil {
99+
t.Fatalf("initLeoWebsocket() = %+v, want nil when NextTrace API v4 token is configured", got)
100+
}
101+
if prepareCalls != 1 {
102+
t.Fatalf("PrepareNextTraceAPIV4FastIP calls = %d, want 1", prepareCalls)
103+
}
104+
if wsCalls != 0 {
105+
t.Fatalf("Leo WS calls = %d, want 0 when API v4 preheat succeeds", wsCalls)
106+
}
107+
}
108+
109+
func TestInitLeoWebsocketFallsBackToV3WhenAPIV4FastIPFails(t *testing.T) {
110+
t.Setenv(util.EnvNextTraceAPIV4TokenKey, "v4-token")
111+
oldPrepare := prepareNextTraceAPIV4FastIPFn
112+
oldNewLeo := newLeoWebsocketFn
113+
var prepareCalls int
114+
var wsCalls int
115+
prepareNextTraceAPIV4FastIPFn = func(context.Context, bool) error {
116+
prepareCalls++
117+
return errors.New("fastip unavailable")
118+
}
119+
newLeoWebsocketFn = func(context.Context) *wshandle.WsConn {
120+
wsCalls++
121+
return nil
122+
}
123+
t.Cleanup(func() {
124+
prepareNextTraceAPIV4FastIPFn = oldPrepare
125+
newLeoWebsocketFn = oldNewLeo
126+
})
127+
dataProvider := "LeoMoeAPI"
128+
powProvider := "api.nxtrace.org"
129+
130+
_ = initLeoWebsocket(context.Background(), &dataProvider, &powProvider, false)
131+
if prepareCalls != 1 {
132+
t.Fatalf("PrepareNextTraceAPIV4FastIP calls = %d, want 1", prepareCalls)
133+
}
134+
if wsCalls != 1 {
135+
t.Fatalf("Leo WS calls = %d, want 1 after API v4 preheat failure", wsCalls)
136+
}
137+
}
138+
70139
func TestMaybeRunUninterruptedRawReturnsOnCanceledContext(t *testing.T) {
71140
oldUninterrupted := util.Uninterrupted
72141
util.Uninterrupted = true

0 commit comments

Comments
 (0)