dialer_capture_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. //go:build integration
  2. package tlsfingerprint
  3. import (
  4. "context"
  5. "encoding/json"
  6. "io"
  7. "net/http"
  8. "os"
  9. "strings"
  10. "testing"
  11. "time"
  12. utls "github.com/refraction-networking/utls"
  13. )
  14. // CapturedFingerprint 对应 tls-fingerprint-web 返回的 Fingerprint 结构。
  15. // 用于反序列化 capture server 的 JSON 响应。
  16. type CapturedFingerprint struct {
  17. JA3Raw string `json:"ja3_raw"`
  18. JA3Hash string `json:"ja3_hash"`
  19. JA4 string `json:"ja4"`
  20. HTTP2 string `json:"http2"`
  21. CipherSuites []int `json:"cipher_suites"`
  22. Curves []int `json:"curves"`
  23. PointFormats []int `json:"point_formats"`
  24. Extensions []int `json:"extensions"`
  25. SignatureAlgorithms []int `json:"signature_algorithms"`
  26. ALPNProtocols []string `json:"alpn_protocols"`
  27. SupportedVersions []int `json:"supported_versions"`
  28. KeyShareGroups []int `json:"key_share_groups"`
  29. PSKModes []int `json:"psk_modes"`
  30. CompressCertAlgos []int `json:"compress_cert_algos"`
  31. EnableGREASE bool `json:"enable_grease"`
  32. }
  33. // TestDialerAgainstCaptureServer 连接 tls-fingerprint-web capture server,
  34. // 验证 Dialer 的 TLS 指纹是否匹配配置的 Profile。
  35. //
  36. // 该测试依赖外部服务,默认跳过。需要手动验证时设置:
  37. // TLSFINGERPRINT_CAPTURE_URL=https://localhost:8443
  38. //
  39. // 运行方式:go test -tags=integration -v -run TestDialerAgainstCaptureServer ./internal/pkg/tlsfingerprint/...
  40. func TestDialerAgainstCaptureServer(t *testing.T) {
  41. captureURL := strings.TrimSpace(os.Getenv("TLSFINGERPRINT_CAPTURE_URL"))
  42. if captureURL == "" {
  43. t.Skip("跳过外部 TLS 指纹 capture 测试:未设置 TLSFINGERPRINT_CAPTURE_URL")
  44. }
  45. tests := []struct {
  46. name string
  47. profile *Profile
  48. }{
  49. {
  50. name: "default_profile",
  51. profile: &Profile{
  52. Name: "default",
  53. EnableGREASE: false,
  54. // 全部留空时使用内置默认值
  55. },
  56. },
  57. {
  58. name: "linux_x64_node_v22171",
  59. profile: &Profile{
  60. Name: "linux_x64_node_v22171",
  61. EnableGREASE: false,
  62. CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255},
  63. Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
  64. PointFormats: []uint16{0, 1, 2},
  65. SignatureAlgorithms: []uint16{0x0403, 0x0503, 0x0603, 0x0807, 0x0808, 0x0809, 0x080a, 0x080b, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601, 0x0303, 0x0301, 0x0302, 0x0402, 0x0502, 0x0602},
  66. ALPNProtocols: []string{"http/1.1"},
  67. SupportedVersions: []uint16{0x0304, 0x0303},
  68. KeyShareGroups: []uint16{29},
  69. PSKModes: []uint16{1},
  70. Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
  71. },
  72. },
  73. {
  74. name: "macos_arm64_node_v2430",
  75. profile: &Profile{
  76. Name: "MacOS_arm64_node_v2430",
  77. EnableGREASE: false,
  78. CipherSuites: []uint16{4865, 4866, 4867, 49195, 49199, 49196, 49200, 52393, 52392, 49161, 49171, 49162, 49172, 156, 157, 47, 53},
  79. Curves: []uint16{29, 23, 24},
  80. PointFormats: []uint16{0},
  81. SignatureAlgorithms: []uint16{0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601, 0x0201},
  82. ALPNProtocols: []string{"http/1.1"},
  83. SupportedVersions: []uint16{0x0304, 0x0303},
  84. KeyShareGroups: []uint16{29},
  85. PSKModes: []uint16{1},
  86. Extensions: []uint16{0, 65037, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43},
  87. },
  88. },
  89. }
  90. for _, tc := range tests {
  91. t.Run(tc.name, func(t *testing.T) {
  92. captured := fetchCapturedFingerprint(t, captureURL, tc.profile)
  93. if captured == nil {
  94. return
  95. }
  96. t.Logf("JA3 Hash: %s", captured.JA3Hash)
  97. t.Logf("JA4: %s", captured.JA4)
  98. // 解析实际生效的 Profile 值,也就是 Dialer 最终使用的值。
  99. effectiveCipherSuites := tc.profile.CipherSuites
  100. if len(effectiveCipherSuites) == 0 {
  101. effectiveCipherSuites = defaultCipherSuites
  102. }
  103. effectiveCurves := tc.profile.Curves
  104. if len(effectiveCurves) == 0 {
  105. effectiveCurves = make([]uint16, len(defaultCurves))
  106. for i, c := range defaultCurves {
  107. effectiveCurves[i] = uint16(c)
  108. }
  109. }
  110. effectivePointFormats := tc.profile.PointFormats
  111. if len(effectivePointFormats) == 0 {
  112. effectivePointFormats = defaultPointFormats
  113. }
  114. effectiveSigAlgs := tc.profile.SignatureAlgorithms
  115. if len(effectiveSigAlgs) == 0 {
  116. effectiveSigAlgs = make([]uint16, len(defaultSignatureAlgorithms))
  117. for i, s := range defaultSignatureAlgorithms {
  118. effectiveSigAlgs[i] = uint16(s)
  119. }
  120. }
  121. effectiveALPN := tc.profile.ALPNProtocols
  122. if len(effectiveALPN) == 0 {
  123. effectiveALPN = []string{"http/1.1"}
  124. }
  125. effectiveVersions := tc.profile.SupportedVersions
  126. if len(effectiveVersions) == 0 {
  127. effectiveVersions = []uint16{0x0304, 0x0303}
  128. }
  129. effectiveKeyShare := tc.profile.KeyShareGroups
  130. if len(effectiveKeyShare) == 0 {
  131. effectiveKeyShare = []uint16{29} // X25519
  132. }
  133. effectivePSKModes := tc.profile.PSKModes
  134. if len(effectivePSKModes) == 0 {
  135. effectivePSKModes = []uint16{1} // psk_dhe_ke
  136. }
  137. // 校验每个指纹字段
  138. assertIntSliceEqual(t, "cipher_suites", uint16sToInts(effectiveCipherSuites), captured.CipherSuites)
  139. assertIntSliceEqual(t, "curves", uint16sToInts(effectiveCurves), captured.Curves)
  140. assertIntSliceEqual(t, "point_formats", uint16sToInts(effectivePointFormats), captured.PointFormats)
  141. assertIntSliceEqual(t, "signature_algorithms", uint16sToInts(effectiveSigAlgs), captured.SignatureAlgorithms)
  142. assertStringSliceEqual(t, "alpn_protocols", effectiveALPN, captured.ALPNProtocols)
  143. assertIntSliceEqual(t, "supported_versions", uint16sToInts(effectiveVersions), captured.SupportedVersions)
  144. assertIntSliceEqual(t, "key_share_groups", uint16sToInts(effectiveKeyShare), captured.KeyShareGroups)
  145. assertIntSliceEqual(t, "psk_modes", uint16sToInts(effectivePSKModes), captured.PSKModes)
  146. if captured.EnableGREASE != tc.profile.EnableGREASE {
  147. t.Errorf("enable_grease: got %v, want %v", captured.EnableGREASE, tc.profile.EnableGREASE)
  148. } else {
  149. t.Logf(" enable_grease: %v OK", captured.EnableGREASE)
  150. }
  151. // 校验扩展顺序;如果 Profile 显式配置了 Extensions 就使用配置值,
  152. // 否则使用默认顺序(Node.js 24.x)。
  153. expectedExtOrder := uint16sToInts(defaultExtensionOrder)
  154. if len(tc.profile.Extensions) > 0 {
  155. expectedExtOrder = uint16sToInts(tc.profile.Extensions)
  156. }
  157. // 比较前从期望值和采集值中剔除 GREASE。
  158. var filteredExpected, filteredActual []int
  159. for _, e := range expectedExtOrder {
  160. if !isGREASEValue(uint16(e)) {
  161. filteredExpected = append(filteredExpected, e)
  162. }
  163. }
  164. for _, e := range captured.Extensions {
  165. if !isGREASEValue(uint16(e)) {
  166. filteredActual = append(filteredActual, e)
  167. }
  168. }
  169. assertIntSliceEqual(t, "extensions (order, non-GREASE)", filteredExpected, filteredActual)
  170. // 打印完整采集结果,便于排查指纹差异。
  171. capturedJSON, _ := json.MarshalIndent(captured, " ", " ")
  172. t.Logf("Full captured fingerprint:\n %s", string(capturedJSON))
  173. })
  174. }
  175. }
  176. func fetchCapturedFingerprint(t *testing.T, captureURL string, profile *Profile) *CapturedFingerprint {
  177. t.Helper()
  178. dialer := NewDialer(profile, nil)
  179. client := &http.Client{
  180. Transport: &http.Transport{
  181. DialTLSContext: dialer.DialTLSContext,
  182. },
  183. Timeout: 10 * time.Second,
  184. }
  185. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  186. defer cancel()
  187. req, err := http.NewRequestWithContext(ctx, "POST", captureURL, strings.NewReader(`{"model":"test"}`))
  188. if err != nil {
  189. t.Fatalf("create request: %v", err)
  190. return nil
  191. }
  192. req.Header.Set("Content-Type", "application/json")
  193. req.Header.Set("Authorization", "Bearer test-token")
  194. resp, err := client.Do(req)
  195. if err != nil {
  196. t.Fatalf("request failed: %v", err)
  197. return nil
  198. }
  199. defer resp.Body.Close()
  200. body, err := io.ReadAll(resp.Body)
  201. if err != nil {
  202. t.Fatalf("read body: %v", err)
  203. return nil
  204. }
  205. var fp CapturedFingerprint
  206. if err := json.Unmarshal(body, &fp); err != nil {
  207. t.Logf("Response body: %s", string(body))
  208. t.Fatalf("parse response: %v", err)
  209. return nil
  210. }
  211. return &fp
  212. }
  213. func uint16sToInts(vals []uint16) []int {
  214. result := make([]int, len(vals))
  215. for i, v := range vals {
  216. result[i] = int(v)
  217. }
  218. return result
  219. }
  220. func assertIntSliceEqual(t *testing.T, name string, expected, actual []int) {
  221. t.Helper()
  222. if len(expected) != len(actual) {
  223. t.Errorf("%s: length mismatch: got %d, want %d", name, len(actual), len(expected))
  224. if len(actual) < 20 && len(expected) < 20 {
  225. t.Errorf(" got: %v", actual)
  226. t.Errorf(" want: %v", expected)
  227. }
  228. return
  229. }
  230. mismatches := 0
  231. for i := range expected {
  232. if expected[i] != actual[i] {
  233. if mismatches < 5 {
  234. t.Errorf("%s[%d]: got %d (0x%04x), want %d (0x%04x)", name, i, actual[i], actual[i], expected[i], expected[i])
  235. }
  236. mismatches++
  237. }
  238. }
  239. if mismatches == 0 {
  240. t.Logf(" %s: %d items OK", name, len(expected))
  241. } else if mismatches > 5 {
  242. t.Errorf(" %s: %d/%d mismatches (showing first 5)", name, mismatches, len(expected))
  243. }
  244. }
  245. func assertStringSliceEqual(t *testing.T, name string, expected, actual []string) {
  246. t.Helper()
  247. if len(expected) != len(actual) {
  248. t.Errorf("%s: length mismatch: got %d (%v), want %d (%v)", name, len(actual), actual, len(expected), expected)
  249. return
  250. }
  251. for i := range expected {
  252. if expected[i] != actual[i] {
  253. t.Errorf("%s[%d]: got %q, want %q", name, i, actual[i], expected[i])
  254. return
  255. }
  256. }
  257. t.Logf(" %s: %v OK", name, expected)
  258. }
  259. // TestBuildClientHelloSpecNewFields tests that new Profile fields are correctly applied.
  260. func TestBuildClientHelloSpecNewFields(t *testing.T) {
  261. // Test custom ALPN, versions, key shares, PSK modes
  262. profile := &Profile{
  263. Name: "custom_full",
  264. EnableGREASE: false,
  265. CipherSuites: []uint16{0x1301, 0x1302},
  266. Curves: []uint16{29, 23},
  267. PointFormats: []uint16{0},
  268. SignatureAlgorithms: []uint16{0x0403, 0x0804},
  269. ALPNProtocols: []string{"h2", "http/1.1"},
  270. SupportedVersions: []uint16{0x0304},
  271. KeyShareGroups: []uint16{29, 23},
  272. PSKModes: []uint16{1},
  273. }
  274. spec := buildClientHelloSpecFromProfile(profile)
  275. // Verify cipher suites
  276. if len(spec.CipherSuites) != 2 || spec.CipherSuites[0] != 0x1301 {
  277. t.Errorf("cipher suites: got %v", spec.CipherSuites)
  278. }
  279. // Check extensions for expected values
  280. var foundALPN, foundVersions, foundKeyShare, foundPSK, foundSigAlgs bool
  281. for _, ext := range spec.Extensions {
  282. switch e := ext.(type) {
  283. case *utls.ALPNExtension:
  284. foundALPN = true
  285. if len(e.AlpnProtocols) != 2 || e.AlpnProtocols[0] != "h2" {
  286. t.Errorf("ALPN: got %v, want [h2, http/1.1]", e.AlpnProtocols)
  287. }
  288. case *utls.SupportedVersionsExtension:
  289. foundVersions = true
  290. if len(e.Versions) != 1 || e.Versions[0] != 0x0304 {
  291. t.Errorf("versions: got %v, want [0x0304]", e.Versions)
  292. }
  293. case *utls.KeyShareExtension:
  294. foundKeyShare = true
  295. if len(e.KeyShares) != 2 {
  296. t.Errorf("key shares: got %d entries, want 2", len(e.KeyShares))
  297. }
  298. case *utls.PSKKeyExchangeModesExtension:
  299. foundPSK = true
  300. if len(e.Modes) != 1 || e.Modes[0] != 1 {
  301. t.Errorf("PSK modes: got %v, want [1]", e.Modes)
  302. }
  303. case *utls.SignatureAlgorithmsExtension:
  304. foundSigAlgs = true
  305. if len(e.SupportedSignatureAlgorithms) != 2 {
  306. t.Errorf("sig algs: got %d, want 2", len(e.SupportedSignatureAlgorithms))
  307. }
  308. }
  309. }
  310. for name, found := range map[string]bool{
  311. "ALPN": foundALPN, "Versions": foundVersions, "KeyShare": foundKeyShare,
  312. "PSK": foundPSK, "SigAlgs": foundSigAlgs,
  313. } {
  314. if !found {
  315. t.Errorf("extension %s not found in spec", name)
  316. }
  317. }
  318. // Test nil profile uses all defaults
  319. specDefault := buildClientHelloSpecFromProfile(nil)
  320. for _, ext := range specDefault.Extensions {
  321. switch e := ext.(type) {
  322. case *utls.ALPNExtension:
  323. if len(e.AlpnProtocols) != 1 || e.AlpnProtocols[0] != "http/1.1" {
  324. t.Errorf("default ALPN: got %v, want [http/1.1]", e.AlpnProtocols)
  325. }
  326. case *utls.SupportedVersionsExtension:
  327. if len(e.Versions) != 2 {
  328. t.Errorf("default versions: got %v, want 2 entries", e.Versions)
  329. }
  330. case *utls.KeyShareExtension:
  331. if len(e.KeyShares) != 1 {
  332. t.Errorf("default key shares: got %d, want 1", len(e.KeyShares))
  333. }
  334. }
  335. }
  336. t.Log("TestBuildClientHelloSpecNewFields passed")
  337. }