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

Socks5Client

Socks5Client یک tunnel میانی stream است که سمت client پروتکل SOCKS5 را پیاده می‌کند. این node از transport بعدی برای اتصال به SOCKS proxy استفاده می‌کند، method negotiation را انجام می‌دهد، در صورت نیاز با username/password احراز هویت می‌شود و سپس برای مقصد نهایی تنظیم‌شده درخواست CONNECT یا UDP ASSOCIATE می‌فرستد.

مقصد نوشته‌شده در تنظیمات Socks5Client همان مقصد نهایی داخل درخواست SOCKS است. آدرس خود SOCKS proxy باید در connector بعدی تنظیم شود؛ معمولاً TcpConnector برای TCP یا TcpUdpConnector در حالتی که relay مربوط به UDP لازم است.

جایگاه رایج​

برای TCP:

TcpListener -> Socks5Client -> TcpConnector

برای TCP و UDP از یک SOCKS server:

TcpUdpListener -> Socks5Client -> TcpUdpConnector

در چیدمان دوم، connector باید برای control connection از TCP استفاده کند و relay address برگشتی از UDP ASSOCIATE را از طریق UDP در دسترس قرار دهد.

نمونه پایه​

{
"name": "socks-client",
"type": "Socks5Client",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "tcp",
"user": "alice",
"pass": "secret",
"domain-strategy": "do-not-resolve-domains",
"verbose": false
},
"next": "proxy-transport"
}

proxy-transport باید node‌ای باشد که به SOCKS5 server متصل می‌شود؛ برای مثال یک TcpConnector که آدرس و port مربوط به proxy در آن تنظیم شده است.

پروتکل پویا​

اگر line ورودی از قبل protocol مقصد را مشخص کرده است، می‌توانید از dest_context->protocol استفاده کنید تا client همان protocol را دنبال کند.

{
"name": "socks-client",
"type": "Socks5Client",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "dest_context->protocol"
},
"next": "proxy-transport"
}

اگر destination context دقیقاً TCP باشد، CONNECT فرستاده می‌شود و اگر دقیقاً UDP باشد، UDP ASSOCIATE انجام خواهد شد. در نبود flag مناسب، در حالت مبهم یا برای protocolی غیر از TCP/UDP، یک warning ثبت می‌شود و node به TCP برمی‌گردد.

مقصد ثابت​

اگر می‌خواهید مقصد SOCKS را به‌صورت ثابت در JSON بنویسید:

{
"name": "socks-client",
"type": "Socks5Client",
"settings": {
"address": "example.com",
"port": 443,
"protocol": "tcp",
"domain-strategy": "resolve-domains-and-prefer-ipv4"
},
"next": "proxy-transport"
}

با فعال بودن resolve محلی domain، DomainResolver داخلی پیش از فرستادن درخواست SOCKS، example.com را resolve می‌کند. اگر پاسخ IPv4 موجود باشد، همان آدرس در درخواست SOCKS قرار می‌گیرد؛ در غیر این صورت fallback مطابق strategy انتخاب‌شده انجام می‌شود.

فیلدهای اجباری​

فیلدنوعتوضیح
settingsobjectاجباری است و نباید خالی باشد.
settings.address / settings.target-address / settings.targetstringآدرس مقصد نهایی SOCKS. می‌تواند IP ثابت، domain با طول ۱ تا ۲۵۵ بایت، dest_context->address یا line->dest_ctx->address باشد. domain طولانی‌تر هنگام ساخت node رد می‌شود.
settings.portnumber یا stringپورت مقصد نهایی. عدد 1 تا 65535، یا dest_context->port / line->dest_ctx->port.
nextstringاجباری است و به transport سمت SOCKS proxy اشاره می‌کند.

فیلدهای اختیاری​

فیلدپیش‌فرضتوضیح
settings.protocol / settings.prototcpمقدارهای مجاز: tcp, connect, udp, udp-associate, dest_context->protocol, line->dest_ctx->protocol.
settings.user / settings.usernameتنظیم نشدهنام کاربری SOCKS5. باید همراه pass / password بیاید.
settings.pass / settings.passwordتنظیم نشدهرمز SOCKS5. باید همراه user / username بیاید.
settings.domain-strategydo-not-resolve-domainsتعیین می‌کند domain پیش از درخواست SOCKS به‌صورت local resolve شود یا همان domain ارسال گردد.
settings.verbosefalselog‌های debug بیشتری فعال می‌کند.

username و password باید غیرخالی باشند و هر کدام حداکثر ۲۵۵ بایت طول داشته باشند. اگر فقط یکی از آن‌ها تنظیم شود، ساخت node شکست می‌خورد.

Domain strategy​

Socks5Client پیش از اجرای منطق SOCKS، target تنظیم‌شده را آماده می‌کند و protocol انتخاب‌شده را روی line اصلی client و line->dest_ctx نگه می‌دارد.

اگر domain-strategy resolve محلی را فعال کند، client یک DomainResolver داخلی می‌سازد که prepare hook آن پیش از DNS resolution، target تنظیم‌شده را اعمال می‌کند. این کار هم برای domain ثابت و هم برای مقداری که از dest_context->address کپی شده انجام می‌شود؛ IP‌های ثابت دست‌نخورده می‌مانند.

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

جهت مقداردهی اولیه​

line‌های برنامه از طریق Init در جهت upstream وارد Socks5Client می‌شوند. Init در جهت downstream برای همه نقش‌های line، شامل application، control و relay در حالت UDP، غیرفعال است؛ دریافت آن یک پیام fatal ثبت می‌کند و با abortProgramNow(1) فرایند را خاتمه می‌دهد. callback‌های downstream برای Est و payload همچنان برای پاسخ‌های proxy و داده‌های relay پشتیبانی می‌شوند.

رفتار TCP​

در حالت TCP، lifecycle به این ترتیب است:

  1. upstream Init، line state مربوط به Socks5Client را مقداردهی اولیه می‌کند و Init را به transport بعدی می‌فرستد.
  2. transport بعدی اتصال proxy را برقرار می‌کند و Est بلافاصله، پیش از مذاکرهٔ SOCKS، به node قبلی می‌رسد.
  3. Socks5Client greeting مربوط به method‌های SOCKS5 را یک بار ارسال می‌کند. Estهای تکراری transport به برنامه منتقل می‌شوند، بدون ارسال دوبارهٔ greeting.
  4. اگر credential تنظیم شده باشد و server روش username/password را انتخاب کند، درخواست احراز هویت فرستاده می‌شود.
  5. node برای مقصد تنظیم‌شده دستور CONNECT می‌فرستد.
  6. پاسخ موفق، ارسال دادهٔ برنامه را آزاد می‌کند و Est دوباره فرستاده نمی‌شود.
  7. payload‌های upstream که در انتظار بودند، بدون انتظار برای Resume و با حفظ ترتیب FIFO روی اتصال proxy فرستاده می‌شوند.

اعلان‌های Pause/Resume در TCP بلافاصله عبور می‌کنند، حتی اگر تکراری باشند یا هنگام مذاکره برسند. greeting و خروجی آمادهٔ برنامه یا پاسخ منتظر Resume نمی‌مانند. Pause دریافت، حتی هنگام مذاکره به adapter بعدی می‌رسد؛ فرستندهٔ آن باید خودش Resume یا بستن اتصال را ترتیب دهد.

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

اگر پیش از موفق شدن درخواست SOCKS، دادهٔ upstream برسد، در صفی با محدودیت ۲ MiB و ۱۰۲۴ buffer نگه داشته می‌شود. حافظهٔ صف فقط هنگام اولین استفاده تخصیص می‌یابد. محدودیت ۴٬۰۹۶ بایت فقط برای metadata ناقص پاسخ است؛ یک پاسخ کامل می‌تواند همراه با بدنهٔ بزرگ‌تر برنامه دریافت شود. پردازش بازگشتی پاسخ‌ها ترتیبی است و دادهٔ پاسخِ نگه‌داری‌شده بودجهٔ مشترک ۲ MiB و ۱۰۲۴ buffer در جهت پایین‌دست دارد. این صف پس از ورودی قدیمی‌تر پاسخ تخلیه می‌شود، حتی در حضور Pause؛ ارسال مستقیم TCP همچنان همگام است. ورودی خالی TCP از پراکسی نادیده گرفته می‌شود و باقی‌ماندهٔ TCP روی control line مربوط به UDP پس از برقراری ارتباط دور ریخته می‌شود. line‌های UDP application و relay برای stream مربوط به handshake حافظه تخصیص نمی‌دهند.

اگر proxy همه method‌ها را رد کند، authentication شکست بخورد، پاسخ command نامعتبر باشد یا command با خطا تمام شود، tunnel آن line را می‌بندد.

رفتار UDP​

در حالت UDP، line اصلی همان مسیر application است و Socks5Client دو line داخلی می‌سازد:

line داخلیکاربرد
UDP control lineیک line TCP که به SOCKS proxy وصل می‌شود و UDP ASSOCIATE را با مقصد 0.0.0.0:0 می‌فرستد.
UDP relay lineیک line UDP که به relay address برگشتی از proxy وصل می‌شود.

relay line از BND.ADDR و BND.PORT پاسخ موفق UDP ASSOCIATE استفاده می‌کند. این line، port کنترل TCP را دوباره به‌کار نمی‌برد و endpoint مذاکره‌شده را با address و port ثابت SOCKS proxy در connector بعدی جایگزین نمی‌کند. بنابراین server می‌تواند یک relay port موقت و اختصاصی برگرداند؛ policy مربوط به firewall و NAT باید endpoint برگشتی را اجازه دهد، نه فقط port کنترل TCP را.

این authority مذاکره‌شده از پاک‌سازی metadata مربوط به protocol‌های مشاهده‌شده توسط Router یا sniffer عبور می‌کند. adapter خروجی UDP آن را دقیقاً یک‌بار مصرف می‌کند و سپس marker داخلی را پاک می‌کند تا روی routing نامرتبط بعدی اثر نگذارد.

Est مربوط به transport کنترل، یک بار به line برنامه منتقل می‌شود. ارسال datagram همچنان تا آماده شدن control association و relay line منتظر می‌ماند؛ صف آن به ۲ MiB و ۱۰۲۴ buffer محدود است. سپس Socks5Client یک header مربوط به SOCKS5 UDP و شامل مقصد نهایی به ابتدای هر datagram اضافه می‌کند و آن را از relay line می‌فرستد. پاسخ‌های relay اعتبارسنجی می‌شوند، header از آن‌ها برداشته می‌شود و فقط payload به node قبلی برمی‌گردد.

fragment‌های SOCKS5 UDP دوباره سرهم نمی‌شوند؛ datagram‌هایی که FRAG != 0 دارند برای احتیاط نادیده گرفته می‌شوند.

این مقدار padding اجازه می‌دهد header مربوط به SOCKS5 UDP بدون نقض قرارداد buffer به ابتدای datagram افزوده شود. اگر buffer ورودی باز هم فضای کافی در سمت چپ نداشته باشد، tunnel یک buffer جایگزین می‌گیرد و پیش از افزودن header، payload را در آن کپی می‌کند.

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

  • آدرس proxy را داخل settings.address ننویسید؛ این فیلد مقصد نهایی داخل request SOCKS است.
  • حالت UDP را با connector فقط-TCP به کار نبرید؛ این حالت هم control line از نوع TCP و هم relay line از نوع UDP می‌خواهد.
  • domain-strategy روی IP literal اثری ندارد.
  • وقتی از dest_context->address استفاده می‌کنید، port همچنان جداگانه لازم است.

پشتیبانی از splice​

Socks5Client از payload‌های TCP مبتنی بر splice پشتیبانی می‌کند. هنگام مذاکرهٔ روش احراز هویت، اعتبارسنجی و پاسخ command، کل هر ورودی با sbufEnsureOrdinary() پیش از ورود به parser یا صف ورودی بازگشتی به حافظهٔ عادی تبدیل می‌شود. پس از مذاکره، payload‌های TCP و ورودی‌های لازم در صف FIFO می‌توانند buffer اصلی را حفظ کنند. محدودیت handshake ناقص از محدودیت backlog برنامه جدا می‌ماند.

line‌های UDP application، TCP control و UDP relay در هر دو جهت خواندن عادی را برای تمام عمر خود درخواست می‌کنند. آماده‌سازی مقصد UDP این ترجیح را پیش از نگه‌داری ورودی توسط DNS محلی تنظیم می‌کند. این ترجیح تضمین نوع buffer نیست و به line‌های مرتبط منتقل نمی‌شود؛ هر datagram همچنان پیش از نگه‌داری، خواندن header یا قالب‌بندی کاملاً عادی می‌شود. ورودی عادی بدون تغییر عبور می‌کند. line‌های TCP CONNECT ترجیح خواندن عادی را درخواست نمی‌کنند.

DomainResolver داخلی قابلیت splice را حفظ می‌کند. فعال‌شدن واقعی به chain نهاییِ واجد شرایط و misc.splice وابسته است؛ وجود TLS یا node مسدودکنندهٔ دیگر splice را برای کل chain payload غیرفعال می‌کند.

متادیتای node​

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

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

Est اتصال transport را اعلام می‌کند، نه موفقیت مذاکرهٔ SOCKS. برای UDP نیز Est کنترل TCP یک بار به line برنامه نگاشت می‌شود. داده پس از Init پذیرفته می‌شود، اما تا آماده شدن پروتکل در صف محدود به ۲ MiB و ۱۰۲۴ buffer می‌ماند. در TCP، greeting و خروجی آمادهٔ برنامه یا پاسخ بدون انتظار برای Resume و با حفظ ترتیب FIFO فرستاده می‌شوند. اولین دادهٔ نگه‌داری‌شده، منبع خود را Pause می‌کند و پس از تخلیهٔ صف قدیمی‌تر، Resume فرستاده می‌شود. این توقف محلی پروتکل مستقل از فشار transport است که شفاف عبور داده می‌شود. Pause دریافت در TCP حتی هنگام مذاکره به adapter بعدی می‌رسد؛ فرستندهٔ آن باید Resume یا بستن اتصال را ترتیب دهد. در UDP، فشار مسیرهای کنترل و relay تجمیع می‌شود و Resume یکی، Pause دیگری را پاک نمی‌کند. تخلیهٔ backlog برنامه و greeting خط کنترلِ تحت مالکیت node همچنان این فشار را رعایت می‌کنند؛ فشار دریافت برنامه پس از Init خط relay به همان خط منتقل می‌شود. line‌های UDP application، control و relay برای قالب‌بندی، مالکیت association و نگاشت فشار فعال می‌مانند.