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-address | string | آدرس IP محلی برای bind. باید IP معتبر باشد. |
listen-port | integer | پورت UDP محلی. بازه معتبر 0 تا 65535 است. |
object مربوط به settings الزامی است و نباید خالی باشد.
تنظیمات اختیاری
| فیلد | پیشفرض | توضیح |
|---|---|---|
interface | ندارد | در صورت پشتیبانی پلتفرم، socket را به یک network device محلی محدود میکند. |
fwmark | ندارد | socket mark در پلتفرمهایی که SO_MARK دارند؛ عمدتاً Linux. |
source-ip | ندارد | آدرس مؤثر bind را تغییر میدهد و در این node جای listen-address را میگیرد. |
large-send-buffer | true | مقدار true از socket buffer بزرگ پیشفرض WaterWall استفاده میکند، false تنظیم پیشفرض kernel را نگه میدارد و یک عدد مثبت همان اندازه را بر حسب byte درخواست میکند. |
large-recv-buffer | true | همان رفتار large-send-buffer را برای SO_RCVBUF دارد؛ false یعنی استفاده از مقدار پیشفرض kernel. |
verbose | false | جزئیات 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 برسد:
- worker مالک socket، datagram را دریافت میکند
UdpStatelessSocketاز endpoint peer، endpoint محلی، و هویت tunnel یک flow key میسازد- اگر flow متناظری وجود نداشته باشد، یک line معمولی میسازد و upstream
Initرا بهnextمیفرستد - آدرس و پورت فرستنده داخل
routing_context.src_ctxنوشته میشود و local listener port هم ثبت میشود - بدنه 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 برسد:
- اگر line مربوط به یک peer ورودی و ساخته همین tunnel باشد، datagram به endpoint ذخیرهشده همان peer ارسال میشود
- در غیر این صورت مقصد از
line->routing_context.dest_ctxخوانده میشود - اگر مقصد یک domain حلنشده باشد، async DNS آغاز میشود
- اگر مقصد IP و port آماده باشد، به socket address تبدیل میشود
- 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 flags | kNodeFlagChainHead | kNodeFlagSupportsSplice |
can_have_prev | false |
can_have_next | true |
layer_group | kNodeLayerAnything |
layer_group_prev_node | kNodeLayerNone |
layer_group_next_node | kNodeLayerAnything |
required_padding_left | 0 بایت |
ارسال 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 نمیشود.