پرش به مطلب اصلی

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:

fieldtypeتوضیح
namestringنام دلخواه node؛ باید داخل config یکتا باشد.
typestringباید دقیقاً "TlsClient" باشد.
settingsobjectتنظیمات TLS client.
nextstringnode stream-facing که TLS record‌های رمزنگاری‌شده را حمل می‌کند؛ در استفاده معمول لازم است.

فیلد اجباری داخل settings:

fieldtypeتوضیح
snistringمقدار server Name Indication در TLS با طول ۱ تا ۲۵۵ byte.

وقتی verify فعال باشد، همین sni برای certificate verification هم استفاده می‌شود.

تنظیمات اختیاری​

fielddefaultتوضیح
alpns["h2", "http/1.1"]فهرست ترتیبی protocol‌های پیشنهادی ALPN؛ ترتیب JSON عیناً در ClientHello حفظ می‌شود.
verifytrueبررسی certificate متعلق به peer را در BoringSSL فعال می‌کند.
x25519mlkem768trueگروه hybrid توافق کلید با نام X25519MLKEM768 را اعلام می‌کند؛ الگوریتم‌های امضا را تغییر نمی‌دهد.
fragmentتنظیم نشدهتنظیمات StreamFragmenter داخلی برای تقسیم و تأخیر بایت‌های TLS خروجی.
tls13-record-shapingتنظیم نشدهتنظیم آزمایشی padding و delay سمت فرستنده برای TLS 1.3.
ech-config-listتنظیم نشدهECHConfigList با Base64 استاندارد برای ECH واقعی الزامی.
verbosefalseجزئیات بیشتری از 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 versionTLS 1.2
حداکثر TLS versionTLS 1.3
session cacheحداکثر دو session برای هر node و worker
timeout در TLS 1.23600 seconds
timeout در TLS 1.3طول عمر session در BoringSSL با سقف طول عمر ticket سمت server
GREASEفعال
Extension permutationفعال
Certificate compressionBrotli 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:

  1. state مربوط به BoringSSL را برای line مقداردهی اولیه می‌کند
  2. memory BIO‌ها را می‌سازد
  3. SSL object را در client mode قرار می‌دهد
  4. SNI تنظیم‌شده را اعمال می‌کند
  5. upstream Init را به node بعدی می‌فرستد
  6. SSL_connect() را اجرا می‌کند تا اولین ClientHello flight تولید شود
  7. بایت‌های 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() را صدا نمی‌زند و TLS close_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 flagskNodeFlagBlocksSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left0 بایت

قدردانی​

قابلیت‌های تقسیم داده در WaterWall از کارهای این توسعه‌دهندگان الهام گرفته‌اند:

  • GFW-knocker — برای کارهای عملی در زمینه تقسیم ترافیک TLS ClientHello به ارسال‌های کوچک‌تر در جریان TCP با تأخیر میان آن‌ها، که الهام‌بخش روش تقسیم جریان ما بوده است.
  • patterniha — برای توضیح و بررسی تقسیم رکوردهای TLS و تفاوت آن با تقسیم TCP، که در طراحی قابلیت تقسیم رکوردهای TLS ClientHello ما نقش داشته است.

از هر دو توسعه‌دهنده برای به‌اشتراک‌گذاری کارهایشان با جامعه قدردانی می‌کنیم.