tracepath 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/docs/platforms.md +94 -0
- data/docs/releasing.md +16 -0
- data/docs/verification.md +116 -0
- data/exe/tracepath +8 -0
- data/lib/tracepath/cli/elevated.rb +27 -0
- data/lib/tracepath/cli/privilege_check.rb +65 -0
- data/lib/tracepath/cli/privileges.rb +76 -0
- data/lib/tracepath/cli/text_output.rb +122 -0
- data/lib/tracepath/cli.rb +254 -0
- data/lib/tracepath/errors.rb +33 -0
- data/lib/tracepath/models.rb +138 -0
- data/lib/tracepath/options.rb +89 -0
- data/lib/tracepath/packet.rb +433 -0
- data/lib/tracepath/pcap.rb +219 -0
- data/lib/tracepath/resolver.rb +82 -0
- data/lib/tracepath/runner.rb +248 -0
- data/lib/tracepath/transport.rb +705 -0
- data/lib/tracepath/version.rb +5 -0
- data/lib/tracepath.rb +110 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 441e986ff469245d21a8bf03f3751ccf16f1efad2905eef44eef01f9cf3383bc
|
|
4
|
+
data.tar.gz: d0b67002a846ea369c1bc6fc682bb669de31060cf4608ea35809a6d7b96f55ca
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 606f9838407712e9f3a48c105bce7719f923ab299d2a9399521235744ec70d42884c19058d09420162ca3eee004195d1b831427375dce45d52d8cb73ad21486a
|
|
7
|
+
data.tar.gz: 5a99e585b4332412d420b91e6a7117a12db9488b715faf03711e5ee61a84b8c0b21f790049048776c8ce0b42b90ca4538782e6e1e7fec95e819ff0dc56d0e8e7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- Adopt MIT licensing and tag-triggered RubyGems OIDC publishing with exact CI artifact verification.
|
|
6
|
+
|
|
7
|
+
- Preserve buffered replies across cancellation and partial send, receive, or capture failures; report subsequent errors separately and return CLI status 2 even when arrival was observed.
|
|
8
|
+
- Escape terminal controls in displayed names and diagnostics without changing raw observations.
|
|
9
|
+
- Serialize non-UTF-8 PTR octets with reversible decimal escapes so JSON and JSONL retain every result.
|
|
10
|
+
- Report cancellation and deadlines during the final reverse lookup while preserving terminal network evidence.
|
|
11
|
+
- Materialize and validate finite lazy batch inputs before starting workers.
|
|
12
|
+
- Initial IPv4/IPv6 UDP, ICMP and TCP SYN traceroute API and CLI.
|
|
13
|
+
- Immutable per-probe observations, bounded batch execution, cancellation and JSON output.
|
|
14
|
+
- Linux socket and macOS socket/libpcap transports.
|
|
15
|
+
- Restore Darwin's host-order quoted IPv4 length before validating ICMP errors; retain the original ICMP checksum and quoted fragment flags.
|
|
16
|
+
- Stream single-target text progress in probe order, group repeated responders, show the resolved target and actual packet size, and preserve partial output with one interruption summary.
|
|
17
|
+
- Add immutable caller-thread `on_event` progress callbacks without changing completed-hop callbacks or JSON result schemas.
|
|
18
|
+
- Automatically request sudo from the CLI only when socket permission checks fail, before sending probes; preserve the current Ruby, runtime dependencies and validated targets. Add `--no-sudo`; noninteractive execution uses `sudo -n`.
|
|
19
|
+
- Correctly retain literal targets following the `--` option terminator.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tracepath contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# tracepath
|
|
2
|
+
|
|
3
|
+
使用 Ruby 原生 socket 实现的 traceroute 库与命令行工具。支持 Linux、macOS,要求 Ruby 3.4 或更新版本;不调用系统 `traceroute`、`tracepath` 或 `ping` 命令。Ruby 的加载名为 `tracepath`,模块常量为 `Tracepath`。
|
|
4
|
+
|
|
5
|
+
面向日常网络运维:定位从哪一跳开始没有响应、比较 IPv4/IPv6 路径、使用 TCP SYN 检查特定端口方向,以及对数十或数百个目标执行有界批量探测。当前提供 UDP、ICMP Echo、TCP SYN 三种探测方式、结构化结果、逐跳/逐目标回调、取消、截止时间和共享批量速率限制。
|
|
6
|
+
|
|
7
|
+
这不是 Linux iputils `tracepath` 命令的 Ruby 包装。持续 MTR、Paris traceroute、主动路径 MTU 搜索、ASN/地理信息查询和 Windows 后端不在当前范围内。平台实现和实际验证要求见 [平台说明](docs/platforms.md)。
|
|
8
|
+
|
|
9
|
+
## 开始使用
|
|
10
|
+
|
|
11
|
+
在项目目录安装开发依赖,并运行本地入口:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bundle install
|
|
15
|
+
bundle exec ruby -Ilib exe/tracepath --help
|
|
16
|
+
bundle exec ruby -Ilib exe/tracepath -4 127.0.0.1
|
|
17
|
+
bundle exec ruby -Ilib exe/tracepath -T -p 443 example.com
|
|
18
|
+
bundle exec rake test
|
|
19
|
+
gem build tracepath.gemspec
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
命令行会在探测前检查 socket 权限,必要时自动调用系统 `sudo`,可直接运行 `ruby exe/tracepath qq.com`。系统需要验证身份时会提示输入密码;已有有效的 sudo 授权时直接继续。Linux UDP 使用内核错误队列,通常无需提权。权限与非交互行为见下文及平台说明;Ruby 库本身不自动提权。测试默认使用本地替身和报文样本,不要求外网或管理员权限。
|
|
23
|
+
|
|
24
|
+
## 业务模型
|
|
25
|
+
|
|
26
|
+
一次 trace 是针对**一个已解析地址**、指定协议与探测配置的有限测量。它由多个 TTL 对应的 Hop 组成;每个 Hop 保留各个 ProbeResult,不能用单一地址覆盖同一跳的多路径响应。
|
|
27
|
+
|
|
28
|
+
| 对象 | 职责 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `Request` | 目标字符串和该目标的配置覆盖,适合异构批量任务 |
|
|
31
|
+
| `Target` | 原始目标、实际选择的 IP、地址族和 IPv6 scope |
|
|
32
|
+
| `Probe` | 一次发送尝试的 TTL、序号、关联字段和时间预算 |
|
|
33
|
+
| `ProbeResult` | 回复、超时、发送失败或取消;保留响应者、RTT、ICMP/TCP 信息 |
|
|
34
|
+
| `Hop` | 同一 TTL 的探针结果及地址、RTT、响应率统计 |
|
|
35
|
+
| `Result` | 一次 trace 的配置、逐跳结果、终止原因和运行错误 |
|
|
36
|
+
| `BatchResult` | 按输入顺序保存全部结果,提供状态计数和批量耗时 |
|
|
37
|
+
|
|
38
|
+
结果及其嵌套集合不可变。`to_h` / `to_json` 供存储和系统集成使用;`started_at` 序列化为 UTC ISO 8601,`duration`、`rtt` 和 RTT 统计以秒为单位。`ProbeResult#rtt_ms` 是毫秒便捷读方法。探针内部发送/截止时刻使用单调时钟,不能当作日历时间。
|
|
39
|
+
|
|
40
|
+
`ProbeResult#hostname` 保留原始 PTR 名称字节。输出到 Hash、JSON 或文本时,名称中的非 UTF-8 字节用三位十进制 `\DDD` 表示,原始反斜杠用 `\092` 表示;有效 UTF-8 保留。因此异常 DNS 名称不会中断整批 JSON 输出,序列化名称也能按这些转义还原原始字节。JSON 字符串中的反斜杠遵循 JSON 自身的转义规则。
|
|
41
|
+
|
|
42
|
+
## Ruby 接口
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "tracepath"
|
|
46
|
+
|
|
47
|
+
result = Tracepath.trace(
|
|
48
|
+
"example.com",
|
|
49
|
+
protocol: :tcp,
|
|
50
|
+
port: 443,
|
|
51
|
+
family: :ipv4,
|
|
52
|
+
max_hops: 30,
|
|
53
|
+
timeout: 2.0,
|
|
54
|
+
max_duration: 60.0
|
|
55
|
+
) do |hop|
|
|
56
|
+
puts "#{hop.ttl}: #{hop.addresses.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts result.stop_reason
|
|
60
|
+
puts result.to_json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
单目标回调在每一跳完成时接收 `Hop`。一个 TTL 的探针全部获得回复、超时或终止后才继续下一 TTL;发现目标或终止性不可达后,不再发送下一跳。`Tracepath.trace` 也接受 `Request`,其目标配置优先于该次调用的公共配置。
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
requests = [
|
|
67
|
+
"router.example.com",
|
|
68
|
+
Tracepath::Request.new("service.example.com", protocol: :tcp, port: 443),
|
|
69
|
+
Tracepath::Request.new("service.example.com", family: :ipv6, protocol: :icmp)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
batch = Tracepath.trace_many(
|
|
73
|
+
requests,
|
|
74
|
+
concurrency: 4,
|
|
75
|
+
rate_limit: 50.0,
|
|
76
|
+
timeout: 2.0,
|
|
77
|
+
max_duration: 60.0
|
|
78
|
+
) do |result, index|
|
|
79
|
+
puts "input #{index}: #{result.target.original} #{result.stop_reason}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
p batch.counts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`Request` 的配置覆盖批量公共配置。最多同时执行 `concurrency` 个目标,`rate_limit` 限制整个批次每秒发送的探针;每个目标仍受 `max_inflight` 和 `send_interval` 约束。重复目标独立保留。回调按完成顺序在调用线程串行执行,`batch.results` 保持输入顺序,`index` 从零开始。
|
|
86
|
+
|
|
87
|
+
批量输入可使用有限的 Enumerable,包括 Lazy 枚举器;整个计划在启动工作线程前完成物化与校验。
|
|
88
|
+
|
|
89
|
+
回调应及时返回。网络工作线程独立计算超时,但库不能安全中止任意调用方代码。回调抛出异常时,库停止调度、清理工作线程与网络资源,再向调用方重抛原异常。
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
token = Tracepath::CancellationToken.new
|
|
93
|
+
worker = Thread.new do
|
|
94
|
+
Tracepath.trace_many(requests, cancellation: token)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
token.cancel # 也可由应用的停止事件调用;重复调用无副作用
|
|
98
|
+
batch = worker.value
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
取消后保留已经获得的结果;尚未启动的目标也返回 `:cancelled`,因此批量结果不会无声缺项。库本身不安装信号处理器。
|
|
102
|
+
|
|
103
|
+
### 配置
|
|
104
|
+
|
|
105
|
+
| 关键字 | 默认值 | 含义 |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `protocol` | `:udp` | `:udp`、`:icmp`、`:tcp` |
|
|
108
|
+
| `family` | `:auto` | `:auto`、`:ipv4`、`:ipv6` |
|
|
109
|
+
| `first_ttl` / `max_hops` | `1` / `30` | 首个与最后一个 TTL / IPv6 hop limit |
|
|
110
|
+
| `probes` / `max_inflight` | `3` / `3` | 每跳探针数、每目标同时等待的探针上限 |
|
|
111
|
+
| `timeout` | `3.0` | 每个探针等待回复的秒数 |
|
|
112
|
+
| `max_duration` | `120.0` | 单目标总时限,包含解析和探测 |
|
|
113
|
+
| `dns_timeout` | `3.0` | 正向解析时间预算 |
|
|
114
|
+
| `reverse_dns` / `ptr_timeout` | `false` / `1.0` | 是否查询响应者主机名、单次反向解析预算 |
|
|
115
|
+
| `send_interval` | `0.05` | 同一目标相邻探针发送的最小间隔,秒 |
|
|
116
|
+
| `port` | UDP `33434`;TCP `443` | UDP 起始端口逐探针递增;TCP 目标端口固定;ICMP 不接受该项 |
|
|
117
|
+
| `source` / `interface` | `nil` / `nil` | 本机源 IP 和出接口 |
|
|
118
|
+
| `traffic_class` | `0` | IPv4 TOS / IPv6 traffic class,`0..255` |
|
|
119
|
+
| `packet_size` | `nil` | IP 包总字节数;默认 UDP/ICMP 载荷为 32 字节,TCP 无额外载荷 |
|
|
120
|
+
| `dont_fragment` | `false` | IPv4 不分片;IPv6 不接受该项 |
|
|
121
|
+
| `cancellation` | 无 | 可选共享 `CancellationToken`,运行控制对象不写入结果配置 |
|
|
122
|
+
|
|
123
|
+
运行参数 `rate_limit` 默认 `50.0` 探针/秒;单目标和批量均可指定,批量所有目标共享同一个限制。`trace_many` 另外接受 `concurrency`,默认 `4`,范围 `1..256`。
|
|
124
|
+
|
|
125
|
+
`family: :auto` 从系统解析结果中选择一个地址,不表示同时探测双栈,也不承诺在失败后切换另一个地址。比较双栈时应创建两个分别指定地址族的请求。IPv6 link-local 地址需要 `%接口名`、`%接口索引` 或 `interface:`,例如 `fe80::1%en0`。
|
|
126
|
+
|
|
127
|
+
配置非法抛出 `ArgumentError`;批量在启动探测前校验公共配置及各请求。DNS、权限和捕获等预期运行故障保存在 `Result#error`,含 `code`、`message` 和 `details`。调用方异常及程序错误不会伪装成网络失败结果。
|
|
128
|
+
|
|
129
|
+
发送或接收批次中途失败时,已经读取且通过关联和时间校验的回复仍保留在 Hop 中,故障另外写入 `error`。若这些回复已经证明到达,`stop_reason` 可以是 `:reached`,同时带有后续故障;调用方应分别检查到达证据与 `error`。
|
|
130
|
+
|
|
131
|
+
### 结果如何解释
|
|
132
|
+
|
|
133
|
+
| `stop_reason` | 含义 |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `:reached` | 收到能关联到本次探针的目标响应 |
|
|
136
|
+
| `:unreachable` | 收到终止性不可达响应;具体原因保留在探针中 |
|
|
137
|
+
| `:max_hops` | 已用完配置的 TTL 范围,尚未证明到达 |
|
|
138
|
+
| `:deadline_exceeded` | 单目标整体时间预算耗尽 |
|
|
139
|
+
| `:cancelled` | 调用方或 CLI 中断了测量 |
|
|
140
|
+
| `:error` | 解析、权限、发送/捕获等运行故障,见 `error` |
|
|
141
|
+
|
|
142
|
+
`reached?` 只在 `:reached` 时为真。UDP 到达依靠目标发出的匹配端口不可达;ICMP 依靠匹配 Echo Reply;TCP 的匹配 SYN/ACK 或 RST 都能证明到达。TCP 到达不等于应用服务健康,也没有建立业务连接。
|
|
143
|
+
|
|
144
|
+
`*` 表示本探针在预算内没有得到可关联回复,不能直接证明链路丢包或目标故障。路由器可能限速、不回答 TTL 超时,或采用不同返回路径。每跳 RTT 是源端到该响应者的往返时间,不能用相邻跳 RTT 相减断言链路时延。地址重复不能单独证明路由环路。
|
|
145
|
+
|
|
146
|
+
`Hop#response_rate` 只在回复和真实超时之间计算比例;取消和发送失败不计为丢包。没有有效样本时返回 `nil`。ECMP 下同一跳可能出现多个地址;本项目未提供保持固定流标识的 Paris traceroute 保证。探针中的被动 MTU 信息也不等于主动完成路径 MTU 搜索。
|
|
147
|
+
|
|
148
|
+
校验和卸载可能使本机捕获的数据包尚未包含最终校验和。Linux 使用内核附带元信息处理;macOS 仅在已确认的本机 loopback TCP 捕获中使用明确的 `unverified_local_capture` 标记。具体边界见 [平台说明](docs/platforms.md),该标记不表示线上的报文已通过校验。
|
|
149
|
+
|
|
150
|
+
## 命令行
|
|
151
|
+
|
|
152
|
+
以下命令在安装本地构建的 gem 后使用 `tracepath`,在源码目录也可换成 `bundle exec ruby -Ilib exe/tracepath`。
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
tracepath -4 -U -m 20 -q 3 example.com
|
|
156
|
+
tracepath -6 -I --reverse-dns example.com
|
|
157
|
+
tracepath -T -p 443 --timeout 2 --max-duration 45 example.com
|
|
158
|
+
tracepath --concurrency 4 --rate-limit 50 --targets-file targets.txt --json
|
|
159
|
+
tracepath --jsonl router-a.example.com router-b.example.com
|
|
160
|
+
tracepath --no-sudo --json example.com
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
命令行入口默认启用自动提权,也适用于 `bundle exec` 与安装后的 gem 命令。完成参数和目标列表校验后,只打开并关闭所需 socket 来检查权限,不发送探针;只有明确权限拒绝才尝试 sudo。root、Linux UDP 以及已有所需 socket 能力的进程直接运行。帮助、版本和参数校验失败不会触发 sudo。
|
|
164
|
+
|
|
165
|
+
提权继续使用当前 Ruby 的绝对路径、当前入口及已选运行依赖,不依赖 sudo 的 `PATH`,也不使用 `sudo -E`。密码完全由 sudo 处理,tracepath 不读取、保存或转发密码。目标文件和 stdin 只由原用户读取一次,已校验目标作为独立参数传递;不会提权后重读文件,也不会在已开始探测后重放任务。
|
|
166
|
+
|
|
167
|
+
没有控制终端时使用 `sudo -n`,仅在现有授权允许时继续,不会等待密码;sudo 拒绝时保留其退出状态和 stderr,stdout 不生成探测结果。自动化调用可用 `--no-sudo` 关闭重启,直接获得库的权限错误及 JSON 结果。`Tracepath.trace`、`trace_many` 和默认的程序化 `CLI.new` 不会请求提权;只有命令行入口显式启用此策略。自动提权不会修改 sudoers、系统能力或设备权限。
|
|
168
|
+
|
|
169
|
+
目标文件每行一个目标,去除行首尾空白后忽略空行和以 `#` 开头的整行注释,不支持行内注释;`--targets-file -` 从 stdin 读取。文件和位置参数按命令行出现顺序合并,重复目标保留。文本模式单目标先显示解析后的目标地址、协议和实际 IP 包长,再按探针顺序逐项输出。等待期间先显示当前跳号;同一响应地址只打印一次,地址发生变化时另起缩进行。批量按目标完成顺序输出。
|
|
170
|
+
|
|
171
|
+
例如,同一跳三个探针均收到同一地址的响应时:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
tracepath to example.com (192.0.2.9), UDP, 30 hops max, 60 byte packets
|
|
175
|
+
1 192.0.2.1 0.282 ms 0.372 ms 0.272 ms
|
|
176
|
+
2 * * *
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
乱序回包按探针索引排列,后面的结果会等待前面的探针结束后一起显示。`--reverse-dns` 在整跳完成、有界 PTR 查询结束后显示名称,不会让 DNS 阻塞网络收发。Ctrl-C 保留已取得的 RTT,换行并显示一次 `Trace interrupted`;未完成探针不会变成星号或重复的取消列,结构化结果仍保留全部取消记录。
|
|
180
|
+
|
|
181
|
+
文本中的目标、PTR 名称和诊断信息会把终端控制字符显示为 `\xNN`,避免远端名称改变终端状态;这不修改原始观测。若尚无终止性网络证据,最后一跳 PTR 查询期间的取消或整体截止也会成为 `stop_reason`;已有终止性证据继续保留其原终止原因。
|
|
182
|
+
|
|
183
|
+
默认每跳最多并行 3 个探针、每探针超时 3 秒;macOS 系统 traceroute 默认依次探测、每探针超时 5 秒,因此两者出现后续星号的时间不同。需要依次观察每次超时时可用 `--max-inflight 1 -w 5`。静默跳不会自动终止探测,也不表示上一跳就是目标。
|
|
184
|
+
|
|
185
|
+
程序化单目标进度可通过 `Tracepath.trace(host, on_event: callback)` 获取;原有 block 仍只接收完成的 `Hop`。`callback` 在调用线程执行,事件为冻结的 Hash:
|
|
186
|
+
|
|
187
|
+
| `event[:type]` | 其余字段 | 时机 |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `:trace_started` | `target`、`options`、`packet_size` | 完成目标解析并打开 transport 后 |
|
|
190
|
+
| `:hop_started` | `ttl` | 开始探测该跳时 |
|
|
191
|
+
| `:probe_finished` | `result`(`ProbeResult`) | 单个探针收到响应、超时、发送失败或被取消时 |
|
|
192
|
+
|
|
193
|
+
探针事件按完成顺序送达,使用 `event[:result].probe.index` 恢复探针顺序。事件中的探针不包含后续 PTR 查询名称;带名称的结果由原有 Hop 回调提供。回调抛出的异常会取消并等待网络线程清理后重新抛出。
|
|
194
|
+
|
|
195
|
+
`--json` 单目标输出一个 Result,多目标输出一个 BatchResult。`--jsonl` 每完成一个目标输出一行 `{"index": 0, "result": {...}}`,没有额外汇总行;它适合长批次流式消费。机器输出使用 stdout,诊断使用 stderr。`--json` 与 `--jsonl` 不能同时指定。
|
|
196
|
+
|
|
197
|
+
文本不可达标记包括 `!N`(网络)、`!H`(主机/地址)、`!P`(协议)、`!X`(管理策略)、`!S`(源路由/范围)和 `!F`(包过大/需要分片)。完整 ICMP type/code 以结构化探针结果为准。
|
|
198
|
+
|
|
199
|
+
退出码:所有目标到达且无故障为 `0`;存在未到达结果为 `1`;配置错误或运行故障为 `2`,包括已到达但携带后续 `error` 的结果;探测期间取消/中断优先为 `130`。SIGINT 会请求取消并输出已取得的部分结果,然后恢复原信号处理器。sudo 认证阶段尚未开始探测,认证拒绝或取消保留 sudo 自身的退出码。
|
|
200
|
+
|
|
201
|
+
## 验证
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
bundle exec rake test
|
|
205
|
+
gem build tracepath.gemspec
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
CI 配置覆盖 Linux/macOS 与 Ruby 3.4/4.0 的离线测试和 gem 构建。已执行的本地矩阵、Linux 实际网络和安装结果见 [验收记录](docs/verification.md)。离线报文、时钟与传输替身测试不能证明真实内核、BPF 权限或外网路由已经验收。实际网络验证需要主动执行 [平台说明中的测试步骤](docs/platforms.md#主动执行网络验收),并保留成功、失败和未覆盖项。
|
|
209
|
+
|
|
210
|
+
## 设计参考
|
|
211
|
+
|
|
212
|
+
- [Trippy(Rust)的 Probe 模型](https://github.com/fujiapple852/trippy/blob/master/crates/trippy-core/src/probe.rs) 将发送、等待和完成状态分开,支持将单次探针作为独立证据保存。本项目据此明确区分超时、取消和发送错误。
|
|
213
|
+
- [NextTrace(Go)的 trace 模型](https://github.com/nxtrace/NTrace-core/blob/main/trace/trace.go) 包含逐跳结果与实时输出边界。本项目将完成的 Hop 和 Result 用于回调,让展示与网络执行分别承担职责。
|
|
214
|
+
- [Scapy(Python)的 TCP traceroute](https://scapy.readthedocs.io/en/stable/usage.html#tcp-traceroute-2) 展示多目标和并行探测能力。本项目针对运维批次采用有界目标并发,并逐 TTL 收敛结果,以便及时停止后续探测。
|
|
215
|
+
- [iputils(C)的 tracepath](https://github.com/iputils/iputils/blob/master/tracepath.c) 展示 Linux 错误队列的使用。本项目在 Linux UDP 后端使用相同内核接口,将平台机制限制在传输层。
|
|
216
|
+
- [Apple traceroute 的输出循环](https://github.com/apple-oss-distributions/network_cmds/blob/main/traceroute.tproj/traceroute.c) 在响应地址变化时打印地址,并逐探针刷新 RTT 或超时;文本呈现参照这一行为,同时保留本项目的并行调度与结构化证据。
|
|
217
|
+
|
|
218
|
+
## 发布与许可
|
|
219
|
+
|
|
220
|
+
项目采用 MIT 许可,见 [LICENSE](LICENSE)。标签触发 RubyGems OIDC 可信发布,
|
|
221
|
+
不保存长期 API key。完整离线矩阵通过后发布同次 CI 的已验证包,流程见 [发布说明](docs/releasing.md)。
|
data/docs/platforms.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 平台与网络验收
|
|
2
|
+
|
|
3
|
+
目标运行平台是 Linux 和 macOS,Ruby 最低版本为 3.4。协议处理、探针关联、调度和结果模型由 Ruby 实现;Linux TCP 接收与 macOS libpcap 捕获使用小型 Fiddle 边界。运行期间不执行系统 traceroute 程序。
|
|
4
|
+
|
|
5
|
+
## 后端与权限
|
|
6
|
+
|
|
7
|
+
| 平台 | UDP | ICMP Echo | TCP SYN |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| Linux | UDP socket + 内核 ICMP 错误队列 | 原始 ICMP socket | 原始 TCP 发送、AF_PACKET 接收 TCP、原始 ICMP 接收错误 |
|
|
10
|
+
| macOS | UDP socket + 原始 ICMP 接收 | 原始 ICMP socket | 原始发包 + 系统 libpcap/BPF 捕获 |
|
|
11
|
+
|
|
12
|
+
Linux UDP 通常不需要原始 socket 权限;原始 socket 需要 root 或相应网络能力。macOS 原始 socket 和 BPF 捕获需要系统允许访问,具体受运行用户、设备节点权限及系统策略影响。接口绑定还可能需要额外权限。库不提升权限、不修改设备节点、不自动降级到另一种协议。
|
|
13
|
+
|
|
14
|
+
命令行入口在发送探针前检查原始 socket 能力,遇到明确 `EPERM` / `EACCES` 时可自动通过 sudo 重新执行;使用 `--no-sudo` 关闭。检查与身份认证不发送探针、不安装永久权限。IPv4/IPv6 显式选择检查对应地址族,`:auto` 先检查 IPv4,不支持时再检查 IPv6;正式地址解析、路由、接口及 BPF 错误仍由 transport 负责。少见的“raw socket 已获授权但 BPF 仍拒绝”等情况直接报告运行错误,不在探测开始后再次提权。
|
|
15
|
+
|
|
16
|
+
无控制终端时 sudo 使用 `-n`,没有有效授权会立即退出而非读取 stdin。提权以参数数组调用,保持同一 Ruby、入口路径和已选 gem;仅传递所需 gem 路径,清除 Ruby 启动钩子和 Bundler 环境,不转发密码或全量用户环境。用户仍可显式运行 sudo,默认库 API 始终不提权。
|
|
17
|
+
|
|
18
|
+
macOS TCP 的接收路径不能仅依靠原始 TCP socket:系统 TCP 栈会消费相关流量,因此使用系统 libpcap/BPF。没有可用 libpcap、接口不存在、数据链路类型不受支持或捕获权限不足,都应作为可辨识运行错误返回,不能报告“目标未响应”。
|
|
19
|
+
|
|
20
|
+
Linux TCP 的 AF_PACKET 接收同时读取 `PACKET_AUXDATA`。只有内核明确给出校验和卸载或校验完成元信息时,才相应处理 TCP 校验和,并在响应中保留校验状态;缺少该信息时仍检查报文字节中的校验和。
|
|
21
|
+
|
|
22
|
+
此处通过 Fiddle 直接调用 libc `recvmsg(2)`,使用 `MSG_DONTWAIT` 保持非阻塞。原因是 Ruby 在 Linux 的 `recvmsg` 路径会自动附带 `MSG_CMSG_CLOEXEC`,而 AF_PACKET 拒绝该标志并返回 `EINVAL`。该边界只负责数据与辅助消息的接收,报文解码、关联、超时及资源生命周期仍由 Ruby 层控制;其他 socket 接收继续使用 Ruby socket API。
|
|
23
|
+
|
|
24
|
+
macOS 的普通 libpcap 数据包接口不提供这类校验元信息。[Apple 的 loopback 实现](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/net/if_loop.c) 可在校验和字段尚未完成时将本机报文交给 BPF。仅当捕获接口确为 `IFF_LOOPBACK`,且源和目标地址均属于本机时,直接 TCP 响应允许这种情况:不验证 TCP 校验和,并允许 IPv4 校验和字段为零;非零且错误的 IPv4 校验和仍被拒绝。响应保留 `unverified_local_capture` 标记,使用完整地址/端口、TCP flags 和序号关联探针;该标记不表示内核已经验证报文。普通接口、ICMP 及 ICMP 引用报文继续使用各自的校验规则。
|
|
25
|
+
|
|
26
|
+
访问本机网卡地址的路由可能实际走 loopback,macOS TCP 捕获会识别这种本机目标,避免误在该地址所属物理网卡上等待回复。这些平台逻辑有源码与离线测试依据,macOS 特权原生收发仍需要独立实际验收。
|
|
27
|
+
|
|
28
|
+
`source` 必须是本机数字 IP 地址;`interface` 使用当前系统接口名。IPv6 link-local 地址必须提供 zone/scope 或出接口,而且两者不能矛盾。系统无法满足明确请求的源地址、接口、流量类别或不分片能力时,应保留错误,不能静默忽略配置。
|
|
29
|
+
|
|
30
|
+
`dont_fragment` 仅适用于 IPv4。接收到的 IPv4 fragmentation-needed 或 IPv6 packet-too-big 可携带 MTU;这仅是该次报文的被动证据,不代表已经完成 PMTU 搜索。
|
|
31
|
+
|
|
32
|
+
## 自动化验证的边界
|
|
33
|
+
|
|
34
|
+
默认 `bundle exec rake test` 使用构造的报文、可控时钟和传输替身验证逻辑,不需要发送网络探针。CLI 测试覆盖参数转发、文件目标顺序、文本/JSON/JSONL 输出、退出状态、信号处理恢复和中断部分结果。
|
|
35
|
+
|
|
36
|
+
GitHub Actions 配置 Linux/macOS × Ruby 3.4/4.0 离线矩阵。该配置本身不是远程 CI 已通过的证据,离线矩阵也不等同于真实设备验收。CI 使用 [ruby/setup-ruby](https://github.com/ruby/setup-ruby) 和 [actions/checkout](https://github.com/actions/checkout)。
|
|
37
|
+
|
|
38
|
+
## 主动执行网络验收
|
|
39
|
+
|
|
40
|
+
下面命令会真实发送探针,仅在准备验证的主机上主动执行。对于需要权限的组合,应由运行者在具备权限的会话中运行;不要把整个 Ruby 解释器永久授予额外能力来迁就单次测试。
|
|
41
|
+
|
|
42
|
+
先确认 IPv4/IPv6 回环的六种组合,各次应在第一跳到达。TCP 端口关闭产生的匹配 RST 仍是正常到达证据。
|
|
43
|
+
|
|
44
|
+
项目提供显式启用的本机网络检查,覆盖双栈 UDP、ICMP 和 TCP 开放/关闭端口:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
bundle exec ruby scripts/verify_network.rb
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
也可以逐项保留 CLI JSON:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
for protocol in udp icmp tcp; do
|
|
54
|
+
bundle exec ruby -Ilib exe/tracepath --protocol "$protocol" -4 -m 2 -q 1 --timeout 1 --max-duration 5 --json 127.0.0.1
|
|
55
|
+
bundle exec ruby -Ilib exe/tracepath --protocol "$protocol" -6 -m 2 -q 1 --timeout 1 --max-duration 5 --json ::1
|
|
56
|
+
done
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 容器内受控多跳拓扑
|
|
60
|
+
|
|
61
|
+
项目另提供独立 Linux 拓扑,包含客户端、路由器和目标网络命名空间,用于检查双栈中间跳、最终响应及减小 MTU 后的错误证据:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
docker build -f test/network/Dockerfile -t tracepath-network:test .
|
|
65
|
+
docker run --rm --network none \
|
|
66
|
+
--cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_ADMIN \
|
|
67
|
+
--security-opt apparmor=unconfined \
|
|
68
|
+
-e TRACEPATH_NETWORK_TEST=1 \
|
|
69
|
+
-v "$PWD:/app:ro" -w /app \
|
|
70
|
+
tracepath-network:test bash test/network/run.sh
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
网络配置仅发生在这个一次性容器中。测试运行使用 `--network none`,不连接公网或宿主网络;项目目录只读挂载,容器退出后自动删除。`run.sh` 同时检查容器环境与显式启用变量,不应直接在宿主机运行。镜像构建需要下载系统测试依赖,和隔离的测试运行是两个步骤。
|
|
74
|
+
|
|
75
|
+
该命令是可重复执行的验收入口;具体组合是否通过,应以当次输出和退出状态为准,不能仅因测试脚本存在就记为通过。
|
|
76
|
+
|
|
77
|
+
随后在可控实验网络逐项验收:
|
|
78
|
+
|
|
79
|
+
| 场景 | 必须保留的证据 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| 至少两个路由跳的 IPv4/IPv6 路径 | 中间 TTL 超时、最终到达、TTL/地址/RTT 正确关联 |
|
|
82
|
+
| UDP 目标端口拒绝、ICMP Echo、TCP 开放和关闭端口 | 协议对应的到达证据;TCP RST 不被误报为失败 |
|
|
83
|
+
| TCP 回环地址及本机网卡地址 | 实际捕获接口正确;卸载/本机捕获校验状态可见,真实错误校验和仍按策略拒绝 |
|
|
84
|
+
| 防火墙丢弃与显式拒绝 | 分别得到超时/耗尽与带具体 ICMP code 的不可达 |
|
|
85
|
+
| 多路径或乱序响应 | 同一跳多个地址保留,晚到/重复回复不归入其他探针 |
|
|
86
|
+
| 指定源地址和接口、IPv6 link-local | 实际出接口、源地址与 scope 正确;无效配置明确失败 |
|
|
87
|
+
| 大包和 IPv4 不分片 | 捕获到的 ICMP 原因和 MTU 正确;没有证据时不声称 PMTU |
|
|
88
|
+
| 数十/数百目标批量 | 实际在途目标不超过并发上限、共享发送率受控、结果无缺项 |
|
|
89
|
+
| 探测中 SIGINT、达到整体截止时间 | 有限时间退出、部分结果可解析、socket/pcap/线程释放 |
|
|
90
|
+
| 权限不足和缺少捕获能力 | `:error` 及可辨识错误码;不得伪装成 `*` 或成功 |
|
|
91
|
+
|
|
92
|
+
受公网防火墙、ICMP 限速、NAT、ECMP、网络命名空间和系统权限影响,单个公网目标不能覆盖这些场景。Linux 可在独立网络命名空间/实验拓扑中构建多跳路由;macOS 需要真实路由环境或外部实验设备。未提供条件的场景应记录为未验证,不能以跳过替代通过。
|
|
93
|
+
|
|
94
|
+
记录验证时至少包含系统版本、Ruby 版本、协议/地址族、权限条件、完整命令、退出码与 JSON 结果;公开记录前去除不应公开的网络地址或主机名。
|
data/docs/releasing.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 可信发布
|
|
2
|
+
|
|
3
|
+
RubyGems Trusted Publisher 配置为 GitHub owner `gatework`、`tracepath`、workflow `release.yml`、environment `release`。
|
|
4
|
+
不配置长期 RubyGems API key。发布任务仅在完整 CI 通过后获得 OIDC 临时凭据。
|
|
5
|
+
|
|
6
|
+
1. 更新版本常量和 CHANGELOG;完成仓库规定的本地测试、lint 与包验证。
|
|
7
|
+
2. 提交并推送,确认同一提交的远端 CI 通过。
|
|
8
|
+
3. 推送与版本一致的 `vX.Y.Z` 标签。`release.yml` 重跑完整矩阵,并下载该次 CI 的 gem。
|
|
9
|
+
4. 发布脚本核对包身份、元数据、文件列表与源文件内容,验证该包后上传到 RubyGems。
|
|
10
|
+
5. RubyGems 下载、GitHub Release gem 和 `SHA256SUMS` 必须匹配同一产物。
|
|
11
|
+
|
|
12
|
+
恢复失败时先核实注册表版本和校验和;同版本不同包禁止覆盖。手动重跑使用对应标签 ref。
|
|
13
|
+
|
|
14
|
+
首次发布前在 RubyGems 账号下创建 pending Trusted Publisher,gem 名称为 `tracepath`;
|
|
15
|
+
仓库 `gatework/tracepath`、workflow `release.yml`、environment `release`。
|
|
16
|
+
网络与权限验收的范围见 [verification](verification.md),离线 CI 不代表生产网络验收。
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 验收记录
|
|
2
|
+
|
|
3
|
+
更新日期:2026-10-01(America/Los_Angeles)。本轮自我审查复核了四组离线测试、Linux 回环与受控多跳/MTU、普通用户权限边界及隔离 gem 安装;历史现场、抓包及自动提权记录仍单独保留在下文。这是本地执行记录,不代表远程 CI 或真实网络设备矩阵已经通过。
|
|
4
|
+
|
|
5
|
+
## 离线矩阵
|
|
6
|
+
|
|
7
|
+
四组均为 **193 tests、2236 assertions、0 failures、0 errors、0 skips**。
|
|
8
|
+
|
|
9
|
+
| 环境 | Ruby | 结果 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| macOS Darwin 25.5.0 / arm64 | 3.4.11 | 通过 |
|
|
12
|
+
| macOS Darwin 25.5.0 / arm64 | 4.0.7 | 通过 |
|
|
13
|
+
| Linux Docker / aarch64 | 3.4.11 | 通过 |
|
|
14
|
+
| Linux Docker / aarch64 | 4.0.7 | 通过 |
|
|
15
|
+
|
|
16
|
+
Ruby 4.0 的 macOS 隔离副本使用 `bundle exec rake test`;其余三组使用目标 Ruby 执行 `ruby -S rake test`,加载同一个 Rake 测试任务和 Minitest 5.x。Linux Ruby 4.0 在一次性容器中从本地 gem 缓存安装 Minitest 5.27.0,离线测试容器均使用 `--network none`。测试不依赖外部 Rails/Minitest 插件。
|
|
17
|
+
|
|
18
|
+
覆盖报文边界、校验和、首片引用、IPv6 扩展首部、迟到和重复回包、跨探针关联、预算、取消、回调异常、批量顺序、全局发送率、平台资源释放、FFI 缓冲区与 CLI 通道等行为。
|
|
19
|
+
|
|
20
|
+
## 2026-10-01 自我审查改进
|
|
21
|
+
|
|
22
|
+
从初始提交的独立 worktree 验证候选,保留原工作区的格式调整及本地 IDE 文件。新增 21 项回归;以下问题均先实际复现失败,再验证修复:
|
|
23
|
+
|
|
24
|
+
- 最后一跳 PTR 查询中取消或耗尽整体预算,原结果误写成 `max_hops`;修复后保留该跳,并复核中断原因,不覆盖已有到达、不可达或运行错误的终止证据。
|
|
25
|
+
- PTR、目标及错误信息中的终端控制字符可原样输出;修复覆盖文本、批量目标、stderr 和提权运行时错误。DNS 编解码后的 OSC52 样例现在显示为转义文本,原观测保持不变。
|
|
26
|
+
- 非 UTF-8 PTR 字节原本会中断 JSON/JSONL。现在采用可逆十进制转义,并区分字面反斜杠,验证单目标、批量 JSON 和 JSONL 全部结果完整输出。
|
|
27
|
+
- 有限 Lazy 枚举输入原本在计数前抛出 `NoMethodError`;现在先物化及校验,再创建 worker。额外离线运行 1000 个已取消的 Lazy 目标,结果完整且无存活 worker。
|
|
28
|
+
- 发送前错误队列、接收批次及 libpcap 读取中途故障原本丢弃先前观测;现在回复与故障分别传递,统一经过原有关联和时间校验。取消前已缓冲的 129 条回复全部保留,不再受单轮 64 次新 I/O 上限截断,也不额外读取尚在内核队列中的记录。已证明到达但带后续故障时,CLI 保留 `reached` 并退出 2。
|
|
29
|
+
|
|
30
|
+
本轮 Linux Ruby 3.4/4.0 双栈回环各通过 10/10 场景;Ruby 3.4 的重复目标批次为每个地址族 100 项。受控网络的双栈三协议/开放关闭端口 8/8 场景,以及两项 MTU 1280 证据检查均通过。普通用户权限检查在 Linux 和 macOS 各通过 6/6 场景;macOS 通过的是明确报告权限拒绝,没有执行特权收发。
|
|
31
|
+
|
|
32
|
+
## 实际网络
|
|
33
|
+
|
|
34
|
+
| 场景 | 已执行结果 |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Linux Ruby 3.4.11 回环 | IPv4/IPv6 的 UDP、ICMP、TCP SYN-ACK、TCP RST,全部通过 |
|
|
37
|
+
| Linux Ruby 4.0.7 回环 | 同上,另含双栈重复目标批量;10/10 场景通过 |
|
|
38
|
+
| Linux Ruby 3.4.11 批量 | IPv4、IPv6 各 100 个独立任务,每任务 2 探针;全部收到匹配回复 |
|
|
39
|
+
| Linux 受控两跳网络 | IPv4/IPv6 × UDP、ICMP、TCP 开放/关闭端口,8/8 场景通过;每跳每探针均有预期响应 |
|
|
40
|
+
| Linux 受控 MTU | 第二链路 MTU 1280、探针 1400 字节;IPv4/IPv6 均保留 `packet_too_big` 和 MTU 1280 |
|
|
41
|
+
| Linux 非特权用户、移除全部 capabilities | 双栈 UDP 到达;ICMP/TCP 明确返回 `permission_denied`,6/6 场景通过 |
|
|
42
|
+
| macOS 普通用户 | 双栈三协议均明确返回原始 socket 权限错误,6/6 场景通过;BPF activate 也已独立确认权限错误 |
|
|
43
|
+
| Linux 显式源地址/接口/流量类别/包长 | 双栈三协议 6/6;独立抓包确认 IP 总长 80、traffic class 16;TCP SYN 载荷仅 ACK SYN 时仍正确关联 |
|
|
44
|
+
| Linux 实际 CLI 中断 | 确认已发送 UDP 探针后发送 SIGINT;约 52 ms 退出,状态 130,JSON 保留取消探针,没有误计超时 |
|
|
45
|
+
| macOS 用户现场 IPv4 UDP 首跳 | 同一目标 qq.com,以系统允许的 ICMP datagram socket 接收真实回复,其余使用原有解析/关联/调度/CLI;3 次均识别 198.18.0.1,约 0.25–0.32 ms。此项不代表已执行 raw socket 模式 |
|
|
46
|
+
|
|
47
|
+
可复现入口:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
ruby scripts/verify_network.rb
|
|
51
|
+
TRACEPATH_BATCH_SIZE=100 ruby scripts/verify_network.rb
|
|
52
|
+
ruby scripts/verify_permissions.rb
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
前两项需要对应原始 socket 权限;最后一项必须在无 root、无 `CAP_NET_RAW` 的环境运行。容器多跳命令见 [平台说明](platforms.md)。这些脚本不属于默认离线测试。
|
|
56
|
+
|
|
57
|
+
多跳环境的配置有明确原因:先等待 IPv6 DAD 完成,否则内核邻居解析会让回复在探针截止后才到达;仅在隔离路由器 namespace 中关闭 IPv4 ICMP 限速,使“每个探针均有回复”的断言具有稳定前提。未放宽探针超时或删除断言,也未改变库对真实限速/迟到回复的处理。
|
|
58
|
+
|
|
59
|
+
Linux TCP 实际测试暴露并修复了虚拟接口校验和卸载问题:接收使用 AF_PACKET 的内核辅助信息。Ruby 的 recvmsg 附加标志与 AF_PACKET 不兼容,因此该接收边界用 Fiddle 调用非阻塞 libc recvmsg。实现没有根据校验和数值猜测卸载状态。
|
|
60
|
+
|
|
61
|
+
macOS 用户反馈“系统 traceroute 正常,库首跳全为星号”后,实际接收并保存了一份 ICMP TTL 超时报文。XNU 在交付 ICMP socket 前把引用的 IPv4 `ip_len` 转为主机字节序,旧实现只还原外层首部,造成完整 ICMP checksum 校验失败。还原引用长度后,ICMP 与引用 IPv4 checksum 同时归零。`test/darwin_reply_test.rb` 使用该真实样本,已实际记录修复前 2 项失败、修复后全部通过;还覆盖不可达、参数问题、DF 标志保留、无效代码和真实损坏报文。只修复 Darwin raw/datagram 的数据格式归一化,未放宽 Packet 校验或关联规则。
|
|
62
|
+
|
|
63
|
+
## 2026-10-01 文本进度复核
|
|
64
|
+
|
|
65
|
+
用户已反馈 `sudo ruby tracepath qq.com` 的 IPv4 UDP 首跳三次均得到 `198.18.0.1`。本次解决后续呈现问题:重复打印响应地址、完整 Hop 才输出、中断时重复取消列。未根据无回复推断到达,也未改变超时、并发或停止条件。
|
|
66
|
+
|
|
67
|
+
先执行系统 `/usr/sbin/traceroute -n -m 3 -q 1 -w 1 qq.com`,确认目标为 `198.19.149.41`,首跳 `198.18.0.1`,第二、三跳均为 `*`。原生实现通过 macOS 允许的 ICMP datagram 接收接口、使用相同参数时结果相同;将包长改为系统默认的 40 字节后仍相同。后续静默并非本次输出修复可消除的响应。
|
|
68
|
+
|
|
69
|
+
呈现回归 `test/cli_text_test.rb` 在独立旧源码副本上实际运行:7 项测试有 5 项失败;修复后 7 项、46 个断言通过。核心事件回归先实际记录旧实现 9 项测试出现 2 项失败、7 项错误,再扩展为 12 项测试并全部通过。覆盖逐探针刷新、乱序缓冲、地址切换、PTR、JSON、冻结事件、调用线程及回调失败清理。
|
|
70
|
+
|
|
71
|
+
新 CLI 对同一实际目标的输出(ICMP datagram 接口,仅接收 socket 类型替换,解析/关联/调度/CLI 均为项目实现):
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
tracepath to qq.com (198.19.149.41), UDP, 3 hops max, 60 byte packets
|
|
75
|
+
1 198.18.0.1 0.215 ms 0.337 ms 0.393 ms
|
|
76
|
+
2 * * *
|
|
77
|
+
3 * * *
|
|
78
|
+
max_hops (198.19.149.41); 0.987 s
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
参数为 `-n -4 -m 3 -q 3 -w 0.3`。另以 Open3 启动真实探测,读取到第二跳前缀后发送 SIGINT:首跳三次 RTT 保留,第二跳换行,只有一次 `Trace interrupted; 0.167 s`;状态 130、stderr 为空,没有取消占位列或虚假超时。读取和退出均设置 5 秒边界,子进程已退出。此项不代表本工具进程执行了 sudo/raw socket;用户现场首跳成功与本次 datagram 验证分别记录。
|
|
82
|
+
|
|
83
|
+
相邻路径检查:批量文本复用地址分组,JSON/JSONL 仍输出完整探针记录;PTR 仍在完整 Hop 后处理;所有取消、截止时间、发送/接收失败分支均发出每探针一次的事件。Linux Ruby 3.4.11 的双栈 UDP、ICMP、TCP 开放/关闭端口及重复目标批量重新执行,10/10 场景通过。
|
|
84
|
+
|
|
85
|
+
## 2026-10-01 自动提权验收
|
|
86
|
+
|
|
87
|
+
新增权限、CLI 重启计划及运行依赖恢复测试共 30 项、191 个断言。覆盖 root/已有能力跳过、socket 权限与其他错误区分、描述符关闭、参数数组、同一 Ruby、无终端 `sudo -n`、目标文件/stdin 只读取一次、`--` 后的字面目标、禁止自动提权、缺少 sudo、清理 Ruby 启动环境及实际子进程中的 gem 版本恢复。库调用默认没有提权策略。
|
|
88
|
+
|
|
89
|
+
一次性 `ruby:3.4-bookworm` 容器中安装 sudo 1.9.13p3,容器内仅为 nobody 允许免密执行测试所需 `/usr/bin/env`。仓库只读挂载,每场景设置 10 秒边界,仅探测 `127.0.0.1`:
|
|
90
|
+
|
|
91
|
+
| 场景 | 实际结果 |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| nobody ICMP 自动提权 | 完整 `sudo → env → Ruby bootstrap → CLI` 链路成功,`reached`,退出 0,仅一次提示 |
|
|
94
|
+
| stdin 两个重复目标自动提权 | 两项按原顺序保留、全部 `reached`,退出 0 |
|
|
95
|
+
| ICMP `--no-sudo` | JSON `permission_denied`、空 hops,退出 2,没有重启 |
|
|
96
|
+
| nobody UDP | `reached`,退出 0,stderr 为空,没有 sudo |
|
|
97
|
+
|
|
98
|
+
容器已删除,未修改宿主机 sudoers、capabilities 或设备权限。另已实际确认 Linux 非 root 持有 `CAP_NET_RAW` 时所需 raw/AF_PACKET socket 可打开,预检无需提权。
|
|
99
|
+
|
|
100
|
+
macOS 无控制终端且无 sudo 缓存:真实 CLI 输出非交互重启提示,sudo 立即以 1 返回 `a password is required`,不等待密码。PTY 终端真实 CLI 正常进入系统 `Password:` 提示,随后用 Ctrl-C 取消;sudo 返回 1。未输入、读取或存储密码,未完成本机认证后的 macOS root 探测;该成功链路的证据来自上面的 Linux 容器。
|
|
101
|
+
|
|
102
|
+
## 安装与交付
|
|
103
|
+
|
|
104
|
+
本轮构建 `.gem` 并核对 20 个文件的允许列表,不含测试、IDE 配置或临时文件。在独立 `GEM_HOME` / `GEM_PATH` 安装真实运行依赖,从源码目录之外验证 `require "tracepath"`、CLI `--help` / `--version`、错误退出与控制字符转义,以及非 UTF-8 PTR 的 JSON 序列化。加载路径确认来自隔离安装;依赖为 Fiddle 1.1.8、JSON 2.18.0、Resolv 0.7.2,没有依赖开发 bundle 的偶然加载。
|
|
105
|
+
|
|
106
|
+
构建仍有原有的 license 和 homepage 元数据警告;本轮未选择许可证或配置发布地址。版本保持 `0.1.0`。
|
|
107
|
+
|
|
108
|
+
没有发布 gem、创建远程仓库或运行远程 CI。
|
|
109
|
+
|
|
110
|
+
## 未验证范围
|
|
111
|
+
|
|
112
|
+
- macOS 特权环境中的完整三协议、双栈和多跳矩阵。用户已反馈 IPv4 UDP 原始 socket 首跳成功;当前工具进程无 raw socket/BPF 权限,datagram 诊断、源码与离线测试不能代替其余场景验收。
|
|
113
|
+
- 真实网络设备、防火墙/NAT/ECMP 组合,以及 Intel/x86_64 平台的本地运行矩阵。
|
|
114
|
+
- 公网路由、长期持续运行及生产监控集成;项目也不提供 MTR、Paris 或主动 PMTU 搜索。
|
|
115
|
+
|
|
116
|
+
macOS 本地 loopback TCP 捕获的 `unverified_local_capture` 表示缺少内核校验元信息,不表示报文已经通过校验;具体限制见平台说明。
|
data/exe/tracepath
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
5
|
+
require "tracepath/cli"
|
|
6
|
+
|
|
7
|
+
privileges = Tracepath::CLI::Privileges.new(executable: __FILE__)
|
|
8
|
+
exit Tracepath::CLI.new(ARGV, privileges: privileges).run
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Internal re-exec entrypoint: activate the caller's selected runtime gems
|
|
4
|
+
# before loading the exact same executable with its already validated argv.
|
|
5
|
+
require "rubygems"
|
|
6
|
+
require_relative "text_output"
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
count = Integer(ARGV.shift, 10)
|
|
10
|
+
raise ArgumentError, "invalid dependency count" unless (0..256).cover?(count)
|
|
11
|
+
|
|
12
|
+
count.times do
|
|
13
|
+
path = ARGV.shift
|
|
14
|
+
specification = path && Gem::Specification.load(path)
|
|
15
|
+
raise LoadError, "cannot load runtime dependency #{path}" unless specification
|
|
16
|
+
|
|
17
|
+
specification.activate
|
|
18
|
+
end
|
|
19
|
+
executable = ARGV.shift
|
|
20
|
+
raise ArgumentError, "missing executable" unless executable
|
|
21
|
+
|
|
22
|
+
$PROGRAM_NAME = executable
|
|
23
|
+
load executable
|
|
24
|
+
rescue Gem::LoadError, LoadError, ArgumentError => error
|
|
25
|
+
warn Tracepath::CLI::TextOutput.escape("tracepath: could not restore the Ruby runtime: #{error.message}")
|
|
26
|
+
exit 2
|
|
27
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Tracepath
|
|
6
|
+
class CLI
|
|
7
|
+
# Advisory startup check only: opening and closing sockets sends no probes.
|
|
8
|
+
# The transport remains responsible for reporting other operating-system
|
|
9
|
+
# errors and for validating the chosen route, source and capture interface.
|
|
10
|
+
class PrivilegeCheck
|
|
11
|
+
UNSUPPORTED_ERRORS = [Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT, Errno::ENOPROTOOPT].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(platform: RUBY_PLATFORM, sockets: Socket)
|
|
14
|
+
@platform = platform.include?("linux") ? :linux : (platform.include?("darwin") ? :darwin : nil)
|
|
15
|
+
@sockets = sockets
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def required?(options)
|
|
19
|
+
return false unless @platform
|
|
20
|
+
return false if @platform == :linux && options.protocol == :udp
|
|
21
|
+
|
|
22
|
+
families = options.family == :auto ? %i[ipv4 ipv6] : [options.family]
|
|
23
|
+
families.each do |family|
|
|
24
|
+
outcome = check_family(family, options.protocol)
|
|
25
|
+
return true if outcome == :permission_denied
|
|
26
|
+
return false unless outcome == :unsupported
|
|
27
|
+
end
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_family(family, protocol)
|
|
34
|
+
address_family = constant(family == :ipv4 ? :AF_INET : :AF_INET6)
|
|
35
|
+
icmp = constant(family == :ipv4 ? :IPPROTO_ICMP : :IPPROTO_ICMPV6)
|
|
36
|
+
check_socket(address_family, constant(:SOCK_RAW), icmp)
|
|
37
|
+
if protocol == :tcp
|
|
38
|
+
check_socket(address_family, constant(:SOCK_RAW), constant(:IPPROTO_TCP))
|
|
39
|
+
# Protocol zero checks AF_PACKET access without receiving any packets.
|
|
40
|
+
check_socket(constant(:AF_PACKET), constant(:SOCK_DGRAM), 0) if @platform == :linux
|
|
41
|
+
end
|
|
42
|
+
:available
|
|
43
|
+
rescue Errno::EPERM, Errno::EACCES
|
|
44
|
+
:permission_denied
|
|
45
|
+
rescue *UNSUPPORTED_ERRORS
|
|
46
|
+
:unsupported
|
|
47
|
+
rescue SystemCallError, SocketError
|
|
48
|
+
:other_error
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def constant(name) = @sockets.const_get(name)
|
|
52
|
+
|
|
53
|
+
def check_socket(family, type, protocol)
|
|
54
|
+
resource = @sockets.new(family, type, protocol)
|
|
55
|
+
ensure
|
|
56
|
+
begin
|
|
57
|
+
resource&.close
|
|
58
|
+
rescue SystemCallError, IOError
|
|
59
|
+
# A close failure is not evidence that opening the transport needs
|
|
60
|
+
# more privilege, and must not replace an earlier socket failure.
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|