网络排障最容易出现的一种误判,是把“代理端口能连上”说成“网络正常”。端口有监听,只说明某个TCP连接建立了;它不证明代理认证成功、目标域名能解析、证书可信或业务接口返回了正确数据。
这篇CLI的目标不是300毫秒内确诊所有故障,而是把观察到的事实分层记录。没有测到的层就留为未测,不让一盏绿灯替其他层作结论。
先写清请求实际走哪条路
直连、HTTP CONNECT和SOCKS不是同一条探测路径。直连HTTPS常涉及目标解析、TCP与TLS;经代理时,还要分清连接代理、代理认证、隧道建立,以及目标解析由哪一侧完成。
Go的ProxyFromEnvironment按HTTP_PROXY、HTTPS_PROXY与NO_PROXY等变量决定某个请求是否使用代理。它不能被当成任意工具都一致支持ALL_PROXY的依据,也不能因为代理地址不是私网IP就判定配置错误。net/http官方文档
| 观察 | 能说明什么 | 不能说明什么 |
|---|---|---|
| 代理端口连接成功 | 当前地址上有可建立的TCP连接 | 代理协议和认证正确 |
| 域名解析得到IP | 当前解析路径给出了结果 | 连接、证书和业务正常 |
| TLS握手通过 | 此次握手满足采用的校验策略 | HTTP业务成功 |
| HTTP收到503 | 已收到应用层响应 | 服务满足业务要求 |
图中状态是独立探测的并列示意,不是一次请求必然同时出现的结果。DNS探测失败而TCP探测成功,可能涉及不同目标、缓存或直接使用IP;应把目标与路径一起记录。
一个不访问外网的最小演示
下面netcheck.go创建本地教学TLS服务。它打算分别检查TCP连接、默认信任策略下的TLS失败,以及信任测试证书后收到HTTP 503。测试客户端的证书信任设置只用于httptest,不能搬到生产环境绕过证书校验。
package main import ( "fmt" "io" "log" "net" "net/http" "net/http/httptest" "time" ) func main() { server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) _, _ = w.Write([]byte("maintenance")) })) server.Config.ErrorLog = log.New(io.Discard, "", 0) server.StartTLS() defer server.Close() conn, err := net.DialTimeout("tcp", server.Listener.Addr().String(), time.Second) if err != nil { panic(err) } _ = conn.Close() fmt.Println("TCP: connected") transport := &http.Transport{Proxy: nil, TLSHandshakeTimeout: time.Second} defer transport.CloseIdleConnections() client := &http.Client{Transport: transport, Timeout: 2 * time.Second} resp, err := client.Get(server.URL) if err == nil { resp.Body.Close() panic("untrusted teaching certificate unexpectedly accepted") } fmt.Println("TLS: rejected untrusted teaching certificate") trusted := server.Client() // httptest-only trust setup trusted.Timeout = 2 * time.Second defer trusted.CloseIdleConnections() resp, err = trusted.Get(server.URL) if err != nil { panic(err) } defer resp.Body.Close() _, _ = io.Copy(io.Discard, resp.Body) if resp.StatusCode != http.StatusServiceUnavailable { panic("wrong status") } fmt.Println("HTTP:", resp.StatusCode) }在允许本地监听的隔离环境中可执行:
go run netcheck.go预期输出是TCP连接成功、未信任的教学证书被拒绝,最后收到HTTP 503。这是预期,不是本文已经观察到的运行结果。
本次使用go1.26.5 darwin/arm64,程序已完成编译,但沙盒在httptest.NewUnstartedServer准备监听端口时拒绝了操作:
listen tcp6 [::1]:0: bind: operation not permitted因此示例未进入三个网络断言,不能把它写成测试通过。我没有重试或改权限绕过这一限制,也没有连接真实代理、外网接口或生产服务。读者在允许的环境复现时,应保留完整错误,而不是把监听失败归类成“目标DNS坏了”。
2026年10月19日Go 1.27.3的发布记录包含crypto/x509安全修复。这里引用它说明证书相关工具也需要维护;本地教学编译器版本是1.26.5,并不代表推荐的部署版本,更不能伪称已经测试1.27.3。Go发布记录
故障报告要能保留未知
实际CLI应为每个阶段记录目标、开始时间、截止时间、错误类别和原始原因,并对总预算计时。DNS超时只能报告“本次解析在预算内未完成”,不能仅凭这一条就确诊解析服务器损坏;TCP拒绝也可能来自策略或监听状态,而不是唯一某个进程没启动。
下一步可以在获准的本地测试环境加入需要认证的假代理、NO_PROXY、IPv6、过期证书和应用超时用例。这些尚未运行。当前完成的是分层设计、可编译示例和一份真实的环境限制记录,没有网络延迟或故障定位准确率数据。
10月25日只是文章排期;封面与内图均为教学概念图,不是工具运行截图。