TlsClient
TlsClient لایه TLS سمت client در WaterWall است و از نسخه BoringSSL موجود در همین repository استفاده میکند. داده
cleartext را از نود قبلی میگیرد و پس از رمزنگاری بهشکل TLS record به نود بعدی میفرستد. TLS recordهای برگشتی نیز در
جهت downstream رمزگشایی میشوند و بهصورت cleartext به نود قبلی برمیگردند.
این نود یک 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 باز نمیکند. معمولاً بعد از آن نودی مثل TcpConnector قرار میگیرد تا byteهای رمزنگاریشده 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 | نام دلخواه نود؛ باید داخل config یکتا باشد. |
type | string | باید دقیقاً "TlsClient" باشد. |
settings | object | تنظیمات TLS client. |
next | string | نود 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 | برای نزدیکتر شدن رفتار به Chrome، گروه hybrid با نام X25519MLKEM768 را اعلام میکند. |
tls13-record-shaping | تنظیم نشده | تنظیم آزمایشی padding و delay سمت فرستنده برای TLS 1.3. |
verbose | false | جزئیات بیشتری از state مربوط به TLS را log میکند. |
با verify: true، فایل CA داخلی utils/cacert.h در context مربوط به BoringSSL بارگذاری میشود. اگر verify را false
بگذارید، handshake همچنان واقعی است اما زنجیره certificate برای این instance از tunnel بررسی نمیشود.
مقادیر پیشفرض اختیاری فقط وقتی اعمال میشوند که key مربوطه وجود نداشته باشد. اگر alpns وجود داشته باشد، باید arrayای
از stringها باشد. طول هر نام باید بین ۱ تا ۲۵۵ byte باشد، نام تکراری پذیرفته نمیشود و طول فهرست encodeشده نباید از
۶۵٬۵۳۳ byte بیشتر شود. اگر verify، x25519mlkem768 یا verbose وجود داشته باشد، مقدار آن باید boolean واقعی JSON
باشد. نوع اشتباه باعث خطای startup میشود و بهمعنای انتخاب مقدار پیشفرض نیست.
بهتر است x25519mlkem768 را فعال نگه دارید. غیرفعال کردن آن ClientHello را کوچکتر میکند، اما شباهت handshake به نسخههای
فعلی Chrome را هم کاهش میدهد.
شکلدهی آزمایشی TLS 1.3 Record
تنظیم tls13-record-shaping بهصورت اختیاری نخستین application recordهای TLS 1.3 را که همین نود میفرستد pad میکند و
به تأخیر میاندازد. اگر این تنظیم وجود نداشته باشد، قابلیت غیرفعال است. این قابلیت هرگز ciphertext دریافتی، handshake
recordها، alertها، application recordهای خالی، byteهای 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 از byteهای صفر استاندارد TLS 1.3 در TLSInnerPlaintext استفاده میکند و بهاندازه انتخابشده به ciphertext
میافزاید، البته تا ظرفیت قانونی باقیمانده در TLS record. Delay پس از رمزنگاری اعمال میشود و latency انتخابشده را
اضافه میکند. ترتیب recordها روی wire با رابطه
release_at = max(now + delay, previous_release_at) حفظ میشود. ciphertext صفشده برای هر line حداکثر ۱ MiB است؛ در
۷۶۸ KiB به producer backpressure داده میشود و در ۳۸۴ KiB دوباره آزاد میشود. انتقال Pause و Resume از state فعلی wire
پیروی میکند: اگر تخلیه هنگام Resume بهصورت re-entrant یک Pause دیگر از wire دریافت کند، Resume قدیمی تا رسیدن Resume
واقعی بعدی ارسال نمیشود. اگر سمت cleartext پایان یابد، timer لغو میشود و تا زمانی که wire قابل نوشتن باشد ciphertext
صفشده client بهصورت synchronous و با ترتیب FIFO آزاد میشود. اگر wire در حالت pause باشد، باقی صف دور ریخته میشود؛
state محلی و upstream Finish پیش از بازگشت callback تکمیل میشوند.
در طول این تخلیه نهایی همگام، downstream Payload و Est دور ریخته میشوند و Pause و Resume فقط state محلی wire-paused را
بهروزرسانی میکنند؛ هیچیک از این 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
نتیجه این انتخاب را به نود قبلی WaterWall گزارش نمیدهد، mode مربوط به HTTP آن نود را عوض نمیکند و بررسی نمیکند که
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 را بهصورت
خودکار تشخیص نمیدهد؛ نود قبلی باید از قبل برای 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 | client session cache فعال |
| Session timeout | 7200 seconds |
| GREASE | فعال |
| Extension permutation | فعال |
| Certificate compression | Brotli decompression support |
| Signed certificate timestamps | فعال |
| OCSP stapling request | فعال per line |
گروههای پشتیبانیشده:
X25519MLKEM768:X25519:P-256:P-384:P-521
اگر x25519mlkem768 برابر false باشد، گروه اول حذف میشود:
X25519:P-256:P-384:P-521
ترتیب signature algorithmها نیز ثابت و شبیه Chrome است. نسخه BoringSSL همراه پروژه هم تغییرات محلی لازم برای شکل ClientHello و ترتیب cipherها را دارد.
ALPS
TlsClient فقط وقتی protocol متناظر در alpns وجود داشته باشد، application setting شناختهشده و Chrome-like آن را
برای ALPS اضافه میکند:
| Protocol | ALPS payload |
|---|---|
h2 | Chrome payload ثابت 02 68 32 |
http/1.1 | payload خالی |
مقدار h2 همان payload خام Chrome است، نه یک HTTP/2 SETTINGS frame سریالشده. پس از ثبت مقدار هر protocol، BoringSSL
نمایش نهایی ALPS روی wire را میسازد. مقدارهای سفارشی ALPN بدون ALPS payload تعریفشده توسط TlsClient پیشنهاد میشوند.
رفتار در زمان اجرا
در upstream Init، TlsClient:
- state مربوط به BoringSSL را برای line مقداردهی اولیه میکند
- memory BIOها را میسازد
- SSL object را در client mode قرار میدهد
- SNI تنظیمشده را اعمال میکند
- upstream
Initرا به نود بعدی میفرستد SSL_connect()را اجرا میکند تا اولین ClientHello flight تولید شود- byteهای handshake تولیدشده را در جهت upstream میفرستد
به همین دلیل قرار دادن این نود پیش از TcpConnector طبیعی است: connector میتواند ClientHello را تا برقرار شدن اتصال
واقعی socket در buffer نگه دارد.
مفهوم Establishment
در WaterWall، Est یعنی transport زیرین برقرار شده است؛ نه اینکه TLS handshake هم پایان یافته باشد.
در حالت عادی، downstream Est نود بعدی بلافاصله به نود قبلی میرسد. اگر tunnel قبلی پیش از کامل شدن TLS handshake
payload بفرستد، TlsClient آن را در صف نگه میدارد و بعد از پایان handshake از طریق SSL_write() ارسال میکند.
در handshake-takeover mode، TlsClient downstream Est معمول را تا پایان TLS handshake نگه میدارد.
در این حالت ورودی takeover بهصورت record کامل TLS و یکییکی به BoringSSL داده میشود. پردازش دقیقاً پس از recordی که
handshake را کامل میکند متوقف میشود تا recordهای بعدی بیرون SSL object و در اختیار dispatcher صریح باقی بمانند.
جریان Payload
cleartext upstream:
- پیش از کامل شدن handshake در صف میماند
- بعد از handshake به
SSL_write()داده میشود - TLS recordهای تولیدشده از write BIO خوانده و به نود بعدی ارسال میشوند
TLS recordهای downstream:
- bytes رمزنگاریشده داخل read BIO نوشته میشوند
SSL_connect()handshake را جلو میبرد- byteهای protocol تولیدشده در جهت upstream ارسال میشوند
- byteهای application که
SSL_read()رمزگشایی کرده است به نود قبلی میروند
در صورت خطا در بررسی certificate، نتیجه و دلیل گزارششده توسط BoringSSL در log ثبت میشود.
API
TlsClient یک API کوچک برای ساخت raw ClientHello دارد:
generateTlsHello:<sni>
ClientHello تولیدشده از تنظیمات همین tunnel instance استفاده میکند، از جمله alpns و x25519mlkem768.
طول SNI ورودی API باید بین ۱ تا ۲۵۵ byte باشد.
API Handshake Takeover
کد داخلی WaterWall میتواند از TlsClient در handshake-takeover mode استفاده کند:
| API | کاربرد |
|---|---|
tlsclientTunnelEnableHandshakeTakeover() | takeover mode را روی tunnel فعال میکند. |
tlsclientTunnelIsHandshakeCompleted() | بررسی میکند که آیا TLS handshake یک line کامل شده است. |
tlsclientTunnelGetHandshakeBinding() | نسخه، cipher، randomها و sequenceهای TLS 1.2 را پیش از آزاد شدن BoringSSL ثبت میکند. |
tlsclientTunnelDeinitAfterHandshake() | فقط برای takeover فوری TLS 1.2 است و TLS 1.3 را رد میکند. |
tlsclientTunnelBeginTakeoverDrain() | dispatch خارجی post-handshake در TLS 1.3 را آغاز میکند و bytes خام جمعشده را با حفظ 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 محلی و بسته شدن هر دو جهت میشوند. این نود مالک lineهای عادی بیرونی نیست و برای آنها lineDestroy() را فراخوانی
نمیکند.
TlsClient برای بستن اتصال از سیاست direct transport close استفاده میکند:
این انتخاب در بستن عادی برای تقلید از رفتار Chrome موردنظر این tunnel است: وقتی در این وضعیت کاربر یا برنامه اتصال را
میبندد، Chrome بدون فرستادن TLS close_notify پیش از آن، transport را مستقیماً میبندد. این توضیح به این معنا نیست که
Chrome در همهٔ مسیرهای TLS shutdown هرگز close_notify نمیفرستد.
- در upstream
Finishعادی، state مربوط به TLS آزاد میشود وFinishفقط به نود بعدی میرود - در downstream
Finishخام از سمت transport، state مربوط به TLS آزاد میشود وFinishفقط به نود قبلی میرود - مسیر بستن عادی
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 انجام میشود.
Metadata نود
| Property | Value |
|---|---|
| Node flag | kNodeFlagChainHead |
| Previous node | مجاز، در استفاده عادی required |
| Next node | مجاز، در استفاده عادی required |
| Layer group | kNodeLayerAnything |
required_padding_left | 0 |
| Line state | SSL object مربوط به BoringSSL، memory BIOها، handshake flagها، payloadهای upstream queue شده |
قابلیتهای پشتیبانینشده
این پیادهسازی از موارد زیر پشتیبانی نمیکند:
- TLS termination سمت server
- انتخاب dynamic certificate
- TLS روی packet lineها
- wrapping transport غیر TLS مثل WebSocket یا HTTP بهتنهایی
برای این layerها از نودهای جداگانه WaterWall استفاده کنید.
اشتباههای رایج
- انتظار نداشته باشید
TlsClientخودش به remote server وصل شود؛ بعد از آن connector بگذارید. - فرض نکنید WaterWall
Estیعنی TLS handshake کامل شده است. - از key مفرد
alpnاستفاده نکنید؛ فهرست ترتیبی را با arrayِalpnsتنظیم کنید. - هنگام پیشنهاد چند application protocol، انتخاب ثابت target server را بدانید و نود قبلی را برای همان protocol تنظیم
کنید.
TlsClientانتخاب ALPN انجامشده توسط server را به آن نود گزارش نمیدهد. - برای اتصال عمومی،
verifyرا فقط زمانی غیرفعال کنید که عمداً نمیخواهید certificate را اعتبارسنجی کنید. - اگر هدف شما handshake نزدیک به Chrome فعلی است،
x25519mlkem768را غیرفعال نکنید.
پیشرفته: ترفند ECH SNI
این قابلیت بخشی از پیکربندی عادی TLS یا مسئولیت معمول TlsClient نیست. فقط زمانی از آن استفاده کنید که deployment بهطور
عمدی ساخت TLS ClientHello را با دستکاری سازگار در سطح packet هماهنگ میکند.
تنظیم اختیاری ech-sni-trick یک string مربوط به hostname با طول ۱ تا ۲۵۵ byte میپذیرد:
"ech-sni-trick": "example.net"
وقتی این گزینه تنظیم شود، TlsClient با hostname دادهشده یک ClientHello جعلی و دوم میسازد و byteهای آن را بهعنوان
GREASE encrypted_client_hello payload داخل ClientHello واقعی و بیرونی قرار میدهد. SNI بیرونی و cleartext همچنان
settings.sni باقی میماند.
ClientHello جعلی از همان فهرست و ترتیب تنظیمشده alpns استفاده میکند، اما برای کوچکتر نگه داشتن payload،
x25519mlkem768 را غیرفعال میکند. این رفتار برای هماهنگی با مکانیزمهای packet-side مانند ترفند packet-splitting در
IpManipulator طراحی شده است تا byteهایی که BoringSSL از ClientHello hash میکند با byteهای قرارگرفته روی wire یکسان
بمانند.
مقدار این گزینه باید یک string در JSON با طول ۱ تا ۲۵۵ byte باشد؛ مقدار خارج از این محدوده یا هر نوع دیگر JSON باعث
خطای startup میشود. API تونل generateTlsHello:<sni> نیز در صورت تنظیم بودن، این گزینه را اعمال میکند.