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

UdpStatelessSocket

UdpStatelessSocket یک adapter در ابتدای chain و در لبه UDP است. یک UDP socket را bind می‌کند، datagram‌ها را از peer‌های مختلف می‌گیرد و برای flow ورودی هر peer یک line معمولی WaterWall می‌سازد.

کاربرد اصلی این node کنار node‌هایی مثل WireGuardDevice است؛ جایی که یک UDP socket مشترک باید هم برای peer‌های تنظیم‌شده داده بفرستد و هم packet‌های ورودی آن‌ها را بپذیرد. datagram‌های ورودی بر اساس endpoint‌های peer و local گروه‌بندی می‌شوند. line‌هایی که این node نساخته است نیز می‌توانند با استفاده از routing context از همان socket داده بفرستند.

جایگاه رایج​

UdpStatelessSocket -> WireGuardDevice
UdpStatelessSocket -> WireGuardDevice -> TunDevice

در لبه UDP یک packet chain زمانی از این node استفاده کنید که node کناری برای send و receive به یک UDP socket مشترک نیاز دارد و هر peer ورودی باید line و lifetime جداگانه‌ای داشته باشد. نمونه اصلی WireGuardDevice است: پیش از ارسال datagram رمزنگاری‌شده، endpoint peer را در destination routing context می‌نویسد؛ datagram‌های ورودی نیز با line مخصوص همان peer وارد chain می‌شوند.

این node فقط در ابتدای chain پشتیبانی می‌شود: prev ممنوع و next الزامی است. این یک تصمیم طراحی فعلی است، نه محدودیتی موقت. ساخت peer از انتهای chain و ارسال downstream Init غیرفعال است.

نمونه تنظیم​

{
"name": "wg-udp-socket",
"type": "UdpStatelessSocket",
"settings": {
"listen-address": "0.0.0.0",
"listen-port": 51820,
"interface": "eth0",
"fwmark": 10,
"large-send-buffer": true,
"large-recv-buffer": true,
"source-ip": "192.0.2.10",
"verbose": false
},
"next": "wg"
}

تنظیمات ضروری​

فیلدنوعتوضیح
listen-addressstringآدرس IP محلی برای bind. باید IP معتبر باشد.
listen-portintegerپورت UDP محلی. بازه معتبر 0 تا 65535 است.

object مربوط به settings الزامی است و نباید خالی باشد.

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

فیلدپیش‌فرضتوضیح
interfaceندارددر صورت پشتیبانی پلتفرم، socket را به یک network device محلی محدود می‌کند.
fwmarkنداردsocket mark در پلتفرم‌هایی که SO_MARK دارند؛ عمدتاً Linux.
source-ipنداردآدرس مؤثر bind را تغییر می‌دهد و در این node جای listen-address را می‌گیرد.
large-send-buffertrueمقدار true از socket buffer بزرگ پیش‌فرض WaterWall استفاده می‌کند، false تنظیم پیش‌فرض kernel را نگه می‌دارد و یک عدد مثبت همان اندازه را بر حسب byte درخواست می‌کند.
large-recv-buffertrueهمان رفتار large-send-buffer را برای SO_RCVBUF دارد؛ false یعنی استفاده از مقدار پیش‌فرض kernel.
verbosefalseجزئیات datagram‌های ارسالی و دریافتی را در debug log می‌نویسد.

اندازه buffer بزرگ پیش‌فرض فعلی 16777216 بایت است.

رفتار interface و source-ip​

interface پیش از bind اعمال می‌شود. در Linux معمولاً socket مستقیماً به device متصل می‌شود. اگر پلتفرم چنین قابلیتی نداشته باشد و source-ip هم تنظیم نشده باشد، WaterWall تلاش می‌کند socket را به آدرس IPv4 همان interface bind کند.

در این node source-ip مستقیماً جای listen-address را می‌گیرد، چون یک socket مشترک هم ارسال را انجام می‌دهد و هم دریافت را.

اگر loop protection در TunDevice یک egress pin خودکار ایجاد کرده باشد، source-ip به‌تنهایی آن را تغییر نمی‌دهد. برای انتخاب interface دیگر، interface را صریحاً تنظیم کنید.

مسیر دریافت​

وقتی UDP datagram برسد:

  1. worker مالک socket، datagram را دریافت می‌کند
  2. UdpStatelessSocket از endpoint peer، endpoint محلی، و هویت tunnel یک flow key می‌سازد
  3. اگر flow متناظری وجود نداشته باشد، یک line معمولی می‌سازد و upstream Init را به next می‌فرستد
  4. آدرس و پورت فرستنده داخل routing_context.src_ctx نوشته می‌شود و local listener port هم ثبت می‌شود
  5. بدنه datagram روی line همان peer فرستاده می‌شود

داده ورودی همیشه از ابتدای chain در جهت upstream به next می‌رود. Finish محلی یا ناشی از timeout نیز به همین سمت ارسال می‌شود؛ downstream Finish از next فقط line متعلق به همین node را می‌بندد و بازتاب داده نمی‌شود.

line‌های idle peer برای init timeout برابر 30 seconds و keepalive timeout برابر 300 seconds استفاده می‌کنند.

مسیر ارسال​

وقتی payload از next در جهت downstream به این node برسد:

  1. اگر line مربوط به یک peer ورودی و ساخته همین tunnel باشد، datagram به endpoint ذخیره‌شده همان peer ارسال می‌شود
  2. در غیر این صورت مقصد از line->routing_context.dest_ctx خوانده می‌شود
  3. اگر مقصد یک domain حل‌نشده باشد، async DNS آغاز می‌شود
  4. اگر مقصد IP و port آماده باشد، به socket address تبدیل می‌شود
  5. datagram با همان UDP socket ارسال می‌شود

اگر destination context آماده نباشد یا port نداشته باشد، datagram دور ریخته می‌شود و warning یا error در log ثبت خواهد شد.

نتیجهٔ DNS برای domain‌های حل‌نشده بر اساس domain، port و domain strategy به مدت 30 minutes cache می‌شود. این cache یک FIFO ثابت با ظرفیت 256 entry است: کلید موجود در همان entry تازه می‌شود و ظرفیت را افزایش نمی‌دهد، entry‌های منقضی پیش از insert آزاد می‌شوند و cache پُر قدیمی‌ترین entry را بیرون می‌اندازد. این نکته مهم است، چون routing context روی packet line موقتی است و packet بعدی ممکن است آن را بازنویسی کند.

worker Ownership​

UDP socket تنها در اختیار یک worker loop است. اگر payload از worker دیگری برای ارسال برسد، عملیات روی worker مالک socket زمان‌بندی می‌شود و sendto() همان‌جا اجرا خواهد شد.

چرا UdpListener یا UdpConnector نه؟​

در WireGuard و سناریوهای مشابه، همان socket باید هم برای peer داده بفرستد و هم از آن دریافت کند. UdpListener برای ورودی و UdpConnector برای خروجی طراحی شده‌اند. UdpStatelessSocket هر دو جهت را با یک socket پوشش می‌دهد، برای datagram‌های ورودی line مخصوص هر peer می‌سازد و line‌های دیگر را بر اساس routing context ارسال می‌کند.

packet-line Semantics​

UdpStatelessSocket adapterی برای UDP datagramهاست. هر peer ورودی یک line معمولی می‌گیرد؛ datagram‌ها مستقیماً وارد payload callback مربوط به packet line نمی‌شوند.

packet line‌ها همچنان می‌توانند بر اساس routing context داده outbound بفرستند و این tunnel هیچ packet line‌ای را در runtime از بین نمی‌برد.

خطاهای رایج​

  • ارسال payload قبل از اینکه dest_ctx مقصد IP/domain و port داشته باشد.
  • در نظر گرفتن line‌های peer ورودی به‌عنوان packet line.
  • استفاده از این node مثل stream adapter عمومی.
  • تکیه به source-ip برای override کردن loop-protection؛ در این حالت interface را صریح تنظیم کنید.
  • فراموش کردن وابستگی fwmark و interface binding به پلتفرم.

متادیتای node​

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

ویژگیمقدار
node flagskNodeFlagChainHead | kNodeFlagSupportsSplice
can_have_prevfalse
can_have_nexttrue
layer_groupkNodeLayerAnything
layer_group_prev_nodekNodeLayerNone
layer_group_next_nodekNodeLayerAnything
required_padding_left0 بایت

ارسال UDP با splice​

دریافت UDP همچنان از buffer عادی استفاده می‌کند. در chain واجد شرایط روی Linux، ارسال UDP دادهٔ عادی یا prefix واقعی همراه با بدنهٔ pipe خصوصی را به‌صورت یک datagram به مقصد مشخص می‌پذیرد؛ datagram خالی نیز معتبر است. چیدمان‌های پشتیبانی‌شده از splice بین pipe و socket استفاده می‌کنند. برای pipe با fragment‌های زیاد، یا وقتی پیش از ارسال نتوان حد امن fragment‌ها را اثبات کرد، کل datagram پیش از شروع تجمیع روی socket به buffer عادی تبدیل می‌شود. دادهٔ بیش‌ازحد بزرگ بدون تقسیم رد می‌شود.

فشار پیش از شروع تجمیع همان سیاست drop فعلی را حفظ می‌کند و صف retry برای UDP ایجاد نمی‌شود. splice ناقص یا شکست commit پس از priming فقط socket مربوط را بازنشسته می‌کند و line‌های عادی وابسته از مسیر مالکشان بسته می‌شوند. بنابراین peer‌های یک socket مشترک ممکن است با هم بسته شوند. باقی‌ماندهٔ ناقص دوباره ارسال نمی‌شود و socket‌های دیگر فعال می‌مانند. سیاست خطای ارسال عادی تغییر نمی‌کند.

این پرچم بررسی پشتیبانی پلتفرم، misc.splice، ممنوعیت chain packet و پشتیبانی همهٔ node‌های chain نهایی را دور نمی‌زند. zero-copy سرتاسری یا بهبود کاراییِ اندازه‌گیری‌شده تضمین نمی‌شود.

socket همچنان فقط نقش سر chain دارد. worker مالک I/O ارسال محلی و ارسال‌شده از worker دیگر را سریالی اجرا می‌کند. buffer محلی به pool محلی برمی‌گردد؛ buffer جداشده از worker دیگر و buffer لغوشده مستقیماً آزاد می‌شوند. retry فعلی EINTR برای ارسال عادی حفظ می‌شود، اما تجمیع ناقص splice هرگز retry نمی‌شود.