---
title: "اثر انگشت TLS و ⁦JA3⁩ چیست و چگونه کار می‌کند؟"
description: "اثر انگشت TLS شناسه‌ای است که از فهرست رمزها و افزونه‌های پیام ClientHello ساخته می‌شود؛ محاسبه ⁦JA3⁩ و ⁦JA4⁩ و نقش پروکسی را توضیح می‌دهیم."
url: https://proxynet.io/fa/blog/tls-fingerprinting
date: 2026-09-19
author: "Acar Diveroli"
category: "وب اسکرپینگ, مبانی پروکسی"
lang: fa
---

# اثر انگشت TLS و ⁦JA3⁩ چیست و چگونه کار می‌کند؟

اسکریپتی به زبان پایتون نوشته‌اید. از یک پروکسی بیرون می‌رود، آدرس IP در هر درخواست عوض می‌شود و در هدر `User-Agent` هم یک رشته به‌روز از کروم گذاشته‌اید. با این حال سایت همان نخستین درخواست را با `403` پاسخ می‌دهد. وقتی همان آدرس را با همان IP در یک مرورگر واقعی باز می‌کنید، صفحه می‌آید. تفاوت در بسته‌ای است که پیش از هر هدر HTTP حرکت می‌کند: پیام `ClientHello` که اتصال رمزگذاری‌شده را آغاز می‌کند. سایت از روی چیدمان همین پیام تشخیص می‌دهد که طرف مقابلش کروم است، پایتون است یا curl، بی‌آنکه حتی یک هدر را بخواند.

در این نوشته دست‌دادن TLS، فیلدهای داخل `ClientHello`، شیوه محاسبه رشته ⁦JA3⁩ از این فیلدها و تفاوت کار ⁦JA4⁩ را می‌بینید. سپس به پرسشی می‌رسیم که بیش از همه شنیده‌ایم: آیا پروکسی اثر انگشت TLS را عوض می‌کند؟ در پایان یک نمونه آزموده‌شده پایتون هست که رشته ⁦JA3⁩ کلاینت خودتان را به‌صورت محلی نشان می‌دهد.

> **نکته: پاسخ کوتاه**
>
> اثر انگشت TLS خلاصه‌ای است که از فهرست نسخه، مجموعه‌های رمز، افزونه‌ها و منحنی‌های پیام `ClientHello` ساخته می‌شود؛ همان پیامی که کلاینت هنگام باز کردن اتصال رمزگذاری‌شده می‌فرستد. ⁦JA3⁩ این فهرست‌ها را به‌ترتیب به هم می‌چسباند و هش MD5 آن را می‌گیرد؛ ⁦JA4⁩ فهرست‌ها را مرتب می‌کند و پیشوندی خوانا به آن می‌افزاید. این مقدار **نرم‌افزار** را می‌شناساند، نه شخص را: همه نسخه‌های یکسان کروم اثر انگشت یکسانی دارند. تونل `CONNECT` در HTTP و پروکسی SOCKS5 به این پیام دست نمی‌زنند، یعنی پروکسی IP را عوض می‌کند، نه اثر انگشت TLS را.

## دست‌دادن TLS چیست؟

TLS پروتکلی است که ترافیک میان مرورگر و سایت را رمزگذاری می‌کند؛ همان `https://` نوار آدرس نشان می‌دهد که از آن استفاده می‌کنید. پیش از آغاز رمزگذاری، دو طرف باید بر سر نسخه، مجموعه رمز و کلید به توافق برسند. به این چانه‌زنی کوتاه دست‌دادن (handshake) می‌گویند. نسخه کنونی، یعنی ⁦TLS 1.3⁩، در [⁦RFC 8446⁩](https://www.rfc-editor.org/rfc/rfc8446#section-4.1.2) تعریف شده است و دست‌دادن تقریباً چنین پیش می‌رود:

1. کلاینت با TCP به سرور وصل می‌شود و پیام `ClientHello` می‌فرستد. در آن مجموعه‌های رمز پشتیبانی‌شده، افزونه‌ها و سهم کلید قرار دارد.
2. سرور از فهرست یکی را برمی‌گزیند و با `ServerHello` پاسخ می‌دهد. از این نقطه هر دو طرف می‌توانند کلید مشترک را استخراج کنند.
3. سرور گواهی خود و پیام `Finished` را که درستی دست‌دادن را ثابت می‌کند، هر دو رمزگذاری‌شده، می‌فرستد.
4. کلاینت گواهی را بررسی می‌کند و پیام `Finished` خودش را می‌فرستد.
5. نخستین درخواست HTTP (`GET /`، هدرها، `User-Agent`) تنها پس از این مرحله و از درون کانال رمزگذاری‌شده عبور می‌کند.

از نگاه اثر انگشت، گام نخست اهمیت دارد. `ClientHello` پیش از وجود هر کلید مشترکی فرستاده می‌شود، پس **رمزگذاری‌نشده** است؛ سرور، CDN جلوی آن و هر دستگاه شبکه در مسیر می‌تواند آن را همان‌گونه که هست بخواند، پیش از هر هدر، هر کوکی و هر خط جاوااسکریپت.

## پیام ClientHello چه فیلدهایی دارد؟

`ClientHello` فهرستی است که کلاینت در آن می‌گوید «این‌ها را می‌توانم صحبت کنم». فیلدهایی که در اثر انگشت به کار می‌روند این‌ها هستند:

| فیلد | چه چیزی دارد؟ | چرا متمایزکننده است؟ |
|---|---|---|
| `legacy_version` | فیلد نسخه قدیمی. کلاینت‌های ⁦TLS 1.3⁩ برای سازگاری هنوز مقدار ⁦TLS 1.2⁩ یعنی `771` را اینجا می‌نویسند | به‌تنهایی اطلاعات کمی دارد؛ نسخه واقعی در یک افزونه است |
| `cipher_suites` | مجموعه‌های رمز پشتیبانی‌شده، **به‌ترتیب اولویت** | هم فهرست و هم ترتیب از کتابخانه‌ای به کتابخانه دیگر فرق می‌کند |
| `extensions` | شماره نوع افزونه‌ها: `server_name` (0)، `supported_groups` (10)، `signature_algorithms` (13)، ALPN (16)، `supported_versions` (43)، `key_share` (51) و بقیه | اینکه چه افزونه‌هایی هستند و با چه ترتیبی، ویژه هر نرم‌افزار است |
| `supported_groups` | منحنی‌ها و گروه‌های قابل استفاده در تبادل کلید (`29` = x25519، `23` = secp256r1) | گروه‌های تازه زودتر به مرورگرها می‌رسند |
| `ec_point_formats` | قالب‌های نقطه منحنی بیضوی | برخی کتابخانه‌ها سه مقدار می‌فرستند و برخی تنها یکی |
| `signature_algorithms` | الگوریتم‌های امضای پذیرفته‌شده، به‌ترتیب | ⁦JA3⁩ از آن استفاده نمی‌کند، ⁦JA4⁩ می‌کند |
| ALPN | پروتکل کاربردی که کلاینت می‌خواهد صحبت کند (`h2`، `http/1.1`) | مرورگر `h2` می‌خواهد؛ کلاینت‌های ساده بیشتر وقت‌ها هیچ نمی‌خواهند |

پرکننده این فهرست‌ها برنامه شما نیست، بلکه **کتابخانه TLS** زیر آن است. کروم از BoringSSL و فایرفاکس از NSS استفاده می‌کند. ماژول `ssl` پایتون و Node.js روی OpenSSL بنا شده‌اند. curl همراه ویندوز از Schannel، یعنی پشته TLS خود سیستم‌عامل، بهره می‌برد. هر کتابخانه فهرست رمز پیش‌فرض، مجموعه افزونه و ترتیب متفاوتی دارد؛ هویت کلاینت این‌گونه به نخستین بسته دست‌دادن نشت می‌کند.

## اثر انگشت TLS چیست؟

اثر انگشت TLS همین فهرست‌های `ClientHello` است که به رشته‌ای کوتاه و قابل مقایسه فشرده شده‌اند. سه ویژگی آن را از دیگر سیگنال‌ها جدا می‌کند:

- **منفعل است.** سایت چیزی را در کلاینت اجرا نمی‌کند، فقط نخستین بسته رسیده را می‌خواند. خاموش کردن جاوااسکریپت یا پاک کردن کوکی‌ها در نتیجه اثری ندارد.
- **از هدرها مستقل است.** `User-Agent` یک خط متن است و در هر کتابخانه HTTP با یک خط کد عوض می‌شود. `ClientHello` از رفتار کامپایل‌شده کتابخانه می‌آید.
- **نرم‌افزار را می‌شناساند، نه شخص را.** هر کسی که نسخه یکسانی از کروم را روی سیستم‌عامل یکسانی اجرا کند همان مقدار را تولید می‌کند. لایه جاوااسکریپت (canvas، فونت، WebGL) که هدفش جدا کردن تک‌تک دستگاه‌هاست را در [اثر انگشت مرورگر چیست؟](/fa/blog/browser-fingerprinting) شرح داده‌ایم.

این روش برای تشخیص بات ابداع نشد. نخستین کاربردش امنیت شبکه بود: بدافزار ترافیکش را رمز می‌کند، اما کتابخانه TLS و تنظیماتش بیشتر وقت‌ها ثابت است. تیم امنیتی که محتوا را نمی‌بیند، همان خانواده نرم‌افزاری را از چیدمان `ClientHello` آن بازمی‌شناسد.

## ⁦JA3⁩ چیست و چگونه محاسبه می‌شود؟

⁦JA3⁩ نخستین روش فراگیری است که این ایده را به قالبی استاندارد بست. در سال 2017 در Salesforce به دست John Althouse، Jeff Atkinson و Josh Atkins ساخته و به‌صورت متن‌باز منتشر شد. آن‌گونه که در [مخزن ⁦JA3⁩ در Salesforce](https://github.com/salesforce/ja3) توضیح داده شده، محاسبه پنج گام دارد:

1. از پیام `ClientHello` پنج فیلد برداشته می‌شود: نسخه TLS، مجموعه‌های رمز، افزونه‌ها، منحنی‌ها (`supported_groups`) و قالب‌های نقطه منحنی بیضوی.
2. مقدارهای هر فیلد به **عدد ده‌دهی** تبدیل و به‌ترتیب حضورشان در پیام با `-` به هم چسبانده می‌شوند.
3. پنج فیلد با `,` به هم می‌پیوندند. اگر فیلدی خالی باشد، جایش خالی می‌ماند.
4. مقدارهای GREASE (پایین‌تر توضیح می‌دهیم) اصلاً وارد فهرست‌ها نمی‌شوند.
5. هش MD5 رشته حاصل گرفته می‌شود. این هش 32 نویسه‌ای همان اثر انگشت ⁦JA3⁩ است.

نمونه خود مخزن چنین است:

```text
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0
→ ada70206e40642a3e4461f35503241d5
```

از چپ که بخوانیم: `769` یعنی ⁦TLS 1.0⁩؛ سپس دوازده مجموعه رمز، سه افزونه، سه منحنی و یک قالب نقطه می‌آید. MD5 اینجا برای امنیت نیست، برای تبدیل رشته‌ای بلند به کلیدی جستجوپذیر با طول ثابت است.

دو نکته: چون کلاینت‌های ⁦TLS 1.3⁩ مقدار ⁦TLS 1.2⁩ را در فیلد نسخه می‌نویسند، تقریباً همه رشته‌های ⁦JA3⁩ امروزی با `771` آغاز می‌شوند. Salesforce مخزن را در 1 مه 2025 بایگانی کرد؛ ابزارهایی مانند Wireshark همچنان این مقدار را محاسبه می‌کنند، اما روش دیگر نگهداری نمی‌شود.

## GREASE چیست و چرا ⁦JA3⁩ آن را نادیده می‌گیرد؟

GREASE سازوکاری برای استحکام است که در [⁦RFC 8701⁩](https://www.rfc-editor.org/rfc/rfc8701) تعریف شده. کلاینت به فهرست مجموعه‌های رمز، افزونه‌ها و گروه‌های خود چند مقدار رزروشده تصادفی با الگوی `0x0A0A`، `0x1A1A` … `0xFAFA` می‌افزاید. این مقدارها هیچ معنایی ندارند. هدف آن است که پیوسته آزموده شود آیا سرورها مقدارهای ناشناخته را بی‌صدا نادیده می‌گیرند یا نه: سروری معیوب که با دیدن مقدار ناشناخته اتصال را قطع می‌کند، امروز شناسایی می‌شود، نه روزی که قابلیت تازه‌ای در TLS منتشر شود.

چون این مقدارها تصادفی انتخاب می‌شوند، اگر ⁦JA3⁩ آن‌ها را به حساب می‌آورد همان مرورگر در هر اتصال هش دیگری می‌ساخت. به همین دلیل مستندات روش می‌خواهد مقدارهای GREASE کنار گذاشته شوند؛ در کد پایین این کار را تابع `is_grease` انجام می‌دهد.

## چرا کروم ترتیب افزونه‌ها را به هم می‌ریزد؟

GREASE محتوای فهرست‌ها را نگه می‌دارد، نه ترتیبشان را. تکیه نرم‌افزارهای سرور و دستگاه‌های میانی بر **ترتیب ثابت افزونه‌های** کروم همان خطر را داشت، پس تیم کروم ترتیب را هم تصادفی کرد. بنا بر [رکورد موجود در Chrome Platform Status](https://chromestatus.com/feature/5124606246518784)، قابلیت «TLS ClientHello extension permutation» در ⁦Chrome 110⁩ به‌صورت پیش‌فرض روشن شد. دلیل ثبت‌شده فرار از اثر انگشت نیست، جلوگیری از شکنندگی اکوسیستم است: ترتیب ثابت، توسعه‌دهندگان سرور را به شناختن کروم و فرض کردن رفتاری مشخص می‌راند و تغییرهای آینده TLS را دشوار می‌کند. ⁦RFC 8446⁩ از پیش می‌گوید افزونه‌ها می‌توانند به هر ترتیبی بیایند؛ تنها استثنا `pre_shared_key` است که در صورت حضور باید آخر بایستد.

چون ⁦JA3⁩ افزونه‌ها را به‌ترتیب پیام به هم می‌چسباند، این تغییر هش ⁦JA3⁩ مرورگرهای مبتنی بر Chromium را ناپایدار کرد. با شنونده پایین اندازه گرفتیم: 24 اتصال پیاپی یک مرورگر مبتنی بر Chromium به **24 هش ⁦JA3⁩ متفاوت** رسید. وقتی شماره افزونه‌ها را مرتب کردیم، در همه یک مجموعه بود؛ تنها چیزی که عوض می‌شد ترتیب بود.

## ⁦JA4⁩ چیست و با ⁦JA3⁩ چه فرقی دارد؟

⁦JA4⁩ قالب تازه‌تری است که FoxIO منتشر کرده و به این مشکل پاسخ می‌دهد. بنا بر [سند فنی ⁦JA4⁩](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md) اثر انگشت از سه بخش به شکل `a_b_c` ساخته می‌شود. نمونه داخل سند، یعنی `t13d1516h2_8daaf6152771_e5627efa2ab1`، چنین خوانده می‌شود:

- **`t`**: TLS روی TCP (`q` یعنی QUIC و `d` یعنی DTLS).
- **`13`**: ⁦TLS 1.3⁩. ⁦JA4⁩ نسخه را از افزونه `supported_versions` می‌خواند، نه از فیلد قدیمی.
- **`d`**: افزونه SNI هست، یعنی کلاینت به یک نام دامنه وصل می‌شود (`i` نبودن SNI را نشان می‌دهد).
- **`15`** و **`16`**: 15 مجموعه رمز و 16 افزونه، بدون شمردن GREASE.
- **`h2`**: نویسه نخست و آخر نخستین مقدار فهرست ALPN، یعنی `HTTP/2`.
- **`8daaf6152771`**: کدهای شانزده‌شانزدهی مجموعه‌های رمز **مرتب** می‌شوند، با ⁦SHA-256⁩ هش می‌شوند و 12 نویسه نخست نگه داشته می‌شود.
- **`e5627efa2ab1`**: کدهای افزونه مرتب می‌شوند (بدون SNI و ALPN)، الگوریتم‌های امضا با ترتیب اصلی به آن افزوده و نتیجه به همان شکل هش می‌شود.

مرتب‌سازی اثر به‌هم‌ریختن افزونه‌ها را از میان می‌برد. بخش نخست خوانا هم اجازه می‌دهد بدون نگاه به هش‌ها تفکیکی درشت انجام دهید: دو بخش نخست که یکی به `h2` و دیگری به‌دلیل نفرستادن ALPN به `00` ختم می‌شود، از یک نرم‌افزار نیامده‌اند.

| | ⁦JA3⁩ | ⁦JA4⁩ |
|---|---|---|
| منتشرکننده | Salesforce (2017)، مخزن در 2025 بایگانی شد | FoxIO، توسعه ادامه دارد |
| قالب | یک هش MD5 با 32 نویسه | `a_b_c`: پیشوند خوانا + دو هش کوتاه‌شده ⁦SHA-256⁩ |
| ترتیب افزونه‌ها | ترتیب داخل پیام | مرتب‌شده؛ از به‌هم‌ریختن اثر نمی‌گیرد |
| نسخه TLS | فیلد نسخه قدیمی (`771` حتی در ⁦TLS 1.3⁩) | افزونه `supported_versions` |
| ALPN و SNI | فقط به‌شکل شماره افزونه دیده می‌شوند | هر دو در بخش نخست نوشته می‌شوند |
| الگوریتم‌های امضا | استفاده نمی‌شود | وارد بخش سوم می‌شود |
| GREASE | نادیده گرفته می‌شود | نادیده گرفته می‌شود |

بنا بر یادداشت مجوز در مخزن FoxIO، ⁦JA4⁩ که اثر انگشت کلاینت TLS است با مجوز ⁦BSD 3-Clause⁩ باز است؛ دیگر اعضای خانواده (JA4S، JA4H، JA4X، JA4T و ادامه آن) به مجوز جداگانه FoxIO وابسته‌اند.

## آیا پروکسی اثر انگشت TLS را عوض می‌کند؟

نه. دلیلش در شیوه حمل ترافیک HTTPS به دست پروکسی است.

در پروکسی HTTP، کلاینت نخست درخواست `CONNECT example.com:443` را به پروکسی می‌فرستد. پروکسی یک اتصال TCP به مقصد باز می‌کند، `200 Connection Established` می‌گوید و از آن پس تنها بایت جابه‌جا می‌کند. `ClientHello` از **درون** این تونل می‌گذرد: کلاینت شما آن را می‌سازد، سایت مقصد آن را می‌خواند و پروکسی نه محتوایش را عوض می‌کند و نه دوباره می‌نویسد. در SOCKS5 هم وضع همین است؛ پروتکل اتصال TCP را یک لایه پایین‌تر جابه‌جا می‌کند و اصلاً نمی‌داند داده‌ای که حمل می‌کند TLS است. جریان گام‌به‌گام هر دو پروتکل را در [تفاوت پروکسی SOCKS و HTTP](/fa/blog/socks-vs-http-proxy) شرح داده‌ایم.

نتیجه این است: سایت هم‌زمان آدرس IP پروکسی و اثر انگشت TLS **کلاینت شما** را می‌بیند. استفاده از [پروکسی HTTPS](https://proxynet.io/fa/https-proxy) یا [پروکسی SOCKS5](https://proxynet.io/fa/socks5-proxy) این را عوض نمی‌کند؛ رزیدنشیال، موبایل یا دیتاسنتر بودن پروکسی هم فرقی نمی‌سازد. با یک پروکسی آزمایشی محلی امتحان کردیم: همان کلاینت curl به شنونده پایین یک بار مستقیم، یک بار با تونل `CONNECT` در HTTP و یک بار روی SOCKS5 وصل شد و شنونده هر سه بار همان رشته ⁦JA3⁩ را ثبت کرد.

تنها حالتی که اثر انگشت عوض می‌شود، وجود واسطه‌ای در میانه است که TLS را **پایان می‌دهد**:

| چه چیزی در میانه است | چه کسی TLS را با مقصد برقرار می‌کند؟ | سایت کدام اثر انگشت را می‌بیند؟ |
|---|---|---|
| پروکسی HTTP (تونل `CONNECT`) | کلاینت شما | اثر انگشت کلاینت شما |
| پروکسی SOCKS5 | کلاینت شما | اثر انگشت کلاینت شما |
| VPN | کلاینت شما | اثر انگشت کلاینت شما |
| دروازه سازمانی با بازرسی TLS | دروازه | اثر انگشت دروازه |
| آنتی‌ویروس با پویش HTTPS روشن | آنتی‌ویروس | اثر انگشت آنتی‌ویروس |
| سرویسی که صفحه را به‌جای شما می‌گیرد | کلاینت خود سرویس | اثر انگشت کلاینت سرویس |

سطر پنجم را هنگام آماده کردن همین نوشته روی دستگاه خودمان دیدیم: curl در ویندوز وقتی مستقیم به سرویس بازتاب وصل می‌شد یک اثر انگشت و وقتی از تونل پروکسی محلی می‌گذشت اثر انگشت دیگری برمی‌گرداند. علت پویش HTTPS آنتی‌ویروس بود که اتصال مستقیم را با پشته TLS خودش دوباره برقرار می‌کرد. اگر مقدار سرویس بازتاب به کتابخانه‌ای که انتظار داشتید شبیه نیست، ببینید گواهی را چه کسی امضا کرده است.

## رشته ⁦JA3⁩ خودتان را چگونه ببینید؟

کوتاه‌ترین راه یک سرویس بازتاب است: [صفحه TLS در BrowserLeaks](https://browserleaks.com/tls) را در مرورگر باز کنید تا مقدارهای ⁦JA3⁩ و ⁦JA4⁩ خود را ببینید. اگر می‌خواهید در سطح بسته نگاه کنید، Wireshark این مقدارها را روی بسته‌های `ClientHello` خودش محاسبه می‌کند؛ در [مرجع فیلترهای نمایش](https://www.wireshark.org/docs/dfref/t/tls.html) این فیلدها با نام‌های `tls.handshake.ja3` و `tls.handshake.ja4` فهرست شده‌اند.

برای دیدن خود محاسبه می‌توانید از اسکریپت پایین استفاده کنید. تنها با کتابخانه استاندارد پایتون کار می‌کند و چیزی به بیرون نمی‌فرستد: روی پورت `8443` یک شنونده خام TCP باز می‌کند، پیام `ClientHello` هر کلاینت متصل را تجزیه می‌کند و رشته ⁦JA3⁩ و هش آن را چاپ می‌کند. هنگام شروع، کلاینت `urllib` خود پایتون را یک بار وصل می‌کند. شنونده دست‌دادن را کامل نمی‌کند، نخستین بسته را می‌خواند و می‌بندد؛ خطای اتصال در سمت کلاینت وضعیتی مورد انتظار است.

```python
import hashlib
import socket
import threading
import urllib.request

HOST, PORT = "127.0.0.1", 8443

def is_grease(value):
    # RFC 8701: 0x0a0a, 0x1a1a, ... 0xfafa
    return (value & 0x0F0F) == 0x0A0A and (value >> 8) == (value & 0xFF)

def read_client_hello(conn):
    data = b""
    while len(data) < 5:
        data += conn.recv(4096)
    if data[0] != 22:  # 22 = handshake record
        raise ValueError("not a TLS handshake")
    record_length = int.from_bytes(data[3:5], "big")
    while len(data) < 5 + record_length:
        chunk = conn.recv(4096)
        if not chunk:
            break
        data += chunk
    return data[5 : 5 + record_length]

def ja3_from_client_hello(hello):
    if hello[0] != 1:  # 1 = ClientHello
        raise ValueError("not a ClientHello")
    pos = 4  # message type (1) + length (3)
    version = int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2 + 32  # version + random
    pos += 1 + hello[pos]  # session_id
    size = int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2
    ciphers = [int.from_bytes(hello[i : i + 2], "big") for i in range(pos, pos + size, 2)]
    pos += size
    pos += 1 + hello[pos]  # compression_methods
    end = pos + 2 + int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2

    extensions, groups, point_formats = [], [], []
    while pos < end:
        ext_type = int.from_bytes(hello[pos : pos + 2], "big")
        ext_size = int.from_bytes(hello[pos + 2 : pos + 4], "big")
        body = hello[pos + 4 : pos + 4 + ext_size]
        pos += 4 + ext_size
        extensions.append(ext_type)
        if ext_type == 10:  # supported_groups
            groups = [int.from_bytes(body[i : i + 2], "big") for i in range(2, len(body), 2)]
        elif ext_type == 11:  # ec_point_formats
            point_formats = list(body[1:])

    def join(values):
        return "-".join(str(v) for v in values if not is_grease(v))

    fields = [str(version), join(ciphers), join(extensions), join(groups), join(point_formats)]
    ja3_text = ",".join(fields)
    return ja3_text, hashlib.md5(ja3_text.encode()).hexdigest()

def own_python_client():
    try:
        urllib.request.urlopen(f"https://localhost:{PORT}", timeout=5)
    except OSError:
        pass  # the listener closes without replying, which is expected

def main():
    server = socket.create_server((HOST, PORT))
    print(f"listening on https://localhost:{PORT}, press Ctrl+C to quit")
    threading.Thread(target=own_python_client, daemon=True).start()
    while True:
        conn, _ = server.accept()
        with conn:
            try:
                ja3_text, ja3_hash = ja3_from_client_hello(read_client_hello(conn))
            except (ValueError, IndexError, OSError) as exc:
                print("could not read:", exc)
                continue
        print(ja3_hash, ja3_text)

if __name__ == "__main__":
    main()
```

هنگام اجرای اسکریپت، از ترمینالی دیگر کلاینت‌های گوناگون را به همان آدرس بفرستید. در آدرس `localhost` بنویسید؛ کلاینتی که به آدرس IP وصل شود افزونه SNI را نمی‌فرستد و اثر انگشت عوض می‌شود.

```bash
curl -k https://localhost:8443
node -e "require('https').get('https://localhost:8443', { rejectUnauthorized: false }).on('error', () => {})"
```

روی دستگاه ما (⁦Windows 11⁩، ⁦Python 3.13⁩ با ⁦OpenSSL 3.0⁩، ⁦Node.js 24⁩ و ⁦curl 8⁩ کامپایل‌شده با Schannel) خروجی چنین بود. برای خوانایی، فیلد مجموعه‌های رمز را کوتاه کردیم:

```text
331a436afb23d4e31134c11b301bdcb5 771,4866-4867-4865-…,0-11-10-35-16-22-23-49-13-43-45-51-21,29-23-30-25-24-256-257-258-259-260,0-1-2
2e6c64f66822fc35b6a7a128b557f1de 771,4866-4865-49196-…,0-43-13-35-10-11-16-51-49-23-65281-45,29-23-24,0
944d1e1858cd278718f8a46b65d3212f 771,4866-4867-4865-…,65281-0-11-10-35-22-23-13-43-45-51,4588-29-23-30-24-25-256-257,0-1-2
```

یک دستگاه، یک IP و سه هویت جدا: به‌ترتیب پایتون، curl و Node.js. سرویس بازتاب برای پایتون دقیقاً همان هش را برگرداند، یعنی تجزیه‌گر درست کار می‌کند. عدد `4588` در سطر Node.js همان گروه ترکیبی پساکوانتومی تبادل کلید است که در ثبت‌های IANA با نام `X25519MLKEM768` می‌آید؛ این نسخه پایتون آن را نمی‌فرستد. در همان Node.js، `https.get` و `fetch` درون‌ساخت هم مقدار ⁦JA3⁩ متفاوتی دادند، چون `fetch` افزونه ALPN را می‌افزاید: اثر انگشت به کتابخانه HTTP بستگی دارد، نه به زبان. تفاوت کتابخانه‌ها در سمت پایتون را در [مقایسه HTTPX، Requests و AIOHTTP](/fa/blog/httpx-vs-requests-vs-aiohttp) سنجیده‌ایم.

برای سنجش اثر پروکسی، شنونده به کار نمی‌آید (پروکسی دوردست به `localhost` شما نمی‌رسد)، پس دو بار از سرویس بازتاب بپرسید:

```python
import json
import urllib.request

PROXY = "http://user:pass@pr.proxynet.io:8000"
URL = "https://tls.browserleaks.com/json"

def fingerprint(opener):
    with opener.open(URL, timeout=15) as response:
        result = json.load(response)
    return result["ja3_hash"], result["ja4"]

direct = urllib.request.build_opener(urllib.request.ProxyHandler({}))
proxied = urllib.request.build_opener(urllib.request.ProxyHandler({"https": PROXY}))

print("direct :", *fingerprint(direct))
print("proxied:", *fingerprint(proxied))
```

در هر دو سطر مقدارهای یکسانی می‌بینید؛ تنها چیزی که عوض می‌شود آدرس IP است که سرویس می‌بیند.

## صاحب سایت این داده را چگونه به کار می‌برد؟

محصولات CDN و فایروال مقدار ⁦JA3⁩ یا ⁦JA4⁩ را در کنار هر درخواست به‌صورت یک فیلد ارائه می‌کنند. صاحب سایت این فیلد را هنگام نوشتن قاعده و بررسی گزارش‌ها به کار می‌برد. کاربردهای رایج:

- **بررسی سازگاری.** اگر `User-Agent` می‌گوید «کروم» اما اثر انگشت با هیچ نسخه شناخته‌شده کروم جور نیست، هدر دستکاری شده است. به‌تنهایی دلیل قطعی نیست، ولی نشانه‌ای نیرومند است که بر امتیاز اثر می‌گذارد. خود این هدر را در [User-Agent چیست؟](/fa/blog/what-is-user-agent) شرح داده‌ایم.
- **محدودیت نرخ مستقل از IP.** اگر ترافیکی پخش‌شده روی صدها IP همان اثر انگشت را داشته باشد، شمارنده به‌جای IP به اثر انگشت بسته می‌شود. IP چرخشی این شمارنده را صفر نمی‌کند.
- **شناختن ابزارهای شناخته‌شده.** برای خانواده‌های بدافزار و ابزارهای پویش، فهرست‌های گردآوری‌شده اثر انگشت وجود دارد؛ تیم‌های امنیتی آن‌ها را با گزارش‌ها تطبیق می‌دهند.

محدودیت هم دارد. سایتی که مقدار یک مرورگر به‌روز را مسدود کند، همه بازدیدکنندگان آن مرورگر را مسدود کرده است. به‌روزرسانی مرورگر مقدار را عوض می‌کند و دروازه‌های سازمانی و آنتی‌ویروس‌ها اثر انگشت خودشان را در میانه می‌گذارند. به همین دلیل اثر انگشت TLS به‌تنهایی تصمیم نمی‌سازد؛ یکی از سیگنال‌هایی است که همراه اعتبار IP و رفتار وارد امتیاز می‌شود. کل این امتیازدهی را در [تشخیص بات چگونه کار می‌کند؟](/fa/blog/how-bot-detection-works) و لایه‌های Cloudflare را در [Cloudflare Precursor چیست؟](/fa/blog/cloudflare-precursor) بررسی کرده‌ایم.

## برای توسعه‌دهنده‌ای که داده جمع می‌کند چه معنایی دارد؟

به وضعیت آغاز نوشته برگردیم. کلاینت پایتون در سطر `User-Agent` می‌گوید کروم است و `ClientHello` آن می‌گوید OpenSSL است. سایت این تناقض را در نخستین بسته می‌بیند. واکنش درست پنهان کردن تناقض نیست، برداشتن آن است:

- **ادعا نکنید مرورگری هستید که نیستید.** مقدار `User-Agent` اسکریپت شما باید خود اسکریپت را معرفی کند: یک نام، یک نسخه و یک نشانی تماس. کلاینتی که با هویت صادقانه می‌آید، به `robots.txt` پایبند است و آرام کار می‌کند را صاحب سایت می‌شناسد و می‌تواند در فهرست مجاز بگذارد. قاعده‌ها را در [فایل robots.txt چیست و چگونه آن را بخوانیم؟](/fa/blog/robots-txt) شرح داده‌ایم.
- **اگر API رسمی هست، از آن استفاده کنید.** در درخواستی که با کلید API می‌آید هویت شما از پیش روشن است؛ بحث اثر انگشت برداشته می‌شود.
- **اگر صفحه واقعاً مرورگر می‌خواهد، مرورگر واقعی به کار ببرید.** وقتی صفحه‌ای را که با جاوااسکریپت ترسیم می‌شود با Playwright در یک Chromium واقعی باز می‌کنید، کلاینت شما واقعاً همان مرورگر است؛ هدرها، لایه TLS و محیط جاوااسکریپت با هم سازگارند. نصب آن در [Playwright چیست و چگونه با پروکسی استفاده می‌شود؟](/fa/blog/playwright-proxy) آمده است.
- **سرعت را پایین بیاورید و اجازه بگیرید.** برای کاری منظم و پرحجم، نوشتن به صاحب سایت بیشتر وقت‌ها ماندگارترین راه‌حل است.

دیگر دلیل‌های مسدود شدن و راه‌های مشروع آن را در [وب اسکرپینگ بدون مسدود شدن](/fa/blog/web-scraping-without-getting-blocked) گرد آورده‌ایم.

## کاربردها

- **تشخیص `403` در اسکرپر:** اگر با همان IP مرورگر رد می‌شود و اسکریپت نمی‌شود، تفاوت به احتمال زیاد در هویت کلاینت است. تفکیک کدهای وضعیت در [کدهای وضعیت HTTP در وب اسکرپینگ](/fa/blog/http-status-codes-web-scraping) و طرح کلی در صفحه [راهکار استخراج داده](/fa/data-scraping) ماست.
- **جدا کردن ترافیک بات در سایت خودتان:** گروه‌بندی گزارش‌ها بر پایه اثر انگشت، ترافیک پخش‌شده‌ای را نشان می‌دهد که گروه‌بندی بر پایه IP از دست می‌دهد.
- **معرفی خزنده (crawler) خودتان:** نسخه ثابت کتابخانه اثر انگشتی پایدار می‌سازد و خزنده شما در گزارش‌ها این‌گونه بازشناخته می‌شود. سمت زیرساخت در صفحه [راهکار وب کراولر](/fa/web-crawler) ماست.
- **درست کردن انتظار از پروکسی:** [پروکسی مسکونی](https://proxynet.io/fa/residential-proxy) اعتبار IP و موقعیت را عوض می‌کند؛ هویت کلاینت شما بر عهده خودتان می‌ماند.

## خطاهای رایج

- **گمان اینکه با عوض کردن `User-Agent` کلاینت عوض می‌شود.** هدر یک متن است؛ `ClientHello` رفتار کتابخانه است و پیش از هدر می‌رود.
- **انتظار عوض شدن اثر انگشت TLS از پروکسی.** پروکسی تونل‌ساز IP را عوض می‌کند و به دست‌دادن کاری ندارد.
- **انتظار ثابت ماندن هش ⁦JA3⁩ در مرورگر مبتنی بر Chromium.** ترتیب افزونه‌ها در هر اتصال عوض می‌شود؛ برای مقایسه از ⁦JA4⁩ یا فهرست افزونه مرتب‌شده استفاده کنید.
- **پذیرفتن نتیجه سرویس بازتاب بدون پرسش.** آنتی‌ویروس یا دروازه سازمانی که HTTPS را می‌پوید، اثر انگشت خودش را به سرویس نشان می‌دهد.
- **استفاده از `127.0.0.1` در اندازه‌گیری.** SNI فرستاده نمی‌شود و مقداری متفاوت با اتصال‌های واقعی به دست می‌آید.

## راهنمای انتخاب

| وضعیت | پیشنهاد |
|---|---|
| با پروکسی IP عوض می‌شود ولی در نخستین درخواست `403` می‌گیرید | به هویت کلاینت نگاه کنید: `User-Agent` و کتابخانه‌ای که به کار می‌برید یک چیز می‌گویند؟ |
| می‌خواهید مقدار ⁦JA3⁩ یا ⁦JA4⁩ کلاینت خودتان را بدانید | سرویس بازتاب یا همان شنونده محلی بالا |
| می‌خواهید ترافیک کروم را در گزارش‌ها گروه‌بندی کنید | ⁦JA4⁩ به‌جای ⁦JA3⁩؛ از به‌هم‌ریختن افزونه اثر نمی‌گیرد |
| صفحه جاوااسکریپت و مرورگر واقعی می‌خواهد | خودکارسازی با مرورگر واقعی (Playwright)، سرعت معقول و دامنه‌ای که سایت اجازه می‌دهد |
| سرویس بازتاب مقداری غیرمنتظره نشان می‌دهد | ببینید گواهی را چه کسی امضا کرده؛ ممکن است نرم‌افزاری در میانه TLS را پایان دهد |
| سایت شما ترافیک بات پخش‌شده دارد | محدودیت نرخ را به‌جای IP به جفت IP + اثر انگشت ببندید و بر پایه یک هش مسدود نکنید |

## پرسش‌های متداول

### تفاوت ⁦JA3⁩ و ⁦JA3S⁩ چیست؟

⁦JA3⁩ از پیام `ClientHello` کلاینت و ⁦JA3S⁩ از پیام `ServerHello` سرور ساخته می‌شود. چون سرور به کلاینت‌های گوناگون پاسخ‌های گوناگون می‌دهد، ⁦JA3S⁩ به‌تنهایی سرور را نمی‌شناساند؛ اما پاسخ آن به یک کلاینت همیشه یکسان است. تیم‌های امنیتی به همین دلیل این دو را جفتی به کار می‌برند.

### آیا استفاده از VPN اثر انگشت TLS را عوض می‌کند؟

نه. VPN ترافیک را از تونلی رمزگذاری‌شده می‌گذراند، اما دست‌دادن TLS با سایت را باز هم مرورگر یا اسکریپت شما انجام می‌دهد. سایت آدرس IP سرور VPN و اثر انگشت کلاینت شما را می‌بیند. تفاوت این دو ابزار را در [تفاوت پروکسی و VPN](/fa/blog/proxy-vs-vpn) شرح داده‌ایم.

### آیا اثر انگشت TLS مرا شخصاً می‌شناساند؟

به‌تنهایی نه. هر کسی که نسخه یکسانی از مرورگر را روی سیستم‌عامل یکسانی اجرا کند همان مقدار را می‌سازد. هویتی که به شخص نزدیک می‌شود از ترکیب این مقدار با آدرس IP، کوکی‌ها و سیگنال‌های لایه جاوااسکریپت پدید می‌آید.

### آیا به‌روزرسانی مرورگر اثر انگشت را عوض می‌کند؟

می‌تواند عوض کند. وقتی نسخه تازه یک مجموعه رمز را بردارد یا افزونه‌ای تازه بیفزاید، فهرست و هش عوض می‌شود. فهرست‌های اثر انگشت به همین دلیل نسخه‌به‌نسخه نگه داشته می‌شوند.

### آیا پنجره ناشناس یا پاک کردن کوکی‌ها بر اثر انگشت TLS اثر دارد؟

نه. پیام `ClientHello` را کتابخانه TLS مرورگر می‌سازد؛ تاریخچه، کوکی‌ها و نوع پنجره وارد این فهرست‌ها نمی‌شوند.

### اگر به‌خاطر اثر انگشتم مسدود می‌شوم چه کنم؟

نخست اندازه بگیرید: ببینید در سرویس بازتاب چه مقداری دیده می‌شود و آیا نرم‌افزاری در میانه TLS را پایان می‌دهد. اگر اسکریپت می‌نویسید، هویت خود را صادقانه اعلام کنید، سرعت را پایین بیاورید و از API رسمی یا کانال اجازه سایت استفاده کنید. اگر با مرورگر معمولی مسدود می‌شوید، مشکل به احتمال زیاد در اعتبار IP است، نه در اثر انگشت.

## خلاصه

اثر انگشت TLS از `ClientHello` بیرون می‌آید، یعنی نخستین بسته رمزگذاری‌نشده یک اتصال رمزگذاری‌شده. ⁦JA3⁩ پنج فهرست این پیام را به‌ترتیب به هم می‌چسباند و هش MD5 را می‌گیرد؛ وقتی کروم ترتیب افزونه‌ها را به هم ریخت این هش ناپایدار شد و ⁦JA4⁩ با مرتب کردن فهرست‌ها آن را درست کرد. این مقدار نرم‌افزار را نشان می‌دهد نه شخص را، پیش از هدرها می‌رود و هیچ پروکسی تونل‌سازی به آن دست نمی‌زند: پروکسی IP را عوض می‌کند و هویت کلاینت نزد شما می‌ماند. برای افزودن نوع درست IP در کنار هویت کلاینت پایدار و صادقانه می‌توانید [خدمات پروکسی ما](/fa/proxy) را ببینید.
