TlsClient
TlsClient لایه TLS سمت client در WaterWall است و از نسخه BoringSSL موجود در همین repository استفاده میکند. داده
cleartext را از node قبلی میگیرد و پس از رمزنگاری بهشکل TLS record به node بعدی میفرستد. TLS recordهای برگشتی نیز در
جهت downstream رمزگشایی میشوند و بهصورت cleartext به node قبلی برمیگردند.
این node یک handshake واقعی TLS در سمت client انجام میدهد. ساختار handshake تا حد ممکن به Chromeهای جدید نزدیک شده است؛
از جمله در پیشفرضهای ALPN، ALPS، استفاده از GREASE، جابهجایی ترتیب extensionها، پشتیبانی از certificate compression و گروه
hybrid اختیاری X25519MLKEM768.
جایگاه رایج
chain سمت client برای HTTPS:
HttpClient -> TlsClient -> TcpConnector
chain سمت client همراه با proxy protocol:
TrojanClient -> TlsClient -> TcpConnector
VlessClient -> TlsClient -> TcpConnector
TlsClient خودش socket باز نمیکند. معمولاً بعد از آن nodeای مثل TcpConnector قرار میگیرد تا بایتهای رمزنگاریشده TLS
را به server راه دور برساند.
نمونه
[
{
"name": "tls-client",
"type": "TlsClient",
"settings": {
"sni": "example.com",
"alpns": ["http/1.1"],
"verify": true,
"x25519mlkem768": true,
"verbose": false
},
"next": "server-out"
},
{
"name": "server-out",
"type": "TcpConnector",
"settings": {
"address": "example.com",
"port": 443,
"nodelay": true
}
}
]
فیلدهای اجباری
فیلدهای top-level:
| field | type | توضیح |
|---|---|---|
name | string | نام دلخواه node؛ باید داخل config یکتا باشد. |
type | string | باید دقیقاً "TlsClient" باشد. |
settings | object | تنظیمات TLS client. |
next | string | node stream-facing که TLS recordهای رمزنگاریشده را حمل میکند؛ در استفاده معمول لازم است. |
فیلد اجباری داخل settings:
| field | type | توضیح |
|---|---|---|
sni | string | مقدار server Name Indication در TLS با طول ۱ تا ۲۵۵ byte. |
وقتی verify فعال باشد، همین sni برای certificate verification هم استفاده میشود.
تنظیمات اختیاری
| field | default | توضیح |
|---|---|---|
alpns | ["h2", "http/1.1"] | فهرست ترتیبی protocolهای پیشنهادی ALPN؛ ترتیب JSON عیناً در ClientHello حفظ میشود. |
verify | true | بررسی certificate متعلق به peer را در BoringSSL فعال میکند. |
x25519mlkem768 | true | گروه hybrid توافق کلید با نام X25519MLKEM768 را اعلام میکند؛ الگوریتمهای امضا را تغییر نمیدهد. |
fragment | تنظیم نشده | تنظیمات StreamFragmenter داخلی برای تقسیم و تأخیر بایتهای TLS خروجی. |
tls13-record-shaping | تنظیم نشده | تنظیم آزمایشی padding و delay سمت فرستنده برای TLS 1.3. |
ech-config-list | تنظیم نشده | ECHConfigList با Base64 استاندارد برای ECH واقعی الزامی. |
verbose | false | جزئیات بیشتری از state مربوط به TLS را log میکند. |
با verify: true، فایل CA داخلی utils/cacert.h در context مربوط به BoringSSL بارگذاری میشود. اگر verify را false
بگذارید، handshake همچنان واقعی است اما chain certificate برای این instance از tunnel بررسی نمیشود.
مقادیر پیشفرض اختیاری فقط وقتی اعمال میشوند که key مربوطه وجود نداشته باشد. اگر alpns وجود داشته باشد، باید arrayای
از stringها باشد. طول هر نام باید بین ۱ تا ۲۵۵ byte باشد، نام تکراری پذیرفته نمیشود و طول فهرست encodeشده نباید از
۶۵٬۵۳۳ byte بیشتر شود. اگر verify، x25519mlkem768 یا verbose وجود داشته باشد، مقدار آن باید boolean واقعی JSON
باشد. نوع اشتباه باعث خطای startup میشود و بهمعنای انتخاب مقدار پیشفرض نیست.
بهتر است x25519mlkem768 را فعال نگه دارید. غیرفعال کردن آن ClientHello را کوچکتر میکند، اما شباهت handshake به نسخههای
فعلی Chrome را هم کاهش میدهد.
ECH پیکربندیشده
تنظیم ech-config-list یک string غیرخالی با Base64 استاندارد میپذیرد که ECHConfigList سریالشده مقصد را در خود دارد.
این پیکربندی در طول عمر node ثابت میماند. Base64 نامعتبر، ECHConfigList نامعتبر یا فهرستی که هیچ پیکربندی
پشتیبانیشدهای ندارد، در startup رد میشود. ech-config-list و ech-sni-trick را نمیتوان همزمان استفاده کرد.
اتصالهای عادی مقدار settings.sni را در ClientHelloInner رمزگذاری میکنند. ClientHelloOuter قابل مشاهده از
public_name موجود در ECHConfig انتخابشده استفاده میکند؛ نیازی به تنظیم SNI دوم نیست. اگر این گزینه وجود نداشته
باشد، رفتار فعلی ECH GREASE بدون تغییر میماند.
برای موفقیت اتصال، ECH پیکربندیشده باید پذیرفته شود. رد شدن ECH اتصال را بدون ارسال داده application، تلاش مجدد transport یا بازگشت به SNI آشکار یا GREASE میبندد. TlsClient پیکربندی را از رکوردهای HTTPS در DNS پیدا یا تازهسازی نمیکند و پیکربندیهای retry اعلامشده توسط server را اعمال نمیکند.
بررسی عادی certificate همچنان برای SNI داخلی پیکربندیشده انجام میشود. verify: false انتخاب صریح برای غیرفعال کردن
احراز هویت server باقی میماند؛ این گزینه رد شدن ECH را مجاز نمیکند و مسیر retry احرازشدهای ایجاد نمیکند. 0-RTT
غیرفعال میماند. resumption در TLS 1.3 میتواند از cache موجود استفاده کند و پیشنهاد PSK داخل ClientHelloInner
رمزگذاری میشود؛ sessionهای TLS 1.2 همراه ECH واقعی پیشنهاد نمیشوند.
API به نام generateTlsHello:<sni> همچنان ClientHello تازه با GREASE برای hostname دلخواه میسازد. این API گزینه
ech-config-list مربوط به مقصد ثابت node را اعمال نمیکند و sessionهای cacheشده را مصرف نمیکند. preflight هنگام
startup اندازه پرواز واقعی ClientHelloOuter پیکربندیشده را میسنجد تا محدودیت اندازه، ECH واقعی را هم پوشش دهد.
تقسیم جریان TLS
تنظیم اختیاری fragment یک StreamFragmenter خصوصی بلافاصله پس از TlsClient قرار میدهد:
previous -> TlsClient -> internal StreamFragmenter -> configured next
"fragment": {
"mode": "counter",
"count": 1,
"tls-hello-fragment": true,
"tls-hello-timeout-ms": 1000,
"cuts": [[250, 2, 100], [300, 5, 100]]
}
این object همان تنظیمات و اعتبارسنجی سختگیرانه StreamFragmenter را دارد: mode،
count یا duration-ms، آرایه cuts و گزینههای bypass_chance، wait-for-est،
tls-hello-fragment و tls-hello-timeout-ms. برای نداشتن helper، کلید fragment
را حذف کنید. مقدار حاضر باید object معتبر باشد؛ null، boolean، object خالی و
کلیدهای تکراری رد میشوند.
وقتی tls-hello-fragment وجود ندارد یا false است، offsetهای cuts بر اساس
بایتهای همان payload خروجی محاسبه میشوند. همه بایتهای TLS، از جمله headerهای
record، بدون تغییر میمانند. پرواز اولیه TLS در یک payload به helper میرسد؛
بنابراین count: 1 همان پرواز اول را در این حالت انتخاب میکند.
با tls-hello-fragment: true، offsetها بایتهای پیام handshake مربوط به ClientHello
اولیه را میشمارند: header چهار-byteی handshake شامل میشود، ولی headerهای
پنج-byteی TLS record شامل نمیشوند. برشهای انتخابشده recordهای handshake معتبر
و کوچکتر میسازند، در حالی که تمام بایتهای پیام، مرزهای recordهای اصلی و نسخه
آنها حفظ میشود. headerهای record ممکن است تغییر کنند؛ recordهای رمزگذاریشده
و داده downstream بدون تغییر میمانند. حداکثر اندازه پیام ۶۴ KiB است.
ورودی غیر TLS، قالب نامعتبر یا پشتیبانینشده، پیام بزرگتر، bypass یا پایان مهلت
tls-hello-timeout-ms باعث عبور بایتهای اصلی بدون تغییر و پایان دائمی تشخیص در
آن line میشود. مهلت از نخستین ورودی غیرخالی واجد شرایط آغاز میشود و زمان انتظار
برای Est را هم حساب میکند. Finish ورودی ناقص را دور میریزد و آن را
بازپخش نمیکند. این گزینه محل SNI را خودکار پیدا نمیکند و profile مستقل برای
برش TLS و TCP ندارد.
پیشفرض wait-for-est برابر true است: خروجی تا عبور transport Est منتظر میماند
و سپس تأخیر برشها آغاز میشود. مهلت گردآوری TLS میتواند پیش از Est یا هنگام
Pause فعال باشد؛ پس از کامل شدن بازنویسی دیگر اعمال نمیشود. Pause و Resume،
حتی اگر تکراری باشند، شفاف عبور میکنند و خروجی آماده منتظر Resume نمیماند. بازه timed بهطور
پیشفرض از نخستین Est آغاز میشود. مقدار wait-for-est: false زمانبندی پیش از
Est را مجاز میکند. buffering بعدی ممکن است writeها را ادغام کند؛ هیچیک از دو
حالت، مرز TCP packet را تضمین نمیکند.
helper همان line را قرض میگیرد و FIFO مستقل آن ۸ MiB برای بایتهای منطقی، ۸ MiB برای capacity charge و ۱٬۰۲۴ job ظرفیت دارد. reentry، Finish و shutdown از قواعد عادی StreamFragmenter پیروی میکنند. helper پس از کاهش داده نگهداشتهشده تا آستانه پایین، فشار ایجادشده توسط خودش را مستقل از Pause و Resume عبوری آزاد میکند. پس از پایان شکلدهی، باز شدن انتظار Est و تخلیه دادههای نگهداشتهشده، state مربوط به line را پاک میکند و برای آن line بهصورت دائمی bypass میشود. TlsClient برای پردازش TLS فعال میماند. این بودجه از ذخیره plaintext و ciphertext shaping جداست. TlsClient همچنان splice را برای کل chain مسدود میکند.
میتوانید tls13-record-shaping را همراه fragment فعال کنید: ابتدا padding و
delay مربوط به record و سپس StreamFragmenter اجرا میشوند. API تولید ClientHello
خام همچنان buffer معمولی را بدون ورود به این helper مربوط به line برمیگرداند.
شکلدهی آزمایشی TLS 1.3 record
تنظیم tls13-record-shaping بهصورت اختیاری نخستین application recordهای TLS 1.3 را که همین node میفرستد pad میکند و
به تأخیر میاندازد. اگر این تنظیم وجود نداشته باشد، قابلیت غیرفعال است. این قابلیت هرگز ciphertext دریافتی، handshake
recordها، alertها، application recordهای خالی، بایتهای fallback یا recordهای TLS 1.2 را تغییر نمیدهد. اگر باید هر دو
جهت ارسال شکل داده شوند، TlsClient محلی و TlsServer راه دور را جداگانه تنظیم کنید.
فرم سفارشی:
"tls13-record-shaping": {
"scope": {"first-application-records": 8},
"outcomes": [
{
"probability": 50,
"padding-bytes": [100, 200],
"delay": {"probability": 75, "ms": [10, 20]}
},
{"probability": 10, "padding-bytes": [400, 600]}
]
}
padding-bytes و delay.ms یک integer یا بازه integer شامل دو سر [minimum, maximum] میپذیرند. تعداد outcomeها باید
بین ۱ تا ۱۶ باشد؛ first-application-records بین ۱ تا ۱۰۲۴، padding بین ۱ تا ۴۰۹۶ byte و delay بین ۰ تا ۱۰۰۰
millisecond است. probabilityهای outcome بهصورت تجمعی و mutually exclusive تفسیر میشوند و مجموع آنها نباید از ۱۰۰
بیشتر باشد. هر بار حداکثر یک outcome انتخاب میشود و درصد استفادهنشده record را بدون تغییر میگذارد. probability مربوط
به delay فقط پس از انتخاب همان outcome بررسی میشود. key ناشناخته و مقدار غیرinteger یا خارج از محدوده باعث خطای
startup میشود. هر record واجد شرایط یک جایگاه از scope مصرف میکند، حتی اگر هیچ outcomeای انتخاب نشود.
در حال حاضر فقط فرم سفارشی شامل scope و outcomes پذیرفته میشود. تا زمانی که اندازهگیریهای representative برای
capture، overhead، موفقیت connection و classifier انتشار یک preset versionشده را توجیه نکنند، key به نام profile
رد میشود.
padding از بایتهای صفر استاندارد TLS 1.3 در TLSInnerPlaintext استفاده میکند و بهاندازه انتخابشده به ciphertext
میافزاید، البته تا ظرفیت قانونی باقیمانده در TLS record. Delay پس از رمزنگاری اعمال میشود و latency انتخابشده را
اضافه میکند. ترتیب recordها روی wire با رابطه
release_at = max(now + delay, previous_release_at) حفظ میشود. ciphertext صفشده برای هر line حداکثر ۸ MiB است؛ در
۶ MiB یک Pause محلی برای producer و در ۳ MiB یک Resume محلی ارسال میشود. Pause و Resume دریافتی، حتی اگر تکراری
باشند، شفاف عبور میکنند. خروجی ciphertext در زمان مقرر و با ترتیب FIFO بدون انتظار برای Resume تخلیه میشود؛
Pause تایمر تخلیه را لغو نمیکند. اگر سمت cleartext پایان یابد، timer لغو میشود و ciphertext صفشده client حتی هنگام Pause بهصورت synchronous
و با ترتیب FIFO آزاد میشود. state محلی و upstream Finish پیش از بازگشت callback تکمیل میشوند.
پس از مصرف کامل scope تنظیمشده برای application recordها، ابتدا صف فعلی shaping بهطور کامل تخلیه میشود تا هیچ TLS record بعدی از ciphertext تأخیردار سبقت نگیرد. تا زمانی که این مانع ترتیبدهی فعال است، درخواست توقف محلی shaping حتی پایینتر از low watermark باقی میماند؛ Resume عبوری همچنان مشورتی است. وقتی صف خالی باشد و write-BIO در یک مرز کامل قرار داشته باشد، timer و صف shaping مربوط به line آزاد میشوند؛ خروجیهای بعدی TLS بدون parse کردن record یا نگهداری metadata شکلدهی، حتی هنگام Pause مستقیم forward میشوند. بنابراین محدودیت ۸ MiB و watermarkهای ۶/۳ MiB فقط پنجره محدود و فعال shaping را مقید میکنند. رمزنگاری و رمزگشایی TLS ادامه دارد؛ پایان shaping به معنی bypass دائمی tunnel نیست.
در طول این تخلیه نهایی همگام، downstream payload، Est، Pause و Resume دور ریخته میشوند؛ هیچیک از این callbackها به سمت owner پایانیافته cleartext فرستاده نمیشود. پایان سمت wire خروجی صفشده را لغو و دور میریزد و فقط به سمت cleartext منتقل میشود. اگر ساخت timer شکست بخورد، recordها فوراً و بهترتیب تخلیه میشوند.
شکلدهی همراه با delay برای مصرفکنندگان داخلی handshake takeover مانند RealityClient رد میشود، زیرا takeover خام
نمیتواند TLS record تأخیردار و معلق را به ارث ببرد. شکلدهی فقط با padding، وقتی boundary مربوط به takeover خالی است،
همچنان سازگار میماند.
record shaping میتواند اندازه و زمانبندی recordهای ابتدایی را کمتر قابل پیشبینی کند، اما نمیتواند یک round trip
داخلی handshake را حذف کند، burst را کوچکتر کند یا همه ویژگیهای جهت و زمانبندی را پنهان کند. وقتی multiplexing برای
deployment مناسب است، آن را همراه MuxClient/MuxServer استفاده کنید.
ترتیب ALPN و مسئولیت هماهنگی protocol برنامه
تنظیم alpns فهرست ترتیبی پیشنهادی ALPN را در TLS ClientHello مشخص میکند. برای مثال:
"alpns": ["http/1.1"]
ترتیب array عیناً حفظ میشود. اگر alpns وجود نداشته باشد، پیشفرض Chrome-like زیر استفاده میشود:
"alpns": ["h2", "http/1.1"]
قرار دادن array خالی، ALPN را غیرفعال میکند. key مفرد alpn پشتیبانی نمیشود.
ترتیب تنظیمشده پیشنهاد client را بیان میکند، اما TLS server یکی از protocolهای پیشنهادی را انتخاب میکند. TlsClient
نتیجه این انتخاب را به node قبلی WaterWall گزارش نمیدهد، mode مربوط به HTTP آن node را عوض نمیکند و بررسی نمیکند که
protocol برنامه cleartext با ALPN انتخابشده یکسان باشد. مسئولیت هماهنگ کردن این تنظیمات با کاربر است.
این موضوع در chainای مثل HttpClient -> TlsClient اهمیت ویژه دارد:
- برای WebSocket Upgrade روی HTTP/1.1،
HttpClientرا روی HTTP/1.1 قرار دهید و درTlsClientفقط"alpns": ["http/1.1"]را پیشنهاد کنید. - برای WebSocket روی HTTP/2،
HttpClientرا روی HTTP/2 قرار دهید تا از مسیر CONNECT در HTTP/2 استفاده کند و درTlsClientفقط"alpns": ["h2"]را پیشنهاد کنید. - پیشنهاد همزمان
h2وhttp/1.1معتبر و رایج است و ClientHello را به پیشفرض Chrome نزدیکتر نگه میدارد. وقتی این پیشنهاد را همراه با یکHttpClientبا version ثابت استفاده میکنید، کاربر باید بداند target server کدام protocol را انتخاب میکند وHttpClientرا برای همان version تنظیم کند. اگر انتخاب target نامشخص یا متغیر است، فقط protocol هماهنگ باHttpClientرا پیشنهاد کنید.
تا ژوئیه ۲۰۲۶، برای HTTPS معمولی از طریق hostname پروکسیشده Cloudflare که HTTP/2 در آن
فعال است، وقتی h2 پیشنهاد شود Cloudflare آن را
انتخاب میکند. بنابراین میتوان پیشنهاد Chrome-like برابر "alpns": ["h2", "http/1.1"] را حفظ کرد و HttpClient
قبلی را روی HTTP/2 قرار داد.
این موضوع برای WebSocket صدق نمیکند. رفتار فعلی مستندشده Cloudflare برای WebSocketهای
proxyشده از extended CONNECT مربوط به WebSocket روی HTTP/2
(RFC 8441) پشتیبانی نمیکند. برای WebSocket پشت Cloudflare، با قرار دادن فقط "alpns": ["http/1.1"] در TlsClient و
تنظیم HttpClient روی HTTP/1.1، مسیر WebSocket Upgrade در HTTP/1.1 را اجبار کنید. چون تنظیمات و پشتیبانی provider ممکن
است تغییر کند، پیش از تکیه بر هرکدام از این رفتارها قابلیتهای فعلی Cloudflare را دوباره بررسی کنید.
وقتی انتخاب target server شناختهشده و ثابت است، پیشنهاد چند ALPN مناسب است. این کار application protocol را بهصورت
خودکار تشخیص نمیدهد؛ node قبلی باید از قبل برای protocolای تنظیم شده باشد که server انتخاب خواهد کرد. تغییر alpns
همچنین بخش قابلتشخیصی از ClientHello را تغییر میدهد و ممکن است آن را از ظاهر پیشفرض Chrome-like دور کند.
پس از کامل شدن handshake، TlsClient نسخه TLS، cipher و ALPN مذاکرهشده را در debug log ثبت میکند. protocol انتخابشده
بهشکل alpn="..." و نبودن ALPN مذاکرهشده بهشکل alpn=<none> نمایش داده میشود. این log فقط برای عیبیابی است و مقدار
انتخابشده همچنان به tunnel مربوط به application قبلی منتقل نمیشود.
TLS context با این رفتار ساخته میشود:
| رفتار | مقدار فعلی |
|---|---|
| حداقل TLS version | TLS 1.2 |
| حداکثر TLS version | TLS 1.3 |
| session cache | حداکثر دو session برای هر node و worker |
| timeout در TLS 1.2 | 3600 seconds |
| timeout در TLS 1.3 | طول عمر session در BoringSSL با سقف طول عمر ticket سمت server |
| GREASE | فعال |
| Extension permutation | فعال |
| Certificate compression | Brotli decompression support |
| Signed certificate timestamps | فعال |
| OCSP stapling request | فعال per line |
بدون در نظر گرفتن ورودیهای تصادفی GREASE، گروههای پشتیبانیشده در حالت پیشفرض عبارتاند از:
X25519MLKEM768:X25519:P-256:P-384
در پیشنهاد اولیه TLS 1.3، key_share علاوه بر GREASE، ابتدا X25519MLKEM768 و سپس X25519 را دارد.
ترتیب پیشفرض گروههای پشتیبانیشده با مرجع Chrome 154 مطابقت دارد؛ این موضوع به معنی تطابق کامل handshake با Chrome نیست.
غیرفعالسازی صریح با x25519mlkem768: false همچنان پشتیبانی میشود و گروه hybrid را حذف میکند:
X25519:P-256:P-384
در این حالت، تنها key share اولیه غیر از GREASE، X25519 است.
فهرست signature_algorithms با یک مقدار تصادفی GREASE آغاز میشود و سپس این الگوریتمها را با ترتیب ثبتشده از
Chrome 154 پیشنهاد میکند:
ML-DSA-44
ML-DSA-65
ML-DSA-87
ecdsa_secp256r1_sha256
rsa_pss_rsae_sha256
rsa_pkcs1_sha256
ecdsa_secp384r1_sha384
rsa_pss_rsae_sha384
rsa_pkcs1_sha384
rsa_pss_rsae_sha512
rsa_pkcs1_sha512
قرار دادن x25519mlkem768 روی false فقط توافق کلید را تغییر میدهد؛ الگوریتمهای امضای ML-DSA همچنان پیشنهاد میشوند.
اتصالهای عادی TLS و مولد raw ClientHello از تنظیم یکسان context برای این فهرست استفاده میکنند. تطابق این فیلدها به معنی
تطابق کامل fingerprint با Chrome نیست. نسخه BoringSSL همراه پروژه تغییرات محلی مربوط به شکل ClientHello و ترتیب cipherها
را نیز حفظ میکند.
ALPS
TlsClient فقط برای h2 و تنها وقتی این protocol در alpns باشد، ALPS را ثبت میکند. application setting سمت client
خالی است و با ثبت HTTP/2 در Chrome مطابقت دارد. برای http/1.1 و protocolهای سفارشی ALPN هیچ ALPSی ثبت نمیشود؛
اگر h2 وجود نداشته باشد، ClientHello نیز extension مربوط به ALPS ندارد. codepoint این extension برابر با
17613 (0x44cd) است.
بایتهای 02 68 32 نام protocol یعنی h2 را با پیشوند طول نمایش میدهند و application setting نیستند.
تابع SSL_add_application_settings نام protocol و application setting آن را جداگانه دریافت میکند و BoringSSL فهرست
protocolهای ALPS در ClientHello را بهشکل 00 03 02 68 32 میسازد. اگر server برای h2 بر سر ALPS توافق کند، client
در بخش رمزنگاریشده handshake یک application setting خالی میفرستد. این مقدار خالی را با encoding نام protocol یا
یک HTTP/2 SETTINGS frame جایگزین نکنید.
TlsClient، application setting دریافتی از server را parse یا اعمال نمیکند و آن را به node قبلی انتقال نمیدهد.
این تنظیم ALPS شکل تبادل TLS قابلمشاهده برای ناظر غیرفعال را تعیین میکند؛ رفتار HTTP/2 مرورگر Chrome در پاسخ به
تنظیمات server را پیادهسازی نمیکند و اتصال را از دید server غیرقابلتشخیص از Chrome نمیسازد.
شناسههای Trust Anchor
TlsClient تعداد ۲۸ شناسه Trust Anchor ثبتشده از Chrome 154.0.8037.97 و Chrome Root Store 39 را پیشنهاد میکند.
BoringSSL این شناسهها را در extension با کد 0xca34 قرار میدهد. این فهرست شامل ۱۳ ریشه است که از قبل در CA bundle
داخلی WaterWall وجود دارند و ۱۵ گواهی میانی. مسیر source و fingerprint هر گواهی در
tests/unittests/fixtures/tlsclient_chrome/trust_anchors.json ثبت شده است.
وقتی verification فعال باشد، هر SSL context یک cache از این ۱۵ گواهی میانی دارد. اگر server اعلام کند که Trust Anchor ID
مطابقت دارد، verifier گواهیهای cache را بهعنوان نامزدهای بدون اعتماد مستقل برای ساخت زنجیره اضافه میکند تا server
بتواند آنها را از پیام گواهی حذف کند. این گواهیها به ریشههای مورد اعتماد تبدیل نمیشوند. بررسی عادی امضا، زمان اعتبار،
hostname، کاربرد گواهی، طول مسیر و محدودیت نام همچنان انجام میشود و زنجیره باید به یکی از ریشههای مورد اعتماد موجود برسد.
با verify: false نیز شناسهها پیشنهاد میشوند؛ این تنظیم همچنان احراز هویت server را غیرفعال میکند.
فهرست شناسهها و cache گواهیهای میانی یک profile ثابت و مرتبط با هم هستند. هنگام بهروزرسانی، هر دو را با mappingهای source و ریشههای پشتیبانیشده توسط verifier تطبیق دهید.
مقایسه مرجع ClientHello
تست waterwall.tlsclient_alpn_unit کل ClientHello اولیه را با capture ذخیرهشده از Google Chrome 154.0.8037.97
روی Linux x86-64 با پشتیبانی سختافزاری AES مقایسه میکند. حالت مرجع یک اتصال تازه TCP با SNI برابر
tls.integration.test، ALPN و گروههای پیشفرض و ECH GREASE عادی، بدون override سفارشی یا fragmentation است.
این مقایسه framing رکورد و handshake، نسخهها، ترتیب cipherها، compression، مجموعه کامل extensionها و payload آنها، از جمله Trust Anchor IDها را پوشش میدهد. بایتهای تصادفی، مقدارهای GREASE و ترتیب extensionهای میانی نرمالسازی میشوند؛ جایگاه و payload مربوط به extension خالی GREASE در ابتدا و GREASE یکبایتی در انتها حفظ و بررسی میشود. طول key shareها و framing و اندازههای مجاز payload در ECH GREASE نیز بررسی میشوند. محدوده این بررسی ClientHello اولیه همین profile است و تطابق session resumption، ECH واقعی، اندازه یا زمانبندی رکوردهای بعدی و رفتار application با Chrome را اثبات نمیکند.
Session Resumption
هر node از نوع TlsClient برای هر worker یک cache جداگانه با ظرفیت دو session در SSL context نگه میدارد. SNI، ALPN
و سیاست verification در این context ثابت هستند. اتصالهای runtime پیش از استفاده از cache، SNI و حالت verification
را بررسی میکنند. preflight سازنده، تولید raw ClientHello و تولید ECH-inner همیشه تازه میمانند و هیچ sessionی را از
cache مصرف یا به آن اضافه نمیکنند. مالک cache همان SSL context است و cache همراه آن آزاد میشود.
سقف اندازه ticket بر اساس بزرگترین ClientHello تازه در preflight تعیین میشود و فضای framing مربوط به PSK نیز کنار گذاشته میشود؛ با ECH واقعی، حداکثر ۳۱ byte برای padding داخلی هم ذخیره میشود. ticketهای بیش از این سقف حذف میشوند تا ClientHelloهای بعدی حتی با تنظیمات سفارشی بزرگ ALPN یا ECH از حد اندازه عبور نکنند. اگر session قابل استفادهای باقی نماند، اتصال بعدی با handshake تازه آغاز میشود.
ticketهای TLS 1.3 یکبارمصرف هستند: انتخاب ticket برای یک اتصال آن را از cache حذف میکند، حتی اگر اتصال پیش از ارسال شکست بخورد یا server پیشنهاد resumption را نپذیرد. sessionهای ECDHE در TLS 1.2 قابل استفاده مجدد هستند. انقضا بر اساس زمان ایجاد و timeout ذخیرهشده در هر session بررسی میشود. timeout در TLS 1.2 یک ساعت است؛ TLS 1.3 از طول عمر session در BoringSSL و طول عمر ticket اعلامشده توسط server پیروی میکند. server میتواند session پیشنهادی را رد کند و handshake کامل انجام دهد.
با verify: true، گواهیهای peer ذخیرهشده در cache پیش از پیشنهاد session دوباره با ریشههای فعلی، hostname و
پارامترهای verification همان اتصال بررسی میشوند. گواهیهای میانی TAI هنگام بازسازی زنجیره حذفشده همچنان نامزدهای
بدون اعتماد مستقل هستند. هویت cacheشده نامعتبر یا منقضی حذف میشود؛ اگر session قابل استفادهای باقی نماند، اتصال
handshake کامل انجام میدهد. verify: false همچنان انتخاب صریح برای غیرفعال کردن احراز هویت server است.
این cache بر اساس node و worker جدا میشود. تفکیک مرورگر Chrome بر اساس port، proxy و حریم خصوصی در محدوده این cache نیست. sessionهای TLS 1.2 با static RSA ذخیره نمیشوند، زیرا Chrome آنها را به IP سمت peer وابسته میکند و این TLS transform عمومی نمیتواند آن endpoint را از metadata مسیریابی قابل تغییر به دست آورد.
ticketها هنگام پردازش ورودی TLS جمعآوری میشوند؛ این شامل ticketهای دریافتشده در مرحله drain مربوط به takeover در Reality با TLS 1.3 نیز هست. takeover منتظر ticket نمیماند، بنابراین resumption فقط در صورت وجود session قابل استفاده انجام میشود. 0-RTT غیرفعال میماند و داده application تا تکمیل handshake منتظر میماند.
کتابخانه TLS همراه پروژه
نسخه BoringSSL همراه پروژه commit با شناسه ac39ea6853833c1f18fd23614091d11855e71752 است که Chromium
154.0.8037.97 انتخاب کرده است. این انتخاب به معنی تطابق کامل fingerprint یا رفتار application با Chrome نیست.
patchهای محلی ترتیب ثابت cipherها، overrideهای ECH GREASE، padding رکورد TLS 1.3 و خروجی مستقیم رکوردها به buffer را
حفظ میکنند.
WaterWall کتابخانههای static مربوط به BoringSSL را با پیشوند symbol برابر WW_BSSL و جدا از OpenSSL معمولی میسازد.
headerهای prefix موجود در source بالادستی، symbolهای عمومی، داخلی و assembly هر معماری را پوشش میدهند؛ یک patch محلی
APIهای سفارشی SSL را به header عمومی اضافه میکند. build عادی برای تولید prefix به Go یا Python نیاز ندارد.
هنگام بهروزرسانی dependency، patch مربوط به record padding را پیش از direct output و patch مربوط به prefix عمومی را
پس از همه patchهای API اعمال کنید و پوشش regression مربوط به TLS/Reality و مالکیت buffer را حفظ کنید.
رفتار در runtime
در upstream Init، TlsClient:
- state مربوط به BoringSSL را برای line مقداردهی اولیه میکند
- memory BIOها را میسازد
- SSL object را در client mode قرار میدهد
- SNI تنظیمشده را اعمال میکند
- upstream
Initرا به node بعدی میفرستد SSL_connect()را اجرا میکند تا اولین ClientHello flight تولید شود- بایتهای handshake تولیدشده را در جهت upstream میفرستد
به همین دلیل قرار دادن این node پیش از TcpConnector طبیعی است: connector میتواند ClientHello را تا برقرار شدن اتصال
واقعی socket در buffer نگه دارد.
مفهوم Establishment
در WaterWall، Est یعنی transport زیرین برقرار شده است؛ نه اینکه TLS handshake هم پایان یافته باشد.
در هر دو حالت عادی و takeover، رویداد transport Est بلافاصله و فقط یک بار به node قبلی میرسد.
payload پس از Init node مجاور، حتی پیش از Est یا پایان TLS، معتبر است. plaintext منتظر TLS یا خروجی قدیمیتر
حداکثر ۲ MiB و ۱٬۰۲۴ buffer دارد؛ head فعال نیز در این حساب منظور میشود. برابری با سقف پذیرفته میشود.
شکست پذیرش تراکنشی صف، line قرضگرفتهشده را از طریق owner میبندد و bufferها را آزاد میکند.
پایان handshake صف را با ترتیب FIFO و بدون انتظار برای Resume تخلیه میکند؛ مانع واقعی ترتیب یا shaping محلی همچنان
اعمال میشود. ورودی جدید از head قدیمی سبقت نمیگیرد و محدودیت ciphertext shaping جداست.
نخستین plaintext نگهداشتهشده پیش از ارسال Pause محلی به source در صف ثبت میشود. انتظار protocol و ترتیب و فشار
ciphertext shaping یک latch مشترک دارند که پس از رفع علت محلی آزاد میشود. Pause و Resume دریافتی مستقل از آن و
حتی بهصورت تکراری منتقل میشوند. این فشار محلی دریافت recordهای handshake از شبکه را متوقف نمیکند.
مالک داخلی با tlsclientTunnelEnableHandshakeTakeover(t, owner, callback) یک callback اختصاصی پایان handshake
ثبت میکند. ورودی takeover بهصورت record کامل و یکییکی به BoringSSL داده میشود و پردازش دقیقاً در مرز
record تکمیلکننده handshake متوقف میشود. state تکمیل پیش از callback ثبت میشود؛ reference همان line
تا بازگشت نگه داشته شده و سپس زنده بودن line و state مربوط به TLS دوباره بررسی میشود.
پایان handshake و فعال شدن Reality هیچ Est دیگری تولید نمیکنند.
جریان payload
cleartext upstream:
- پیش از کامل شدن handshake در صف میماند
- بعد از handshake به
SSL_write()داده میشود - TLS recordهای تولیدشده از write BIO خوانده و به node بعدی ارسال میشوند
TLS recordهای downstream:
- بایتهای رمزنگاریشده داخل read BIO نوشته میشوند
SSL_connect()handshake را جلو میبرد- بایتهای protocol تولیدشده در جهت upstream ارسال میشوند
- بایتهای application که
SSL_read()رمزگشایی کرده است به node قبلی میروند
در صورت خطا در بررسی certificate، نتیجه و دلیل گزارششده توسط BoringSSL در log ثبت میشود.
API
TlsClient یک API کوچک برای ساخت raw ClientHello دارد:
generateTlsHello:<sni>
ClientHello تولیدشده از تنظیمات alpns و x25519mlkem768 همین tunnel instance پیروی میکند. طول SNI ورودی API باید
بین ۱ تا ۲۵۵ byte باشد. این API همیشه تازه شروع میکند، sessionهای cacheشده را مصرف نمیکند و رفتار GREASE را حفظ
میکند؛ گزینه ech-config-list مربوط به مقصد ثابت node در آن اعمال نمیشود.
API handshake Takeover
کد داخلی WaterWall میتواند از TlsClient در handshake-takeover mode استفاده کند:
| API | کاربرد |
|---|---|
tlsclientTunnelEnableHandshakeTakeover() | owner و callback پایان handshake را ثبت و takeover را فعال میکند. |
tlsclientTunnelIsHandshakeCompleted() | بررسی میکند که آیا TLS handshake یک line کامل شده است. |
tlsclientTunnelGetHandshakeBinding() | نسخه، cipher، randomها و sequenceهای TLS 1.2 را پیش از آزاد شدن BoringSSL ثبت میکند. client random همان random ارسالشده در ClientHelloOuter است، حتی وقتی ECH پذیرفته شده باشد. |
tlsclientTunnelDeinitAfterHandshake() | فقط برای takeover فوری TLS 1.2 است و TLS 1.3 را رد میکند. |
tlsclientTunnelBeginTakeoverDrain() | dispatch خارجی post-handshake در TLS 1.3 را آغاز میکند و بایتهای خام جمعشده را با حفظ SSL/BIO برمیگرداند. |
tlsclientTunnelConsumePostHandshakeRecord() | دقیقاً یک record کامل TLS 1.3 را مصرف میکند، plaintext مربوط به cover را دور میریزد و protocol output تولیدشده را upstream میفرستد. |
tlsclientTunnelCompleteTakeover() | پس از احراز boundary توسط owner، state نگهداشتهشده TLS را آزاد و raw pass-through را فعال میکند. |
این API مرحلهای اجازه میدهد NewSessionTicket و KeyUpdate قانونی تا تکمیل handoff احرازشده، TLS واقعی باقی بمانند.
پس از record تکمیلکننده handshake، هم read BIO و هم buffer داخلی TLS در BoringSSL باید خالی باشند؛ در غیر این صورت
takeover شکست میخورد. controlهای خام owner و protocol output تولیدشده توسط BoringSSL از یک مسیر upstream و با همان
ترتیب callback عبور میکنند.
این API برای integration داخلی WaterWall است و setting JSON ندارد. در configهای معمول، TlsClient در تمام طول عمر line
بهعنوان wrapper TLS باقی میماند.
رفتار Finish
TlsClient پیش از فرستادن Finish، state مربوط به TLS را روی line از بین میبرد.
در پایان عادی هر جهت، Finish در همان جهت ادامه پیدا میکند. خطاهای fatal هنگام read/write در BoringSSL باعث پاک شدن
state محلی و بسته شدن هر دو جهت میشوند. این node مالک lineهای عادی بیرونی نیست و برای آنها lineDestroy() را فراخوانی
نمیکند.
TlsClient برای بستن اتصال از سیاست direct transport close استفاده میکند:
این انتخاب در بستن عادی برای تقلید از رفتار Chrome موردنظر این tunnel است: وقتی در این وضعیت کاربر یا برنامه اتصال را
میبندد، Chrome بدون فرستادن TLS close_notify پیش از آن، transport را مستقیماً میبندد. این توضیح به این معنا نیست که
Chrome در همهٔ مسیرهای TLS shutdown هرگز close_notify نمیفرستد.
- در upstream
Finishعادی، state مربوط به TLS آزاد میشود وFinishفقط به node بعدی میرود - در downstream
Finishخام از سمت transport، state مربوط به TLS آزاد میشود وFinishفقط به node قبلی میرود - مسیر بستن عادی
SSL_shutdown()را صدا نمیزند و TLSclose_notifyتولید نمیکند - اگر peer یک
close_notifyمعتبر بفرستد، مصرف میشود، پاسخی باclose_notifyداده نمیشود، و هر دو جهت فوراً بسته میشوند - خطاهای fatal مربوط به TLS، certificate، record authentication،
SSL_write()یا BIO هر جهت initialize شده را فوراً میبندند - این tunnel منتظر پاسخ shutdown از peer نمیماند، TLS half-close انجام نمیدهد، و timer مخصوص shutdown ندارد
برای این سیاست تنظیم JSON وجود ندارد. peerهایی که shutdown کامل TLS را لازم میدانند ممکن است این EOF مستقیم را بهعنوان TLS shutdown ناقص گزارش کنند.
در handshake-takeover mode، tlsclientTunnelDeinitAfterHandshake() state مربوط به TLS را بدون بستن line آزاد میکند و line
بعد از آن بهصورت raw passthrough ادامه میدهد.
padding
TlsClient مستقیماً چیزی به ابتدای payloadهای WaterWall اضافه نمیکند، بنابراین مقدار زیر را اعلام میکند:
required_padding_left = 0
ساخت TLS recordها از طریق memory BIOهای BoringSSL انجام میشود.
قابلیتهای پشتیبانینشده
این پیادهسازی از موارد زیر پشتیبانی نمیکند:
- TLS termination سمت server
- انتخاب dynamic certificate
- TLS روی packet lineها
- wrapping transport غیر TLS مثل WebSocket یا HTTP بهتنهایی
برای این layerها از nodeهای جداگانه WaterWall استفاده کنید.
اشتباههای رایج
- انتظار نداشته باشید
TlsClientخودش به remote server وصل شود؛ بعد از آن connector بگذارید. - فرض نکنید WaterWall
Estیعنی TLS handshake کامل شده است. - از key مفرد
alpnاستفاده نکنید؛ فهرست ترتیبی را با arrayِalpnsتنظیم کنید. - هنگام پیشنهاد چند application protocol، انتخاب ثابت target server را بدانید و node قبلی را برای همان protocol تنظیم
کنید.
TlsClientانتخاب ALPN انجامشده توسط server را به آن node گزارش نمیدهد. - برای اتصال عمومی،
verifyرا فقط زمانی غیرفعال کنید که عمداً نمیخواهید certificate را اعتبارسنجی کنید. - اگر هدف شما handshake نزدیک به Chrome فعلی است،
x25519mlkem768را غیرفعال نکنید.
پیشرفته: ترفند ECH SNI
این قابلیت بخشی از config عادی TLS یا مسئولیت معمول TlsClient نیست. فقط زمانی از آن استفاده کنید که deployment بهطور
عمدی ساخت TLS ClientHello را با دستکاری سازگار در سطح packet هماهنگ میکند.
تنظیم اختیاری ech-sni-trick با ech-config-list سازگار نیست. این گزینه یک string مربوط به hostname با طول ۱ تا ۲۵۵ byte
میپذیرد:
"ech-sni-trick": "example.net"
وقتی این گزینه تنظیم شود، TlsClient با hostname دادهشده یک ClientHello جعلی و دوم میسازد و بایتهای آن را بهعنوان
GREASE encrypted_client_hello payload داخل ClientHello واقعی و بیرونی قرار میدهد. SNI بیرونی و cleartext همچنان
settings.sni باقی میماند.
ClientHello جعلی از همان فهرست و ترتیب تنظیمشده alpns استفاده میکند، اما برای کوچکتر نگه داشتن payload،
x25519mlkem768 را غیرفعال میکند. این رفتار برای هماهنگی با مکانیزمهای packet-side مانند ترفند packet-splitting در
IpManipulator طراحی شده است تا بایتهایی که BoringSSL از ClientHello hash میکند با بایتهای قرارگرفته روی wire یکسان
بمانند.
مقدار این گزینه باید یک string در JSON با طول ۱ تا ۲۵۵ byte باشد؛ مقدار خارج از این محدوده یا هر نوع دیگر JSON باعث
خطای startup میشود. API تونل generateTlsHello:<sni> نیز در صورت تنظیم بودن، این گزینه را اعمال میکند.
متادیتای node
متادیتای برگرفته از کد منبع:
این node با kNodeFlagBlocksSplice استفاده از splice را برای کل chain صریحاً غیرفعال میکند. این پرچم حتی در صورت وجود همزمان kNodeFlagSupportsSplice اولویت دارد.
| ویژگی | مقدار |
|---|---|
| node flags | kNodeFlagBlocksSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 0 بایت |
قدردانی
قابلیتهای تقسیم داده در WaterWall از کارهای این توسعهدهندگان الهام گرفتهاند:
- GFW-knocker — برای کارهای عملی در زمینه تقسیم ترافیک TLS ClientHello به ارسالهای کوچکتر در جریان TCP با تأخیر میان آنها، که الهامبخش روش تقسیم جریان ما بوده است.
- patterniha — برای توضیح و بررسی تقسیم رکوردهای TLS و تفاوت آن با تقسیم TCP، که در طراحی قابلیت تقسیم رکوردهای TLS ClientHello ما نقش داشته است.
از هر دو توسعهدهنده برای بهاشتراکگذاری کارهایشان با جامعه قدردانی میکنیم.