| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- //go:build integration
- // Package tlsfingerprint provides TLS fingerprint simulation for HTTP clients.
- //
- // Integration tests for verifying TLS fingerprint correctness.
- // These tests make actual network requests to external services and should be run manually.
- //
- // Run with: go test -v -tags=integration ./internal/pkg/tlsfingerprint/...
- package tlsfingerprint
- import (
- "context"
- "encoding/json"
- "io"
- "net/http"
- "strings"
- "testing"
- "time"
- )
- // skipIfExternalServiceUnavailable checks if the external service is available.
- // If not, it skips the test instead of failing.
- func skipIfExternalServiceUnavailable(t *testing.T, err error) {
- t.Helper()
- if err != nil {
- // Check for common network/TLS errors that indicate external service issues
- errStr := err.Error()
- if strings.Contains(errStr, "certificate has expired") ||
- strings.Contains(errStr, "certificate is not yet valid") ||
- strings.Contains(errStr, "connection refused") ||
- strings.Contains(errStr, "connection reset") ||
- strings.Contains(errStr, "broken pipe") ||
- strings.Contains(errStr, "EOF") ||
- strings.Contains(errStr, "no such host") ||
- strings.Contains(errStr, "network is unreachable") ||
- strings.Contains(errStr, "timeout") ||
- strings.Contains(errStr, "deadline exceeded") {
- t.Skipf("skipping test: external service unavailable: %v", err)
- }
- t.Fatalf("failed to get fingerprint: %v", err)
- }
- }
- // TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
- // This test uses tls.peet.ws to verify the fingerprint.
- // Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
- // Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
- func TestJA3Fingerprint(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
- profile := &Profile{
- Name: "Default Profile Test",
- EnableGREASE: false,
- }
- dialer := NewDialer(profile, nil)
- client := &http.Client{
- Transport: &http.Transport{
- DialTLSContext: dialer.DialTLSContext,
- },
- Timeout: 30 * time.Second,
- }
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
- req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
- if err != nil {
- t.Fatalf("failed to create request: %v", err)
- }
- req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0")
- resp, err := client.Do(req)
- skipIfExternalServiceUnavailable(t, err)
- defer func() { _ = resp.Body.Close() }()
- body, err := io.ReadAll(resp.Body)
- skipIfExternalServiceUnavailable(t, err)
- if resp.StatusCode != http.StatusOK {
- t.Skipf("skipping test: external service unhealthy: status %d", resp.StatusCode)
- }
- var fpResp FingerprintResponse
- if err := json.Unmarshal(body, &fpResp); err != nil {
- t.Logf("Response body: %s", string(body))
- t.Skipf("skipping test: external service returned unparsable response: %v", err)
- }
- t.Logf("JA3: %s", fpResp.TLS.JA3)
- t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
- t.Logf("JA4: %s", fpResp.TLS.JA4)
- expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
- if fpResp.TLS.JA3Hash == expectedJA3Hash {
- t.Logf("✓ JA3 hash matches: %s", expectedJA3Hash)
- } else {
- t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
- }
- expectedJA4CipherHash := "_5b57614c22b0_"
- if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
- t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
- } else {
- t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash)
- }
- }
- // TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
- // Run with: go test -v -tags=integration -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
- func TestAllProfiles(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
- // Define all profiles to test with their expected fingerprints
- // These profiles are from config.yaml gateway.tls_fingerprint.profiles
- profiles := []TestProfileExpectation{
- {
- // Default profile (Node.js 24.x)
- Profile: &Profile{
- Name: "default_node_v24",
- EnableGREASE: false,
- },
- JA4CipherHash: "5b57614c22b0",
- },
- {
- // Linux x64 Node.js v22.17.1 (explicit profile with v22 extensions)
- Profile: &Profile{
- Name: "linux_x64_node_v22171",
- EnableGREASE: false,
- 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},
- Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
- PointFormats: []uint16{0, 1, 2},
- Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
- },
- JA4CipherHash: "a33745022dd6",
- },
- }
- for _, tc := range profiles {
- tc := tc // capture range variable
- t.Run(tc.Profile.Name, func(t *testing.T) {
- fp := fetchFingerprint(t, tc.Profile)
- if fp == nil {
- return // fetchFingerprint already called t.Fatal
- }
- t.Logf("Profile: %s", tc.Profile.Name)
- t.Logf(" JA3: %s", fp.JA3)
- t.Logf(" JA3 Hash: %s", fp.JA3Hash)
- t.Logf(" JA4: %s", fp.JA4)
- t.Logf(" PeetPrint: %s", fp.PeetPrint)
- t.Logf(" PeetPrintHash: %s", fp.PeetPrintHash)
- // Verify expectations
- if tc.ExpectedJA3 != "" {
- if fp.JA3Hash == tc.ExpectedJA3 {
- t.Logf(" ✓ JA3 hash matches: %s", tc.ExpectedJA3)
- } else {
- t.Errorf(" ✗ JA3 hash mismatch: got %s, expected %s", fp.JA3Hash, tc.ExpectedJA3)
- }
- }
- if tc.ExpectedJA4 != "" {
- if fp.JA4 == tc.ExpectedJA4 {
- t.Logf(" ✓ JA4 matches: %s", tc.ExpectedJA4)
- } else {
- t.Errorf(" ✗ JA4 mismatch: got %s, expected %s", fp.JA4, tc.ExpectedJA4)
- }
- }
- // Check JA4 cipher hash (stable middle part)
- // JA4 format: prefix_cipherHash_extHash
- if tc.JA4CipherHash != "" {
- if strings.Contains(fp.JA4, "_"+tc.JA4CipherHash+"_") {
- t.Logf(" ✓ JA4 cipher hash matches: %s", tc.JA4CipherHash)
- } else {
- t.Errorf(" ✗ JA4 cipher hash mismatch: got %s, expected cipher hash %s", fp.JA4, tc.JA4CipherHash)
- }
- }
- })
- }
- }
- // fetchFingerprint makes a request to tls.peet.ws and returns the TLS fingerprint info.
- func fetchFingerprint(t *testing.T, profile *Profile) *TLSInfo {
- t.Helper()
- dialer := NewDialer(profile, nil)
- client := &http.Client{
- Transport: &http.Transport{
- DialTLSContext: dialer.DialTLSContext,
- },
- Timeout: 30 * time.Second,
- }
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
- req, err := http.NewRequestWithContext(ctx, "GET", "https://tls.peet.ws/api/all", nil)
- if err != nil {
- t.Fatalf("failed to create request: %v", err)
- return nil
- }
- req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0")
- resp, err := client.Do(req)
- skipIfExternalServiceUnavailable(t, err)
- defer func() { _ = resp.Body.Close() }()
- body, err := io.ReadAll(resp.Body)
- skipIfExternalServiceUnavailable(t, err)
- if resp.StatusCode != http.StatusOK {
- t.Skipf("skipping test: external service unhealthy: status %d", resp.StatusCode)
- }
- var fpResp FingerprintResponse
- if err := json.Unmarshal(body, &fpResp); err != nil {
- t.Logf("Response body: %s", string(body))
- t.Skipf("skipping test: external service returned unparsable response: %v", err)
- }
- return &fpResp.TLS
- }
|