dialer_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. //go:build unit
  2. // Package tlsfingerprint provides TLS fingerprint simulation for HTTP clients.
  3. //
  4. // Unit tests for TLS fingerprint dialer.
  5. // Integration tests that require external network are in dialer_integration_test.go
  6. // and require the 'integration' build tag.
  7. //
  8. // Run unit tests: go test -v ./internal/pkg/tlsfingerprint/...
  9. // Run integration tests: go test -v -tags=integration ./internal/pkg/tlsfingerprint/...
  10. package tlsfingerprint
  11. import (
  12. "context"
  13. "encoding/json"
  14. "io"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "strings"
  19. "testing"
  20. "time"
  21. )
  22. // TestDialerBasicConnection tests that the dialer can establish TLS connections.
  23. func TestDialerBasicConnection(t *testing.T) {
  24. skipNetworkTest(t)
  25. // Create a dialer with default profile
  26. profile := &Profile{
  27. Name: "Test Profile",
  28. EnableGREASE: false,
  29. }
  30. dialer := NewDialer(profile, nil)
  31. // Create HTTP client with custom TLS dialer
  32. client := &http.Client{
  33. Transport: &http.Transport{
  34. DialTLSContext: dialer.DialTLSContext,
  35. },
  36. Timeout: 30 * time.Second,
  37. }
  38. // Make a request to a known HTTPS endpoint
  39. resp, err := client.Get("https://www.google.com")
  40. if err != nil {
  41. t.Fatalf("failed to connect: %v", err)
  42. }
  43. defer func() { _ = resp.Body.Close() }()
  44. if resp.StatusCode != http.StatusOK {
  45. t.Errorf("expected status 200, got %d", resp.StatusCode)
  46. }
  47. }
  48. // TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
  49. // This test uses tls.peet.ws to verify the fingerprint.
  50. // Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
  51. // Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
  52. func TestJA3Fingerprint(t *testing.T) {
  53. skipNetworkTest(t)
  54. profile := &Profile{
  55. Name: "Default Profile Test",
  56. EnableGREASE: false,
  57. }
  58. dialer := NewDialer(profile, nil)
  59. client := &http.Client{
  60. Transport: &http.Transport{
  61. DialTLSContext: dialer.DialTLSContext,
  62. },
  63. Timeout: 30 * time.Second,
  64. }
  65. // Use tls.peet.ws fingerprint detection API
  66. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  67. defer cancel()
  68. req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
  69. if err != nil {
  70. t.Fatalf("failed to create request: %v", err)
  71. }
  72. req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0")
  73. resp, err := client.Do(req)
  74. if err != nil {
  75. t.Fatalf("failed to get fingerprint: %v", err)
  76. }
  77. defer func() { _ = resp.Body.Close() }()
  78. body, err := io.ReadAll(resp.Body)
  79. if err != nil {
  80. t.Fatalf("failed to read response: %v", err)
  81. }
  82. var fpResp FingerprintResponse
  83. if err := json.Unmarshal(body, &fpResp); err != nil {
  84. t.Logf("Response body: %s", string(body))
  85. t.Fatalf("failed to parse fingerprint response: %v", err)
  86. }
  87. // Log all fingerprint information
  88. t.Logf("JA3: %s", fpResp.TLS.JA3)
  89. t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
  90. t.Logf("JA4: %s", fpResp.TLS.JA4)
  91. t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint)
  92. t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash)
  93. // Verify JA3 hash matches expected value (Node.js 24.x default)
  94. expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
  95. if fpResp.TLS.JA3Hash == expectedJA3Hash {
  96. t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
  97. } else {
  98. t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
  99. }
  100. // Verify JA4 cipher hash (stable middle part)
  101. expectedJA4CipherHash := "_5b57614c22b0_"
  102. if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
  103. t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
  104. } else {
  105. t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash)
  106. }
  107. // Verify JA4 prefix (t13d1714h1 or t13i1714h1)
  108. expectedJA4Prefix := "t13d1714h1"
  109. if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) {
  110. t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 17=ciphers, 14=extensions, h1=HTTP/1.1)", expectedJA4Prefix)
  111. } else {
  112. altPrefix := "t13i1714h1"
  113. if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) {
  114. t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix)
  115. } else {
  116. t.Errorf("✗ JA4 prefix mismatch: got %s, expected %s or %s", fpResp.TLS.JA4, expectedJA4Prefix, altPrefix)
  117. }
  118. }
  119. // Verify JA3 contains expected TLS 1.3 cipher suites
  120. if strings.Contains(fpResp.TLS.JA3, "4865-4866-4867") {
  121. t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites")
  122. } else {
  123. t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites")
  124. }
  125. // Verify extension list (14 extensions, Node.js 24.x order)
  126. expectedExtensions := "0-65037-23-65281-10-11-35-16-5-13-18-51-45-43"
  127. if strings.Contains(fpResp.TLS.JA3, expectedExtensions) {
  128. t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions)
  129. } else {
  130. t.Logf("Warning: JA3 extension list may differ")
  131. }
  132. }
  133. func skipNetworkTest(t *testing.T) {
  134. if testing.Short() {
  135. t.Skip("跳过网络测试(short 模式)")
  136. }
  137. if os.Getenv("TLSFINGERPRINT_NETWORK_TESTS") != "1" {
  138. t.Skip("跳过网络测试(需要设置 TLSFINGERPRINT_NETWORK_TESTS=1)")
  139. }
  140. }
  141. // TestDialerWithProfile tests that different profiles produce different fingerprints.
  142. func TestDialerWithProfile(t *testing.T) {
  143. // Create two dialers with different profiles
  144. profile1 := &Profile{
  145. Name: "Profile 1 - No GREASE",
  146. EnableGREASE: false,
  147. }
  148. profile2 := &Profile{
  149. Name: "Profile 2 - With GREASE",
  150. EnableGREASE: true,
  151. }
  152. dialer1 := NewDialer(profile1, nil)
  153. dialer2 := NewDialer(profile2, nil)
  154. // Build specs and compare
  155. // Note: We can't directly compare JA3 without making network requests
  156. // but we can verify the specs are different
  157. spec1 := buildClientHelloSpecFromProfile(dialer1.profile)
  158. spec2 := buildClientHelloSpecFromProfile(dialer2.profile)
  159. // Profile with GREASE should have more extensions
  160. if len(spec2.Extensions) <= len(spec1.Extensions) {
  161. t.Error("expected GREASE profile to have more extensions")
  162. }
  163. }
  164. // TestHTTPProxyDialerBasic tests HTTP proxy dialer creation.
  165. // Note: This is a unit test - actual proxy testing requires a proxy server.
  166. func TestHTTPProxyDialerBasic(t *testing.T) {
  167. profile := &Profile{
  168. Name: "Test Profile",
  169. EnableGREASE: false,
  170. }
  171. // Test that dialer is created without panic
  172. proxyURL := mustParseURL("http://proxy.example.com:8080")
  173. dialer := NewHTTPProxyDialer(profile, proxyURL)
  174. if dialer == nil {
  175. t.Fatal("expected dialer to be created")
  176. }
  177. if dialer.profile == profile || dialer.profile.Name != profile.Name {
  178. t.Error("expected an independent copy of the profile")
  179. }
  180. profile.Name = "changed after construction"
  181. if dialer.profile.Name != "Test Profile" {
  182. t.Error("caller mutation changed the dialer profile")
  183. }
  184. if dialer.proxyURL != proxyURL {
  185. t.Error("expected proxyURL to be set")
  186. }
  187. }
  188. // TestSOCKS5ProxyDialerBasic tests SOCKS5 proxy dialer creation.
  189. // Note: This is a unit test - actual proxy testing requires a proxy server.
  190. func TestSOCKS5ProxyDialerBasic(t *testing.T) {
  191. profile := &Profile{
  192. Name: "Test Profile",
  193. EnableGREASE: false,
  194. }
  195. // Test that dialer is created without panic
  196. proxyURL := mustParseURL("socks5://proxy.example.com:1080")
  197. dialer := NewSOCKS5ProxyDialer(profile, proxyURL)
  198. if dialer == nil {
  199. t.Fatal("expected dialer to be created")
  200. }
  201. if dialer.profile == profile || dialer.profile.Name != profile.Name {
  202. t.Error("expected an independent copy of the profile")
  203. }
  204. profile.Name = "changed after construction"
  205. if dialer.profile.Name != "Test Profile" {
  206. t.Error("caller mutation changed the dialer profile")
  207. }
  208. if dialer.proxyURL != proxyURL {
  209. t.Error("expected proxyURL to be set")
  210. }
  211. }
  212. // TestBuildClientHelloSpec tests ClientHello spec construction.
  213. func TestBuildClientHelloSpec(t *testing.T) {
  214. // Test with nil profile (should use defaults)
  215. spec := buildClientHelloSpecFromProfile(nil)
  216. if len(spec.CipherSuites) == 0 {
  217. t.Error("expected cipher suites to be set")
  218. }
  219. if len(spec.Extensions) == 0 {
  220. t.Error("expected extensions to be set")
  221. }
  222. // Verify default cipher suites are used
  223. if len(spec.CipherSuites) != len(defaultCipherSuites) {
  224. t.Errorf("expected %d cipher suites, got %d", len(defaultCipherSuites), len(spec.CipherSuites))
  225. }
  226. // Test with custom profile
  227. customProfile := &Profile{
  228. Name: "Custom",
  229. EnableGREASE: false,
  230. CipherSuites: []uint16{0x1301, 0x1302},
  231. }
  232. spec = buildClientHelloSpecFromProfile(customProfile)
  233. if len(spec.CipherSuites) != 2 {
  234. t.Errorf("expected 2 cipher suites, got %d", len(spec.CipherSuites))
  235. }
  236. }
  237. // TestToUTLSCurves tests curve ID conversion.
  238. func TestToUTLSCurves(t *testing.T) {
  239. input := []uint16{0x001d, 0x0017, 0x0018}
  240. result := toUTLSCurves(input)
  241. if len(result) != len(input) {
  242. t.Errorf("expected %d curves, got %d", len(input), len(result))
  243. }
  244. for i, curve := range result {
  245. if uint16(curve) != input[i] {
  246. t.Errorf("curve %d: expected 0x%04x, got 0x%04x", i, input[i], uint16(curve))
  247. }
  248. }
  249. }
  250. // Helper function to parse URL without error handling.
  251. func mustParseURL(rawURL string) *url.URL {
  252. u, err := url.Parse(rawURL)
  253. if err != nil {
  254. panic(err)
  255. }
  256. return u
  257. }
  258. // TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
  259. // Run with: go test -v -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
  260. func TestAllProfiles(t *testing.T) {
  261. skipNetworkTest(t)
  262. profiles := []TestProfileExpectation{
  263. {
  264. // Default profile (Node.js 24.x)
  265. // JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e
  266. // JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
  267. Profile: &Profile{
  268. Name: "default_node_v24",
  269. EnableGREASE: false,
  270. },
  271. JA4CipherHash: "5b57614c22b0",
  272. },
  273. {
  274. // Linux x64 Node.js v22.17.1 (explicit profile)
  275. Profile: &Profile{
  276. Name: "linux_x64_node_v22171",
  277. EnableGREASE: false,
  278. 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},
  279. Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
  280. PointFormats: []uint16{0, 1, 2},
  281. Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
  282. },
  283. JA4CipherHash: "a33745022dd6",
  284. },
  285. }
  286. for _, tc := range profiles {
  287. tc := tc // capture range variable
  288. t.Run(tc.Profile.Name, func(t *testing.T) {
  289. fp := fetchFingerprint(t, tc.Profile)
  290. if fp == nil {
  291. return // fetchFingerprint already called t.Fatal
  292. }
  293. t.Logf("Profile: %s", tc.Profile.Name)
  294. t.Logf(" JA3: %s", fp.JA3)
  295. t.Logf(" JA3 Hash: %s", fp.JA3Hash)
  296. t.Logf(" JA4: %s", fp.JA4)
  297. t.Logf(" PeetPrint: %s", fp.PeetPrint)
  298. t.Logf(" PeetPrintHash: %s", fp.PeetPrintHash)
  299. // Verify expectations
  300. if tc.ExpectedJA3 != "" {
  301. if fp.JA3Hash == tc.ExpectedJA3 {
  302. t.Logf(" ✓ JA3 hash matches: %s", tc.ExpectedJA3)
  303. } else {
  304. t.Errorf(" ✗ JA3 hash mismatch: got %s, expected %s", fp.JA3Hash, tc.ExpectedJA3)
  305. }
  306. }
  307. if tc.ExpectedJA4 != "" {
  308. if fp.JA4 == tc.ExpectedJA4 {
  309. t.Logf(" ✓ JA4 matches: %s", tc.ExpectedJA4)
  310. } else {
  311. t.Errorf(" ✗ JA4 mismatch: got %s, expected %s", fp.JA4, tc.ExpectedJA4)
  312. }
  313. }
  314. // Check JA4 cipher hash (stable middle part)
  315. // JA4 format: prefix_cipherHash_extHash
  316. if tc.JA4CipherHash != "" {
  317. if strings.Contains(fp.JA4, "_"+tc.JA4CipherHash+"_") {
  318. t.Logf(" ✓ JA4 cipher hash matches: %s", tc.JA4CipherHash)
  319. } else {
  320. t.Errorf(" ✗ JA4 cipher hash mismatch: got %s, expected cipher hash %s", fp.JA4, tc.JA4CipherHash)
  321. }
  322. }
  323. })
  324. }
  325. }
  326. // fetchFingerprint makes a request to tls.peet.ws and returns the TLS fingerprint info.
  327. func fetchFingerprint(t *testing.T, profile *Profile) *TLSInfo {
  328. t.Helper()
  329. dialer := NewDialer(profile, nil)
  330. client := &http.Client{
  331. Transport: &http.Transport{
  332. DialTLSContext: dialer.DialTLSContext,
  333. },
  334. Timeout: 30 * time.Second,
  335. }
  336. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  337. defer cancel()
  338. req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
  339. if err != nil {
  340. t.Fatalf("failed to create request: %v", err)
  341. return nil
  342. }
  343. req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0")
  344. resp, err := client.Do(req)
  345. if err != nil {
  346. t.Fatalf("failed to get fingerprint: %v", err)
  347. return nil
  348. }
  349. defer func() { _ = resp.Body.Close() }()
  350. body, err := io.ReadAll(resp.Body)
  351. if err != nil {
  352. t.Fatalf("failed to read response: %v", err)
  353. return nil
  354. }
  355. var fpResp FingerprintResponse
  356. if err := json.Unmarshal(body, &fpResp); err != nil {
  357. t.Logf("Response body: %s", string(body))
  358. t.Fatalf("failed to parse fingerprint response: %v", err)
  359. return nil
  360. }
  361. return &fpResp.TLS
  362. }