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

VlessClient

VlessClient پیاده‌سازی سمت client پروتکل plain VLESS v0 در WaterWall است. request header شامل UUID و مقصد نهایی را می‌نویسد و سپس داده TCP stream یا UDP datagram‌ها را از node stream-facing بعدی عبور می‌دهد.

این node فقط بخش پایه VLESS را پیاده‌سازی می‌کند؛ TLS، REALITY، XTLS Vision، flow addon، mux، XUDP، WebSocket، gRPC و wrapper‌های transport باید با node‌های دیگری ساخته شوند.

جایگاه رایج​

VLESS client برای TCP:

TcpListener -> VlessClient -> TlsClient -> TcpConnector

VLESS client برای UDP:

UdpListener -> VlessClient -> TlsClient -> TcpConnector

مقصد پویا از proxy یا router:

Socks5Server -> VlessClient -> TlsClient -> TcpConnector
SniffRouter -> VlessClient -> TlsClient -> TcpConnector

VlessClient خودش TLS ایجاد نمی‌کند. در deployment معمول VLESS، TlsClient را بعد از آن قرار دهید و TcpConnector نهایی را به address و port سرور VLESS وصل کنید. مقصد واقعی سرویس در request header مربوط به VLESS نوشته می‌شود.

نمونه TCP​

[
{
"name": "vless-client",
"type": "VlessClient",
"settings": {
"uuid": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "example.com",
"port": 443,
"protocol": "tcp",
"domain-strategy": "do-not-resolve-domains",
"verbose": false
},
"next": "tls-client"
},
{
"name": "tls-client",
"type": "TlsClient",
"settings": {
"sni": "vless.example.net",
"verify": true
},
"next": "server-out"
},
{
"name": "server-out",
"type": "TcpConnector",
"settings": {
"address": "vless.example.net",
"port": 443,
"nodelay": true
}
}
]

نمونه dynamic destination​

{
"name": "vless-client",
"type": "VlessClient",
"settings": {
"id": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "dest_context->protocol"
},
"next": "tls-client"
}

id و user-id alias‌های قابل قبول برای uuid هستند.

نمونه UDP​

{
"name": "vless-client-udp",
"type": "VlessClient",
"settings": {
"uuid": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "udp"
},
"next": "tls-client"
}

در حالت UDP، node قبلی همچنان با UDP کار می‌کند. VlessClient برای آن line یک TCP carrier line داخلی می‌سازد، درخواست VLESS UDP را روی carrier می‌فرستد و هر UDP payload را پس از یک length دو بایتی big-endian قرار می‌دهد.

فیلدهای لازم​

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

فیلدهای لازم داخل settings:

فیلدنام‌های قابل قبولتوضیح
UUIDuuid, id, user-iduser ID در VLESS؛ می‌تواند به فرمت dashed در RFC4122 یا hex فشرده ۳۲ کاراکتری باشد و به‌شکل ۱۶ byte خام ارسال می‌شود.
target addresstarget-address, address, targethost یا IP مقصد نهایی در درخواست VLESS، یا "dest_context->address".
target portportport مقصد نهایی، یا "dest_context->port"؛ مقدار عددی باید در بازه 1..65535 باشد.

UUID با byte order شبکه در RFC4122 سریال می‌شود؛ نه به‌شکل ASCII و نه با memory layout مربوط به GUID پلتفرم.

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

فیلدپیش‌فرضتوضیح
protocol"tcp"انتخاب VLESS command. نام proto هم پذیرفته می‌شود.
first-payload-timeout-ms400مهلت به میلی‌ثانیه صحیح در بازه 0 تا 4,294,967,295؛ صفر انتظار عمدی را غیرفعال می‌کند.
verbosefalseجزئیات بیشتری از tunnel را log می‌کند.
domain-strategy"do-not-resolve-domains"مشخص می‌کند domain مقصد پیش از نوشتن درخواست به‌صورت محلی resolve شود یا نه.

مقدارهای پشتیبانی‌شده برای protocol:

مقداررفتار
"tcp" یا "connect"VLESS TCP command 0x01 ارسال می‌شود.
"udp" یا "udp-associate"VLESS UDP command 0x02 ارسال می‌شود و UDP datagram‌ها روی TCP carrier داخلی حمل می‌شوند.
"dest_context->protocol" یا "line->dest_ctx->protocol"protocol از destination context ورودی گرفته می‌شود. TCP دقیق command 0x01 و UDP دقیق command 0x02 را انتخاب می‌کند.

اگر با انتخاب dest_context->protocol، protocol ورودی وجود نداشته باشد، مبهم باشد یا TCP/UDP نباشد، client یک warning در log می‌نویسد و TCP را انتخاب می‌کند.

Domain Strategy​

domain-strategy تعیین می‌کند آدرس نهایی در درخواست VLESS چگونه نوشته شود.

مقداررفتار
"do-not-resolve-domains"نام domain بدون تغییر در درخواست می‌ماند تا سمت VLESS server آن را resolve کند.
"resolve-domains-and-accept-dns-returned-order"resolve محلی انجام می‌شود و نخستین نتیجه قابل استفاده انتخاب می‌شود.
"resolve-domains-and-prefer-ipv4"پس از resolve محلی، IPv4 در اولویت است و در صورت نیاز IPv6 استفاده می‌شود.
"resolve-domains-and-prefer-ipv6"پس از resolve محلی، IPv6 در اولویت است و در صورت نیاز IPv4 استفاده می‌شود.
"resolve-domains-and-use-only-ipv4"فقط IPv4 قبول می‌شود. اگر DNS پاسخ IPv4 ندهد line بسته می‌شود.
"resolve-domains-and-use-only-ipv6"فقط IPv6 قبول می‌شود. اگر DNS پاسخ IPv6 ندهد line بسته می‌شود.
"resolve-domains-with-core-settings"resolve با تنظیمات dns در core.json انجام می‌شود.

با فعال بودن resolve محلی، VlessClient یک DomainResolver داخلی می‌سازد. prepare hook آن پیش از DNS lookup مقصد تنظیم‌شده را اعمال می‌کند. اگر مقصد نهایی VLESS نام domain باشد resolve انجام می‌شود؛ چه مقصد از JSON آمده باشد و چه از "dest_context->address". آدرس‌های IP بدون تغییر می‌مانند.

قالب request و Response​

request header:

version:      00
uuid: 16 raw bytes
addons len: 00
command: 01 TCP or 02 UDP
destination: port first, then address

destination format:

port:          2 bytes big-endian
address type: 01 IPv4, 02 domain, 03 IPv6
address body: IPv4 4 bytes, domain length + bytes, or IPv6 16 bytes

response header:

00 00

برای سازگاری بیشتر، client response addon غیرخالی را می‌پذیرد و یک warning در log ثبت می‌کند. بایت‌های addon نادیده گرفته می‌شوند و سپس body تحویل داده می‌شود؛ خود addon پردازش نخواهد شد.

اولین داده و برقراری transport​

Init مقصد و node بعدی را آماده می‌کند و به‌تنهایی درخواستی ارسال نمی‌کند. اولین payload معتبر می‌تواند پیش از Est، داخل callback آن یا هنگام Pause برسد. درخواست کامل و تمام اولین payload در یک buffer معمولی ارسال می‌شوند؛ در حالت UDP header یک datagram نیز در همان buffer قرار می‌گیرد. ورودی عادی با headroom کافی در همان buffer استفاده می‌شود؛ در غیر این صورت buffer عادی best-fit ساخته می‌شود و prefix مقیم و بدنه pipe خصوصی به ترتیب کپی می‌شوند. TCP خالی درخواست را فعال نمی‌کند. datagram خالی UDP با prefix طول 00 00 ارسال می‌شود و اگر نخستین payload برنامه باشد، درخواست اولیه را فعال می‌کند.

در TCP مستقیم، تمام رویدادهای transport Est، حتی تکراری‌ها، مستقل از داده پروتکل و Pause به برنامه می‌رسند. در UDP، Est مربوط به carrier فقط یک بار به برنامه نگاشت می‌شود. اگر درخواست هنوز ارسال نشده باشد، مهلت از اولین Est آغاز می‌شود. settings.first-payload-timeout-ms به‌طور پیش‌فرض 400 است و عدد صحیح از 0 تا 4,294,967,295 را می‌پذیرد؛ نوع‌ها و مقدارهای دیگر نامعتبرند. صفر انتظار عمدی را غیرفعال می‌کند. زمان Pause نیز محاسبه می‌شود. در TCP مستقیم، رسیدن موعد حتی پس از Pause یک درخواست بدون payload ارسال می‌کند. تمام اعلان‌های Pause/Resume، حتی تکراری‌ها، شفاف عبور می‌کنند و وضعیت Pause گیرنده نگهداری نمی‌شود. در carrier متعلق به client برای UDP، این node سر همان line است و کنترل تولید خروجی را حفظ می‌کند: اگر موعد درخواست بدون payload هنگام Pause آن carrier برسد، ارسال تا Resume منتظر می‌ماند. رسیدن payload معتبر پیش از ارسال درخواست، timer را لغو کرده و خروجی ترکیبی را ارسال می‌کند. Est یا Resume تکراری درخواست را دوباره ارسال نمی‌کنند و مهلت را از نو آغاز نمی‌کنند. Finish نیز timer را لغو می‌کند. قرار گرفتن داده در یک buffer معمولی به معنی ارسال آن با یک syscall، یک packet TCP یا یک TLS record نیست.

مسیر مستقیم TCP و بسته‌بندی هر UDP در upstream، صف خروجی برای Est یا Resume ندارند. در پاسخ VLESS نسخه صفر و تمام addon‌ها، حداکثر 257 بایت metadata، مصرف و اعتبارسنجی می‌شوند. addon غیرخالی با هشدار رد می‌شود. ارسال خروجی منتظر پاسخ نیست.

پس از ارسال درخواست TCP، مصرف کامل header پاسخ و تخلیه ورودی نگه‌داشته‌شده و callback‌های تو در تو، node وضعیت محلی line را نابود می‌کند و برای آن line به‌طور دائمی bypass می‌شود. callback‌های بعدی مستقیماً بین همسایه‌ها عبور می‌کنند. تکمیل فقط درخواست یا فقط پاسخ کافی نیست.

line مربوط به application در UDP همچنان borrowed است و client یک carrier معمولی TCP وابسته روی همان worker ایجاد می‌کند. Est و Pause/Resume و Finish به association درست نگاشت می‌شوند. درخواست مقصد ثابت را حمل می‌کند و هر فریم یک طول دوبایتی big-endian دارد. decoder تمام datagram‌های کامل ورودی پذیرفته‌شده را حتی پس از Pause تحویل می‌دهد. ورودی callback تو در تو پشت ورودی قدیمی در FIFO parser قرار می‌گیرد؛ تنها قطعه ناقص یا وضعیت لازم برای ترتیب باقی می‌ماند. Finish، ورودی خراب یا رد گیرنده، تحویل را متوقف و association را می‌بندد. این node فقط carrier متعلق به خودش را نابود می‌کند؛ مالک برنامه در shutdown همین carrier وابسته را نیز تخلیه می‌کند.

Splice و محدودیت نگهداری​

اولین درخواست و payload معمولی هستند؛ خود درخواست حداکثر 278 بایت است. TCP بعدی هویت buffer معمولی یا splice را حفظ می‌کند. UDP از padding اعلام‌شده 280 بایتی استفاده می‌کند؛ هدر هر datagram همچنان حداکثر 2 بایت است. parser فقط هدر لازم را می‌خواند و حرکت دقیق بدنه، مرز datagram و padding کامل مسیر بعدی را حفظ می‌کند. کمبود padding یا ظرفیت pipe با fallback کامل معمولی مدیریت می‌شود.

ورودی نگهداری‌شده parserسقف منطقی شامل برابری
بدنه TCP فقط برای ترتیب ورودی تو در تو2,097,152 بایت، برابر 2 MiB
تکمیل پاسخ TCP با head فعال و metadata2,097,409 بایت، برابر 2 MiB به‌علاوه 257 بایت
تکمیل ورودی UDP با head فعال و هدرها2,162,689 بایت، برابر 2 MiB به‌علاوه 65,537 بایت
بدنه UDPاز 0 تا 65,535 بایت

برابری مجاز است؛ سرریز یا رد صف parser همان association را می‌بندد. صف خروجی decodeشده و محدودیت 1,024 رکورد برای batch همزمان وجود ندارد. تعداد قطعه‌های ورودی سهمیه خروجی را مصرف نمی‌کند. این حدود از اندازه pool، پروفایل RAM و ظرفیت pipe مستقل‌اند. datagram محلی بیش‌ازحد بزرگ حذف می‌شود و فریم دریافتی خراب association را می‌بندد. length دریافتی 0 prefix دو بایتی خود را مصرف می‌کند و دقیقاً یک datagram خالی UDP تحویل می‌دهد. association باز می‌ماند و parser سپس frame بعدی را به‌ترتیب پردازش می‌کند. این رفتار با پیاده‌سازی base VLESS در Xray تفاوت دارد: writerها datagram خالی را حذف می‌کنند و reader از frame با طول صفر عبور می‌کند. تحویل datagram خالی به پشتیبانی peer نیاز دارد؛ framing داده غیرخالی تغییری نمی‌کند. datagram خالی در لایه UDP معتبر است (RFC 8085 §5).

splice نیازمند misc.splice، build مناسب و پشتیبانی تمام node‌های chain است. DomainResolver پشتیبانی می‌کند؛ TlsClient همچنان splice را برای کل chain غیرفعال می‌کند. در استقرار معمول TLS را نگه دارید؛ این قابلیت تضمین zero-copy یا افزایش سرعت اندازه‌گیری‌شده نیست.

رفتار Finish​

VlessClient پیش از فرستادن Finish واقعی، line state خود را از بین می‌برد. برای line‌های معمولی بیرونی نیز lineDestroy() را فراخوانی نمی‌کند.

در حالت UDP، مالک carrier line داخلی‌ای است که خودش ساخته است. بسته شدن app line یا carrier line باعث بسته شدن سمت مقابل و از بین رفتن امن carrier line می‌شود.

padding​

VlessClient برای بزرگ‌ترین درخواست با اندازه ۲۷۸ بایت و prefix دو بایتی طول اولین datagram UDP مقدار زیر را اعلام می‌کند:

required_padding_left = 280

قابلیت‌های پشتیبانی‌نشده​

این پیاده‌سازی موارد زیر را پشتیبانی نمی‌کند:

  • ساخت TLS یا REALITY داخل node VLESS
  • XTLS Vision یا flow addons
  • VLESS mux
  • reverse
  • XUDP
  • WebSocket، HTTP، gRPC یا transport wrapper‌های دیگر

برای این لایه‌ها از node‌های جدا مثل TlsClient، HttpClient، MuxClient یا TcpConnector استفاده کنید.

اشتباه‌های رایج​

  • انتظار نداشته باشید VlessClient خودش به VLESS server وصل شود؛ بعد از آن stream transport بگذارید.
  • در deployment عمومی VLESS، TlsClient را حذف نکنید مگر اینکه عمداً plain VLESS را روی wire بخواهید.
  • address سرور VLESS را با target address نهایی اشتباه نگیرید. TcpConnector به server وصل می‌شود؛ VLESS request header مقصد نهایی را حمل می‌کند.
  • UDP mode را با next node غیر stream-facing استفاده نکنید.
  • به feature‌های پشتیبانی‌نشده مثل Vision، flow، mux یا XUDP تکیه نکنید.

متادیتای node​

متادیتای برگرفته از کد منبع:

ویژگیمقدار
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left280 بایت