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

ConnectionToPackets

ConnectionToPackets کار معکوس PacketsToConnection را با active-open انجام می‌دهد. یک connection line عادی WaterWall را می‌گیرد، flow متناظر TCP یا UDP را در stack lwIP داخل همان process باز می‌کند و packet‌های خام IPv4 را به سمت packet در chain می‌فرستد. packet‌های برگشتی دوباره به همان flow تزریق می‌شوند و دادهٔ آن‌ها به‌شکل payload در جهت downstream روی connection line اصلی برمی‌گردد.

normal connection -> ConnectionToPackets -> raw IPv4 TCP/UDP packets
return IPv4 packets -> ConnectionToPackets -> original connection downstream

این node یک userspace transport bridge است، نه TUN شفاف سیستم‌عامل. به kernel TUN، raw socket یا دسترسی root نیاز ندارد. برنامه باید از adapter‌ای مثل TcpListener، ‏UdpListener، ‏TcpUdpListener یا Socks5Server وارد WaterWall شود؛ capture کردن socket‌های نامرتبط kernel بیرون از scope آن است.

mtu یک بار هنگام ساخت تعیین و اعتبارسنجی می‌شود. هر نمونه MTU مستقل خود را نگه می‌دارد؛ تنظیم node‌های دیگر آن را تغییر نمی‌دهد. اگر مقدار به‌ارث‌رسیده از misc.mtu در core نامعتبر باشد، باید برای همان node یک مقدار معتبر تعیین کنید.

توپولوژی‌های معمول​

برای مقصد ثابت، target-address و port را در ConnectionToPackets تنظیم کنید:

TcpListener -> ConnectionToPackets -> packet transport

یک مسیر کامل و مستقیم بدون نیاز به root یا transport مجزا:

TcpListener -> ConnectionToPackets -> PacketsToConnection -> TcpUdpConnector -> destination

برای مقصد متفاوت در هر connection، node قبلی می‌تواند destination context را تعیین کند:

TcpListener -> Socks5Server -> ConnectionToPackets
-> PacketsToConnection -> TcpUdpConnector -> destination

یا روی WireGuard:

شاخه‌ی application محلی: TcpListener -> Socks5Server -> ConnectionToPackets -> Bridge A
شاخه‌ی UDP محلی: UdpStatelessSocket -> WireGuardDevice -> Bridge B
سمت راه‌دور: UdpStatelessSocket -> WireGuardDevice -> PacketsToConnection
-> TcpUdpConnector -> destination

Bridge A و Bridge B را pair کنید تا شاخهٔ تبدیل connection به packet، به سمت packet در WireGuard وصل شود. UdpStatelessSocket فقط در ابتدای chain مجاز است؛ پس باید در ابتدای شاخه‌ی خودش بماند و پس از WireGuardDevice قرار نگیرد.

آدرس مجازی source-ipv4 باید در توپولوژی packet مسیر برگشت داشته باشد. در WireGuard، سمت محلی معمولاً peer را با route مقصد 0.0.0.0/0 انتخاب می‌کند. peer سمت مقابل هم باید این آدرس مجازی مبدأ را، معمولاً به‌صورت /32، مجاز بداند.

Scope​

پشتیبانی می‌شود:

  • TCP روی IPv4؛
  • UDP روی IPv4، از جمله datagram بزرگ‌تر از یک MTU؛
  • مقصد IP و مقصد domain از طریق DomainResolver داخلی؛
  • چند event worker.

رد یا drop می‌شود:

  • IPv6، به‌صورت دائمی و عامدانه؛
  • ICMP و protocol‌های غیر TCP/UDP؛
  • UDP payload بالاتر از 65507 بایت؛
  • packet برگشتی بدون flow ثبت‌شده.

تحویل ICMP error و حفظ source port اصلی application فعلاً پیاده‌سازی نشده‌اند. این node فقط IPv4 تولید می‌کند. domain-strategy فقط only-ipv4 را می‌پذیرد؛ اگر domain فقط رکورد AAAA داشته باشد، DNS با پیامی شامل نام domain شکست می‌خورد. آدرس IPv6 صریح در target-address باعث رد config می‌شود؛ مقصد IPv6 در context نیز با بستن همان line رد می‌شود. این تصمیم، IPv6 سراسری lwIP را برای node‌های دیگر غیرفعال نمی‌کند.

مرز نهایی‌سازی Checksum​

در ورودی مسیر برگشت، این node آخرین مصرف‌کنندهٔ درخواست محاسبهٔ مجدد checksum روی buffer همان packet است. node پیش از هر اعتبارسنجی یا drop، درخواست را می‌گیرد و پاک می‌کند. سپس packet را در حافظهٔ قابل تغییر با alignment مناسب قرار می‌دهد و checksum‌های پشتیبانی‌شده را پیش از تحویل به lwIP و پیش از صف‌گذاری اصلاح می‌کند؛ پیام بایتی صف دیگر به flag جداگانه نیاز ندارد. malformed بودن packet، دریافت IPv6، توقف node یا رد ورودی درخواست buffer دیگری را تغییر نمی‌دهد. برای TCP/UDP بدون fragmentation، header IPv4 و transport checksum اصلاح می‌شوند؛ برای fragment فقط checksum header IPv4 قابل محاسبه است و transport checksum ساختگی تولید نمی‌شود.

Fragmentation​

ورودی packet باید datagram کامل IPv4 باشد. packet دارای MF یا fragment offset غیرصفر پیش از تحویل به lwIP حذف می‌شود؛ DF به‌تنهایی مجاز است. این رد ورودی buffer و منابع موقت را آزاد می‌کند و packet line و اتصال‌های دیگر زنده می‌مانند. اولین drop هشدار می‌دهد و تعداد کل dropها هنگام تخریب node گزارش می‌شود. برای تحویل datagram تکه‌شده، پیش از این node reassembly انجام دهید. ساختار malformed همچنان از مسیر validation/drop معمول می‌گذرد.

این الزام برای ورودی downstream است. خروجی UDP بزرگ‌تر از MTU همچنان به‌وسیلهٔ lwIP fragment می‌شود و MSS ارسال TCP از MTU به دست می‌آید. همهٔ fragment‌های خروجی یک flow به همان worker می‌روند. اگر مصرف‌کنندهٔ خروجی، stack دیگری است، باید پیش از تحویل به آن reassembly انجام شود. policy دستگاه فقط خواندن از همان دستگاه را کنترل می‌کند و packet‌های خروجی node دیگر را بازسازی نمی‌کند.

کپی‌های handoff در هر دو جهت، برای هر instance مجموعاً به ۱۶ MiB و ۴۰۹۶ allocation محدودند؛ metadata و padding نیز محاسبه می‌شوند. سهم پیش از allocation رزرو و هنگام آزادسازی واقعی، حتی برای custom pbuf، آزاد می‌شود. اشباع خروجی ERR_MEM برمی‌گرداند و ورودی اضافه drop می‌شود. اولین refusal هشدار و مجموع هر جهت هنگام تخریب گزارش می‌شود.

تنظیمات​

تنظیماجباریپیش‌فرضمعنی
source-ipv4بلهنداردsource IPv4 مجازی netif/PCB‌ها؛ نباید wildcard، loopback، multicast یا broadcast باشد.
target-addressخیرdest_context->addressآدرس IPv4 یا domain مقصد؛ رشتهٔ دقیق dest_context->address آدرس مقصد ورودی را نگه می‌دارد. آدرس تنظیم‌شده فقط بخش آدرس را جایگزین می‌کند. IPv6 صریح پذیرفته نمی‌شود.
portخیرdest_context->portعدد صحیح پورت مقصد در بازهٔ 1..65535، یا رشتهٔ دقیق dest_context->port برای نگه‌داشتن پورت مقصد ورودی.
mtuخیرmisc.mtu سراسریبازهٔ 576..9000؛ MSS از آن مشتق و UDP بالاتر از آن fragment می‌شود.
domain-strategyخیرonly-ipv4تنها مقدار پذیرفته‌شده only-ipv4 است.
tcp-connect-timeout-msخیر30000deadline اتصال TCP active-open.
max-pending-bytesخیر262144سقف دادهٔ application پیش از connect یا هنگام پُربودن send window؛ بازهٔ 1024..67108864.

مقدار پیش‌فرض فقط وقتی استفاده می‌شود که فیلد نوشته نشده باشد. نوع اشتباه، عدد کسری یا مقدار خارج از بازه باعث رد config با ذکر نام فیلد می‌شود. MTU مربوط به core پیش از تبدیل به نوع عددی کوچک‌تر، در بازهٔ 68..65535 اعتبارسنجی می‌شود. اگر مقدار به‌ارث‌رسیده خارج از بازهٔ مجاز خود node باشد، config باز هم رد می‌شود.

{
"name": "connection-to-packets",
"type": "ConnectionToPackets",
"settings": {
"source-ipv4": "10.90.0.1",
"mtu": 1420,
"domain-strategy": "only-ipv4",
"tcp-connect-timeout-ms": 30000,
"max-pending-bytes": 262144
},
"next": "wireguard-device"
}

انتخاب مقصد و Protocol​

قاعده دقیقاً با TcpUdpConnector یکسان است: protocol مشخص‌شده در destination context اولویت دارد؛ در غیر این صورت، protocol مشخص‌شده در source context استفاده می‌شود. اگر protocol موجود نباشد یا مبهم باشد، همان line رد می‌شود.

target-address و port مستقل از هم مقصد را انتخاب می‌کنند. حذف هر فیلد یا استفاده از مقدار دقیق dest_context->... مربوط به آن، همان بخش از destination context ورودی را نگه می‌دارد. source-ipv4 فقط مبدأ مجازی packet‌های خروجی است و مقصد را تعیین نمی‌کند.

تنظیمات یک بار در prepare hook متعلق به node و پیش از DNS در DomainResolver داخلی اعمال می‌شوند. domain تنظیم‌شده مانند domain ورودی resolve می‌شود و node آدرس resolve‌شده را بدون اعمال دوبارهٔ تنظیمات مصرف می‌کند. پیش از بازشدن flow، مقصد باید آدرس و پورت غیرصفر داشته باشد. TcpListener عادی protocol مبدأ TCP را تأمین می‌کند؛ در نتیجه برای forwarding به مقصد ثابت نیازی به SOCKS نیست:

{
"name": "connection-to-packets",
"type": "ConnectionToPackets",
"settings": {
"source-ipv4": "10.90.0.1",
"target-address": "example.com",
"port": 443
},
"next": "packets-to-connection"
}

این تنظیمات node برای مسیر مستقیم بالا مناسب است. اگر مسیر packet به host دیگری می‌رود، next را به ورودی packet transport خود تغییر دهید.

fake DNS وجود ندارد؛ حفظ domain اصلی در سمت مقابل به پروتکل جداگانه‌ای برای انتقال metadata نیاز دارد.

Flow Control​

TCP در هر دو جهت backpressure واقعی دارد. داده‌ای که lwIP هنوز نمی‌تواند بپذیرد، با بودجهٔ max-pending-bytes و حاشیهٔ ورودی توضیح‌داده‌شده در ادامه، در صف می‌ماند و سمت قبلی Pause می‌شود. دادهٔ دریافتی ابتدا downstream تحویل داده می‌شود؛ اگر در همان callback یک Pause به‌صورت re-entrant برسد، receive credit به lwIP برنمی‌گردد و receive window بسته می‌شود. هر flow علاوه بر سقف بایت‌ها، حداکثر 1024 ورودی در صف نگه می‌دارد؛ عبور از این سقف فقط همان flow را reset و بسته می‌کند. UDP صف stream ندارد و هنگام Pause، datagram‌ها را drop می‌کند.

Lifecycle​

  • connection line از نوع borrowed است؛ node فقط state خودش را آزاد می‌کند و هرگز lineDestroy() را فراخوانی نمی‌کند. packet line‌های هر worker متعلق به chain هستند و در طول runtime زنده می‌مانند. دریافت Finish روی آن‌ها نقض قرارداد است و باعث abort شدن process می‌شود.
  • پیش از callback نخست، آرایهٔ netif هر worker، ظرفیت اولیهٔ رجیستری flow و tombstone ring ساخته می‌شوند. اتمام حافظهٔ heap خطای fatal است؛ سایر خطاهای سازنده منابع آماده‌شده را آزاد می‌کنند. رجیستری fragment ورودی وجود ندارد.
  • چون chain ممکن است با L4 آغاز شود، packet side در onStart() با task جدا برای هر worker مقداردهی اولیه می‌شود.
  • بسته‌شدن graceful اتصال TCP، یک tombstone برای مسیریابی به مدت 2 * TCP_MSL در ring ثابتی با 4096 ورودی نگه می‌دارد. ثبت مجدد همان tuple، tombstone را جایگزین می‌کند.
  • flow متصل، حتی با صف خالی، به closer متعلق به node منتقل می‌شود. حساب بایت‌های پذیرفته‌شده و receive credit دقیقاً یک بار تسویه می‌شود. closer ابتدا همهٔ بایت‌ها را می‌نویسد و سپس با tcp_shutdown(pcb, 0, 1) فقط TX را می‌بندد. از tcp_close() استفاده نمی‌شود، چون با وجود receive credit تسویه‌نشده ممکن است RST بفرستد و باعث از دست رفتن داده شود.
  • deadline تحویل داده/FIN برابر 30 ثانیه و deadline بسته‌شدن peer برابر 60 ثانیه است. closer‌ها در مجموع به 4096 object و 16 MiB از دنبالهٔ کپی‌شدهٔ داده محدودند. عبور از هر سقف، PCB را دقیقاً یک بار abort می‌کند. رسیدن به TIME_WAIT پایان graceful است و مالکیت PCB با lwIP می‌ماند.
  • خطای ERR_MEM در write فقط تا وقتی نوشتن blocked است، از طریق tcp_poll دوباره امتحان می‌شود؛ خطاهای دیگر flow را می‌بندند. deadline اتصال یک timer قابل لغو روی worker مالک است که دقیقاً یک reference به line نگه می‌دارد.
  • generation مانع تزریق packet صف‌شدهٔ flow قدیمی به flow جدیدی با همان tuple می‌شود. با این حال، packet دیررس شبکه generation ندارد؛ tombstone فقط بازه‌ای را که ممکن است چنین ابهامی رخ دهد کوتاه‌تر می‌کند.
  • onQuiesceRequest() وضعیت stopping را منتشر می‌کند و پذیرش جدید را از طریق gate‌های prev/next و classification/publication بدون انتظار می‌بندد. سپس onQuiesceWait() بازگشت همهٔ callback‌های admitted را تضمین می‌کند. در lifecycle-v2، message‌های emit، inject و packet-Init اولیه پیش از توقف و آزادشدن component به پایان می‌رسند؛ بنابراین می‌توانند pointer تونل را مستقیم حمل کنند. message لغوشده فقط منابع خودش را آزاد می‌کند. onWorkerStop() روی worker مالک، همهٔ PCB‌ها، flow‌ها، deadline‌ها، drain‌ها و netif‌های محلی را detach می‌کند و PCB‌ها را abort می‌کند. state و buffer و reference مربوط به borrowed line تا Finish از source واقعی باقی می‌مانند، حتی اگر CTP زودتر drain شود. onStop() فقط پس از barrier همهٔ worker‌ها رجیستری مشترک را آزاد می‌کند و PCB خارجی را پیمایش نمی‌کند.
  • داده‌ای که قبلاً پذیرفته نشده، از timer روی event loop همان worker مالک دوباره تحویل داده می‌شود؛ callback تحویل همچنان در صف قرار می‌گیرد. اگر صف، یک کار کنترلی ضروری را نپذیرد، ابتدا callback و route مربوط به PCB جدا می‌شوند و line از نوع borrowed در terminal registry ازپیش‌تخصیص‌یافته ثبت می‌شود. درخواست shutdown منظم برنامه، اقدام بعدی پس از این تحویل مسئولیت است. مالک واقعی line در پایان آن را آزاد می‌کند.

Capacity​

همهٔ node‌هایی که از lwIP استفاده می‌کنند، pool‌های static سراسری مشترک دارند:

CMake variableپیش‌فرضمصرف
WW_LWIP_MAX_TCP_FLOWS2048TCP PCB و segment pool
WW_LWIP_MAX_UDP_FLOWS2048UDP PCB
WW_LWIP_MAX_REASS_DATAGRAMS32datagram fragment‌شده هم‌زمان
WW_LWIP_MAX_TCP_LISTENERS320listener PCB

ظرفیت pool‌های وابسته هم در مرحلهٔ configure و هم در preprocessor از نظر overflow نوع u16_t بررسی می‌شود. heap class‌ها بر اساس مجموع ظرفیت TCP و UDP ساخته می‌شوند. ظرفیت reassembly مستقل از تعداد flow‌هاست: هر datagram حداکثر ۱۶ pbuf، pool سراسری ۳۲×۱۶ و PBUF_POOL_SIZE سه برابر آن است. wrapper از نوع zero-copy در PTC reassembly و TCP out-of-order را پوشش می‌دهد و TCP_OOSEQ_MAX_PBUFS مصرف یک peer را محدود می‌کند.

سقف window ارسال و دریافت به معنی رزرو ۲ MiB برای هر اتصال نیست. فرستنده‌های متوقف‌شده برای pool مشترک segment و heap رقابت می‌کنند؛ کپی full-MSS، packet کوچک و خروجی ACK/control منابع متفاوتی مصرف می‌کنند.

pool‌ها static و بین worker‌ها مشترک‌اند. آرایه‌های اشغال TCP/UDP در مجموع ۵۱۲ KiB حافظهٔ کنترلی ثابت دارند. جدول‌ها و timeout‌های engine فقط برای worker فعال تخصیص می‌یابند. ظرفیت pool، اندازهٔ BSS و RSS سه معیار جدا هستند. هر engine دارای namespace مستقل با ۲۵۵ index برای netif است؛ loopback و node‌های همان worker در آن شریک‌اند. هویت interface شامل engine و netif دقیق است. تمام‌شدن ظرفیت، فقط flow مربوط را با ثبت پیامی دارای rate limit می‌بندد و باعث abort شدن process نمی‌شود.

Composition​

  • node قبلی باید layer 4 و node بعدی layer 3 باشد.
  • next اجباری و مقصد packetهاست.
  • حضور این node chain را packet chain می‌کند و packet line هر worker را می‌سازد.

متادیتای نود​

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

ویژگیمقدار
node flagskNodeFlagNone
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer3 | kNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer3
required_padding_left0 bytes

بودجهٔ صف TCP برابر P = max-pending-bytes است و یک حاشیهٔ محدود به اندازهٔ L، حاشیهٔ ثابت ۱ MiB مستقل از اندازهٔ pool، پروفایل حافظه و تنظیمات splice، برای ورودیِ قبلاً تحویل‌شده دارد. سقف کل بایت‌های pending برابر P + L است تا Pause فرصت اثرگذاری داشته باشد. سقف ۱۰۲۴ entry و زمان ارسال Pause/Resume تغییر نمی‌کند؛ ورودیِ بیش از این سقف پیش از ورود به صف رد می‌شود و فقط همان flow را می‌بندد.

نوشتن TCP همان سر FIFO را پس از هر write موفق حداکثر ۶۵۵۳۵ بایتی ادامه می‌دهد تا ظرفیت واقعی تمام شود یا ERR_MEM رخ دهد. پیشوند موفق حتی هنگام رد write بعدی ارسال می‌شود؛ دنباله در ابتدای صف می‌ماند و byteها تکرار نمی‌شوند.

منابع engine و چرخهٔ عمر​

runtime برای هر event worker معمولی حداکثر یک engine خام lwIP را هنگام نخستین استفاده می‌سازد. همهٔ bridge‌های همان worker از آن استفاده می‌کنند؛ thread جداگانهٔ stack و core mutex وجود ندارد. PTC مالک packet تحویل‌شده را حفظ می‌کند و CTP مالک normal line برنامه را حفظ می‌کند. پاسخ CTP پس از lookup کامل tuple/WID/generation در صف worker مالک قرار می‌گیرد و engine و netif دقیق دوباره بررسی می‌شوند. callback همسایه همیشه deferred اجرا می‌شود، حتی روی همان worker.

pool‌های flow/payload با همان ظرفیت سراسری باقی می‌مانند. هر engine فعال حافظهٔ کنترلی مستقل و ۱۷ رکورد timeout دارد؛ worker استفاده‌نشده engine ندارد. انتخاب خودکار port‌های TCP و UDP از حساب مشترک جداگانه استفاده می‌کند و اشغال تا آزادشدن PCB، شامل TIME_WAIT، باقی می‌ماند. ID‌های تولیدی fragment در IPv4 و IPv6 هرکدام sequence مشترک جداگانه‌ای دارند و با همان عرض محدود قبلی wrap می‌شوند. drain list محلی هر worker است، ولی سقف هر node همچنان ۴۰۹۶ object و ۱۶ MiB است. lock مربوط به pool/quota هنگام پیمایش stack یا تولید خروجی آن نگه داشته نمی‌شود. Fake DNS در PTC یک mutex مشترک node برای transaction و کپی نام reverse دارد؛ هیچ pointer رکوردی بیرون lock باقی نمی‌ماند و output پس از unlock است.

در quiesce، timer مربوط به engine پیش از توقف پذیرش کار عادی جدا می‌شود. node در drain همان owner ریشه‌های stack خود را آزاد می‌کند. runtime پس از تسویهٔ messageهای detached و پیش از نابودی loop و pool‌ها engine را آزاد می‌کند. pool‌ها و port accountهای مشترک و secret مربوط به ISN در سطح process فقط پس از خروج همهٔ worker‌ها نهایی می‌شوند. fragment ورودی هر دو bridge همچنان رد می‌شود و reassembly دستگاه پیش از انتخاب worker انجام می‌شود؛ MTU و fragmentation خروجی تغییر نکرده‌اند.

طول UDP عادی پیش از checksum و انتشار child بررسی می‌شود: حداقل ۸ بایت و حداکثر بایت‌های transport محدودشده توسط IP. بایت‌های اضافهٔ پس از طول معتبر UDP از pbuf حذف می‌شوند؛ فقط payload اعلام‌شده تحویل می‌شود. datagram با header تنها، یک callback با payload خالی دارد. حالت trusted نیز این بررسی ساختاری را حفظ می‌کند.