dialer_integration_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. //go:build integration
  2. // Package tlsfingerprint provides TLS fingerprint simulation for HTTP clients.
  3. //
  4. // Integration tests for verifying TLS fingerprint correctness.
  5. // These tests make actual network requests to external services and should be run manually.
  6. //
  7. // Run with: go test -v -tags=integration ./internal/pkg/tlsfingerprint/...
  8. package tlsfingerprint
  9. import (
  10. "context"
  11. "encoding/json"
  12. "io"
  13. "net/http"
  14. "strings"
  15. "testing"
  16. "time"
  17. )
  18. // skipIfExternalServiceUnavailable checks if the external service is available.
  19. // If not, it skips the test instead of failing.
  20. func skipIfExternalServiceUnavailable(t *testing.T, err error) {
  21. t.Helper()
  22. if err != nil {
  23. // Check for common network/TLS errors that indicate external service issues
  24. errStr := err.Error()
  25. if strings.Contains(errStr, "certificate has expired") ||
  26. strings.Contains(errStr, "certificate is not yet valid") ||
  27. strings.Contains(errStr, "connection refused") ||
  28. strings.Contains(errStr, "connection reset") ||
  29. strings.Contains(errStr, "broken pipe") ||
  30. strings.Contains(errStr, "EOF") ||
  31. strings.Contains(errStr, "no such host") ||
  32. strings.Contains(errStr, "network is unreachable") ||
  33. strings.Contains(errStr, "timeout") ||
  34. strings.Contains(errStr, "deadline exceeded") {
  35. t.Skipf("skipping test: external service unavailable: %v", err)
  36. }
  37. t.Fatalf("failed to get fingerprint: %v", err)
  38. }
  39. }
  40. // TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
  41. // This test uses tls.peet.ws to verify the fingerprint.
  42. // Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
  43. // Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
  44. func TestJA3Fingerprint(t *testing.T) {
  45. if testing.Short() {
  46. t.Skip("skipping integration test in short mode")
  47. }
  48. profile := &Profile{
  49. Name: "Default Profile Test",
  50. EnableGREASE: false,
  51. }
  52. dialer := NewDialer(profile, nil)
  53. client := &http.Client{
  54. Transport: &http.Transport{
  55. DialTLSContext: dialer.DialTLSContext,
  56. },
  57. Timeout: 30 * time.Second,
  58. }
  59. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  60. defer cancel()
  61. req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
  62. if err != nil {
  63. t.Fatalf("failed to create request: %v", err)
  64. }
  65. req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0")
  66. resp, err := client.Do(req)
  67. skipIfExternalServiceUnavailable(t, err)
  68. defer func() { _ = resp.Body.Close() }()
  69. body, err := io.ReadAll(resp.Body)
  70. skipIfExternalServiceUnavailable(t, err)
  71. if resp.StatusCode != http.StatusOK {
  72. t.Skipf("skipping test: external service unhealthy: status %d", resp.StatusCode)
  73. }
  74. var fpResp FingerprintResponse
  75. if err := json.Unmarshal(body, &fpResp); err != nil {
  76. t.Logf("Response body: %s", string(body))
  77. t.Skipf("skipping test: external service returned unparsable response: %v", err)
  78. }
  79. t.Logf("JA3: %s", fpResp.TLS.JA3)
  80. t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
  81. t.Logf("JA4: %s", fpResp.TLS.JA4)
  82. expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
  83. if fpResp.TLS.JA3Hash == expectedJA3Hash {
  84. t.Logf("✓ JA3 hash matches: %s", expectedJA3Hash)
  85. } else {
  86. t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
  87. }
  88. expectedJA4CipherHash := "_5b57614c22b0_"
  89. if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
  90. t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
  91. } else {
  92. t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash)
  93. }
  94. }
  95. // TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
  96. // Run with: go test -v -tags=integration -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
  97. func TestAllProfiles(t *testing.T) {
  98. if testing.Short() {
  99. t.Skip("skipping integration test in short mode")
  100. }
  101. // Define all profiles to test with their expected fingerprints
  102. // These profiles are from config.yaml gateway.tls_fingerprint.profiles
  103. profiles := []TestProfileExpectation{
  104. {
  105. // Default profile (Node.js 24.x)
  106. Profile: &Profile{
  107. Name: "default_node_v24",
  108. EnableGREASE: false,
  109. },
  110. JA4CipherHash: "5b57614c22b0",
  111. },
  112. {
  113. // Linux x64 Node.js v22.17.1 (explicit profile with v22 extensions)
  114. Profile: &Profile{
  115. Name: "linux_x64_node_v22171",
  116. EnableGREASE: false,
  117. 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},
  118. Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
  119. PointFormats: []uint16{0, 1, 2},
  120. Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
  121. },
  122. JA4CipherHash: "a33745022dd6",
  123. },
  124. }
  125. for _, tc := range profiles {
  126. tc := tc // capture range variable
  127. t.Run(tc.Profile.Name, func(t *testing.T) {
  128. fp := fetchFingerprint(t, tc.Profile)
  129. if fp == nil {
  130. return // fetchFingerprint already called t.Fatal
  131. }
  132. t.Logf("Profile: %s", tc.Profile.Name)
  133. t.Logf(" JA3: %s", fp.JA3)
  134. t.Logf(" JA3 Hash: %s", fp.JA3Hash)
  135. t.Logf(" JA4: %s", fp.JA4)
  136. t.Logf(" PeetPrint: %s", fp.PeetPrint)
  137. t.Logf(" PeetPrintHash: %s", fp.PeetPrintHash)
  138. // Verify expectations
  139. if tc.ExpectedJA3 != "" {
  140. if fp.JA3Hash == tc.ExpectedJA3 {
  141. t.Logf(" ✓ JA3 hash matches: %s", tc.ExpectedJA3)
  142. } else {
  143. t.Errorf(" ✗ JA3 hash mismatch: got %s, expected %s", fp.JA3Hash, tc.ExpectedJA3)
  144. }
  145. }
  146. if tc.ExpectedJA4 != "" {
  147. if fp.JA4 == tc.ExpectedJA4 {
  148. t.Logf(" ✓ JA4 matches: %s", tc.ExpectedJA4)
  149. } else {
  150. t.Errorf(" ✗ JA4 mismatch: got %s, expected %s", fp.JA4, tc.ExpectedJA4)
  151. }
  152. }
  153. // Check JA4 cipher hash (stable middle part)
  154. // JA4 format: prefix_cipherHash_extHash
  155. if tc.JA4CipherHash != "" {
  156. if strings.Contains(fp.JA4, "_"+tc.JA4CipherHash+"_") {
  157. t.Logf(" ✓ JA4 cipher hash matches: %s", tc.JA4CipherHash)
  158. } else {
  159. t.Errorf(" ✗ JA4 cipher hash mismatch: got %s, expected cipher hash %s", fp.JA4, tc.JA4CipherHash)
  160. }
  161. }
  162. })
  163. }
  164. }
  165. // fetchFingerprint makes a request to tls.peet.ws and returns the TLS fingerprint info.
  166. func fetchFingerprint(t *testing.T, profile *Profile) *TLSInfo {
  167. t.Helper()
  168. dialer := NewDialer(profile, nil)
  169. client := &http.Client{
  170. Transport: &http.Transport{
  171. DialTLSContext: dialer.DialTLSContext,
  172. },
  173. Timeout: 30 * time.Second,
  174. }
  175. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  176. defer cancel()
  177. req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
  178. if err != nil {
  179. t.Fatalf("failed to create request: %v", err)
  180. return nil
  181. }
  182. req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0")
  183. resp, err := client.Do(req)
  184. skipIfExternalServiceUnavailable(t, err)
  185. defer func() { _ = resp.Body.Close() }()
  186. body, err := io.ReadAll(resp.Body)
  187. skipIfExternalServiceUnavailable(t, err)
  188. if resp.StatusCode != http.StatusOK {
  189. t.Skipf("skipping test: external service unhealthy: status %d", resp.StatusCode)
  190. }
  191. var fpResp FingerprintResponse
  192. if err := json.Unmarshal(body, &fpResp); err != nil {
  193. t.Logf("Response body: %s", string(body))
  194. t.Skipf("skipping test: external service returned unparsable response: %v", err)
  195. }
  196. return &fpResp.TLS
  197. }