7a4d0cf7cd
控制面跑明文 HTTP 在国外 IP 上,白盒诊断请求 URL 里出现 google/youtube 等 GFW 敏感词时被墙重置(返回空)→ 出海段拿不到、报告显示「—」。改:host 用 base64url 传(host_b64),响应也回 b64、不回明文,明文里无敏感词(不影响真实加密隧道流量)。 - diag.go: EgressTiming 支持 ?host_b64=,解码后查白名单,响应 echo b64 - vpn_whitebox.py: node_egress 用 base64url 编码 host 验证:cara(墙内)google/youtube 出海段从 None → 8-9ms,与 github/cloudflare 一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.9 KiB
Go
110 lines
3.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// diagAllowedHosts 限定可测目标(防 SSRF):仅白盒测试用的公共站点。
|
|
// 端点只返回耗时数字、不返回响应体,且目标受限,信息泄露面极小。
|
|
var diagAllowedHosts = map[string]bool{
|
|
"www.google.com": true, "github.com": true, "www.cloudflare.com": true,
|
|
"www.youtube.com": true, "x.com": true, "www.facebook.com": true,
|
|
"en.wikipedia.org": true, "api.openai.com": true, "www.reddit.com": true,
|
|
"www.instagram.com": true,
|
|
"www.baidu.com": true, "www.qq.com": true, "www.taobao.com": true,
|
|
"www.jd.com": true, "www.bilibili.com": true, "weibo.com": true,
|
|
"www.163.com": true, "www.zhihu.com": true, "www.aliyun.com": true,
|
|
}
|
|
|
|
// DiagHandler 暴露只读诊断端点:在节点上量「节点→目标」的出海段耗时,
|
|
// 供白盒把端到端延迟拆成「接入段(客户端→节点) vs 出海段(节点→目标)」。
|
|
type DiagHandler struct{}
|
|
|
|
// NewDiagHandler 构造 DiagHandler。
|
|
func NewDiagHandler() *DiagHandler { return &DiagHandler{} }
|
|
|
|
type egressTiming struct {
|
|
Host string `json:"host"`
|
|
DNSMs *float64 `json:"dns_ms"`
|
|
TCPMs *float64 `json:"tcp_ms"`
|
|
TLSMs *float64 `json:"tls_ms"`
|
|
TTFBMs *float64 `json:"ttfb_ms"`
|
|
Err string `json:"err,omitempty"`
|
|
}
|
|
|
|
// EgressTiming 处理 GET /v1/diag/egress?host=X 或 ?host_b64=<base64url(host)>:
|
|
// 量 节点→host 的 DNS/TCP/TLS/TTFB 各段独立耗时(出海段),返回 JSON。
|
|
// host_b64 用途:控制面跑明文 HTTP 在国外 IP 上,URL/响应里若出现 google/youtube
|
|
// 等 GFW 敏感词会被墙重置;客户端改传 base64、服务端解码并在响应里回 base64(echo),
|
|
// 明文里不出现敏感词,避免诊断请求被 GFW 拦(不影响真实加密隧道流量)。
|
|
func (h *DiagHandler) EgressTiming(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
host := r.URL.Query().Get("host")
|
|
echo := host // 响应里回显的标识;用 b64 时回 b64(不回明文敏感词)
|
|
if b := r.URL.Query().Get("host_b64"); b != "" {
|
|
dec, err := base64.RawURLEncoding.DecodeString(b)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(egressTiming{Err: "bad host_b64"})
|
|
return
|
|
}
|
|
host, echo = string(dec), b
|
|
}
|
|
if !diagAllowedHosts[host] {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_ = json.NewEncoder(w).Encode(egressTiming{Host: echo, Err: "host not allowed"})
|
|
return
|
|
}
|
|
res := measureEgress(host)
|
|
res.Host = echo // 不在响应里回明文敏感词
|
|
_ = json.NewEncoder(w).Encode(res)
|
|
}
|
|
|
|
// measureEgress 在本机(节点)上测到 host:443 的分段耗时。各段为该阶段独立耗时。
|
|
func measureEgress(host string) egressTiming {
|
|
res := egressTiming{Host: host}
|
|
ms := func(d time.Duration) *float64 { v := float64(d.Microseconds()) / 1000.0; return &v }
|
|
|
|
t0 := time.Now()
|
|
ips, err := net.LookupHost(host)
|
|
if err != nil || len(ips) == 0 {
|
|
res.Err = "dns"
|
|
return res
|
|
}
|
|
tDNS := time.Now()
|
|
res.DNSMs = ms(tDNS.Sub(t0))
|
|
|
|
conn, err := (&net.Dialer{Timeout: 12 * time.Second}).Dial("tcp", net.JoinHostPort(ips[0], "443"))
|
|
if err != nil {
|
|
res.Err = "tcp"
|
|
return res
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
tTCP := time.Now()
|
|
res.TCPMs = ms(tTCP.Sub(tDNS))
|
|
|
|
// 只测出海段链路握手耗时,不校验证书(InsecureSkipVerify:诊断用途,不传数据)。
|
|
tlsConn := tls.Client(conn, &tls.Config{ServerName: host, InsecureSkipVerify: true}) //nolint:gosec
|
|
_ = tlsConn.SetDeadline(time.Now().Add(12 * time.Second))
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
res.Err = "tls"
|
|
return res
|
|
}
|
|
tTLS := time.Now()
|
|
res.TLSMs = ms(tTLS.Sub(tTCP))
|
|
|
|
if _, err := tlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + host +
|
|
"\r\nUser-Agent: pangolin-diag\r\nConnection: close\r\n\r\n")); err == nil {
|
|
buf := make([]byte, 64)
|
|
if _, err := tlsConn.Read(buf); err == nil {
|
|
res.TTFBMs = ms(time.Since(tTLS))
|
|
}
|
|
}
|
|
return res
|
|
}
|