//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, "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) if err != nil { t.Fatalf("failed to read response: %v", err) } var fpResp FingerprintResponse if err := json.Unmarshal(body, &fpResp); err != nil { t.Logf("Response body: %s", string(body)) t.Fatalf("failed to parse fingerprint 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) if err != nil { t.Fatalf("failed to read response: %v", err) return nil } var fpResp FingerprintResponse if err := json.Unmarshal(body, &fpResp); err != nil { t.Logf("Response body: %s", string(body)) t.Fatalf("failed to parse fingerprint response: %v", err) return nil } return &fpResp.TLS }