بخش ۴: packet lineها و تونلهای packet
WaterWall علاوه بر streamهای معمول، با packetهای layer 3 هم کار میکند؛ مثلاً در
دستگاه TUN، مسیرهای شبیه WireGuard و پردازش raw packet. مدل packet هم از
line_t استفاده میکند، اما چرخهی عمر آن کاملاً متفاوت است. اگر packet line
را مثل line یک اتصال در نظر بگیرید، یکی از خطرناکترین اشتباههای این بخش از کد
را مرتکب شدهاید.
قبل از تغییر کدی که زیر یک node از نوع layer 3 اجرا میشود، این بخش را بخوانید.
مرجع اصلی رفتار، این فایلها هستند:
ww/net/chain.c، ww/net/packet_tunnel.c، ww/net/line.h و خود تونلهای
packet.
packet line چیست؟
packet line یک line_t واقعی است، اما برخلاف line اتصال:
- برای هر chain دارای node از نوع layer 3، یک بار بهازای هر worker تخصیص مییابد؛
- میان همهی packetهایی که آن worker پردازش میکند مشترک است؛
- به چرخهٔ عمر اتصال TCP/UDP وابسته نیست؛
- در زمان اجرای عادی بسته نمیشود؛
- فقط زمانی نابود میشود که خود chain نابود شود.
کد منبع در این مورد صریح است. در tunnelchainFinalize() از
ww/net/chain.c، اگر chain node از نوع packet داشته باشد، برای هر worker یک
packet line ساخته میشود (در اینجا پاکسازی poolهای پیرامون کد حذف شده است):
line_t **packet_lines = memoryAllocate(sizeof(*packet_lines) * tc->workers_count);
if (! packet_lines)
{
startupFailureRecord(1);
return;
}
// ... ساخت و اعتبارسنجی Pool مربوط به Line همهٔ Workerها ...
tc->packet_lines = packet_lines;
for (wid_t i = 0; i < tc->workers_count; i++)
{
if (tc->contains_packet_node)
{
packet_lines[i] = lineCreateForWorker(0, tc->line_pools, i);
}
}
این lineها فقط در tunnelchainDestroy() از بین میروند:
for (uint32_t i = 0; i < tc->workers_count; i++)
{
if (tc->packet_lines && tc->packet_lines[i])
{
lineDestroy(tc->packet_lines[i]);
}
}
packet line هر worker را با tunnelchainGetWorkerPacketLine(chain, wid) بگیرید.
در کارکرد عادی روی packet line تابع lineDestroy() را فراخوانی نکنید و هیچ
منطقی طراحی نکنید که انتظار داشته باشد packet line پس از Finish دوباره ساخته
شود. برخی adapterهای packet مانند TunDevice و UdpStatelessSocket در نسخههای
Debug با assert بررسی میکنند که packet line پس از forwardشدن یک packet همچنان
زنده باشد. اگر تغییر شما اجازه میدهد packet line هنگام اجرا بمیرد، تقریباً
بهطور قطع دچار خطاست.
دریافت Finish در زمان اجرای packet line
بخش ۲ الزام میکند که handler مربوط به Finish
در مالک یک line عادی، هنگام بازگشت شرط ! lineIsAlive(line) را برقرار کرده
باشد. packet lineها بدون هیچ استثنایی از این شرط مستثنا هستند. مالک packet
line خود chain است، نه هیچیک از تونلها، و فقط tunnelchainDestroy() اجازه دارد
آن را نابود کند. این بخش از قرارداد مطلق است: handler هر کار دیگری که انجام دهد،
هرگز نباید روی packet line تابع lineDestroy() را فراخوانی کند.
با این حال، معنای رسیدن Finish به یک packet line کاملاً به بخش دریافتکننده در
chain، جهتی که callback از آن رسیده و نقش handler بستگی دارد. سه دسته وجود
دارد. جایگاه تونل در chain اطلاعات مفیدی میدهد، اما بهتنهایی برای انتخاب یکی
از این دستهها کافی نیست.
دستهٔ ۱ — anchor چرخهی عمر packet: ردکردن بستهشدن ناممکن
یک adapter یا Bridge مربوط به packet ممکن است state مربوط به packet pipeline
آن worker را روی packet line نگه دارد، بیآنکه اصولاً بستهشدن آن نقش معنایی
داشته باشد. منظور از «adapter» در اینجا مرز یا نقطهٔ پایانی packet pipeline است،
نه تونلی که مالک line_t باشد. در یک chain درست هیچ بخشی برای آن نقش مشخص
Finish نمیفرستد؛ بنابراین رسیدن Finish به آن handler و در آن جهت یعنی یکی
از همسایهها قرارداد را شکسته است:
- هرگز
lineDestroy()را فراخوانی نکنید؛ - خطا را آشکارا و فوری گزارش کنید:
LOGF(...)و سپسabortProgramNow(1)؛ این سیاست پروژه برای نقض قرارداد packet line است؛ - آن را به فرایند عادی بستن اتصال تبدیل نکنید؛
- نابودی واقعی line را به
tunnelchainDestroy()بسپارید.
adapterهایی که state را روی packet line نگه میدارند دقیقاً همین کار را انجام میدهند:
void tundeviceTunnelDownStreamFinish(tunnel_t *t, line_t *l)
{
discard t;
LOGF("TunDevice: unexpected downstream Finish on worker packet line %u", (unsigned int) lineGetWID(l));
abortProgramNow(1);
}
نمونهها عبارتاند از TunDevice و RawSocket؛ سمت packet در upstream در
PacketsToConnection و PacketsToStream؛ سمت packet در downstream در
StreamToPackets؛ همچنین PacketSplitStream، حالتهای packet در TesterClient
و TesterServer و نقش packet line در WireGuardDevice. این دستهبندی برای هر
handler است، نه برچسبی برای کل تونل: همان Bridge ممکن است در سمت دیگر خود مالک
lineهای عادی باشد و آنها را بهشکل معمول ببندد.
تست tests/unittests/tunnels/abort/tunnels_abort_runtime_test.c ثابت میکند فراخوانیهای
لنگر پوششدادهشده بازنمیگردند.
دستهٔ ۲ — transform میانی شفاف: forwardکردن چرخهٔ عمر
تونل میانیای که payloadهای packet را تبدیل میکند، صرفاً بهخاطر این کار
مالک یا پایاندهندهٔ چرخهٔ عمر line نمیشود. چنین تونلی باید Init، Est،
Pause، Resume، Finish و هر callback دیگری را که به تبدیل آن مربوط
نیست، در همان جهت forward کند:
- callbackهای upstream با
tunnelNextUpStream*ادامه مییابند؛ - callbackهای downstream با
tunnelPrevDownStream*ادامه مییابند؛ - callback دریافتشده هرگز بهسمت فرستندهٔ خود بازتابانده نمیشود.
PingClient این callbackهای عبوری چرخهٔ عمر را از packettunnelCreate() به ارث
میبرد؛ PingServer نیز forwarding صریح و معادل آنها را تنظیم میکند. پشتیبانی
از payload قرارداد جداگانهای است: هر دو سر Ping v2 برای مالکیت buffer و
اعتبارسنجی worker به packet line دقیق همان worker در chain نیاز دارند. ترتیبهای
پشتیبانیشده در لبههای آنها چنین است:
TunDevice -> PingClient -> RawSocket
RawSocket -> PingServer -> TunDevice
بنابراین یک توپولوژی معمولیِ line عادی مانند
UdpListener <-> PingServer <-> UdpConnector ترکیب پشتیبانیشدهای برای Ping v2
نیست. packettunnelCreate() همچنان callbackهای عبوری استاندارد چرخهٔ عمر را نگه
میدارد، اما handler پیادهسازیشدهٔ payload ممکن است نقش محدودتر و دقیقتری برای
packet line الزام کند.
دستهٔ ۳ — terminal absorber با رفتار مستند: رسیدگی محلی و مستندسازی
packet line همچنان یک line_t است و یک chain میتواند بهطور مجاز آن را در یک
adapter انتهایی سازگار با packet پایان دهد. آن adapter بدون مالکشدن، packet line
را قرض میگیرد و ممکن است سمت خودش را به دلیلی مستند ببندد.
یکی از نمونههای موجود، BlackHole فعال پس از PacketSender است:
PacketSender ──(Packet Line آن Worker)──> BlackHole(mode=active)
BlackHole فعال هنگام upstream Init، تابع tunnelPrevDownStreamFinish() را در
جهت downstream روی packet line همان worker میفرستد و callback به
packetsenderTunnelDownStreamFinish() میرسد. این اتفاق نقض قرارداد نیست و نباید
برنامه را متوقف کند. PacketSender هیچ state مختص line و هیچ prev ندارد؛ پس
بستهشدن را بدون forwardکردن یا نابودکردن packet line جذب میکند. بااینحال باید
timer و حلقهٔ ارسال آن worker را متوقف کند، زیرا دریافت Finish مقصد upstream را
برای همیشه میبندد.
adapterای که normal line اتصال میخواهد، مقصد packet line نیست. UdpConnector
فقط لایه ۴ است؛ مسیر packet باید از
PacketSender -> PacketsToStream -> UdpConnector استفاده کند و در سمت دریافت،
اگر payload باید به لایه ۳ بازگردد، StreamToPackets قرار گیرد. bridge مالک normal
line است و بازسازی آن را بدون بستن packet line پایدار مدیریت میکند.
این دستهبندی به جهت وابسته است. handler مربوط به downstream در PacketSender
جذبکننده است، اما handler مربوط به upstream آن همچنان باید با خطای کشنده متوقف
شود، زیرا ابتدای chain هیچ prevای ندارد که بتواند بهشکل مجاز callbackای برای
آن بفرستد.
PacketReceiver نیز جذبکنندهٔ عمدی دیگری است: هیچ state مختص line ندارد، ممکن
است یکی از دو انتهای chain باشد و تکمیل کار را بهجای Finish از طریق timer و
مسیر گزارش خود اعلام میکند.
بستهشدن را forward کنید، منابع تحت مالکیت خود را آزاد کنید، نقض قرارداد را گزارش
دهید، یا در handler توضیح دهید که هیچ state مختص line و هیچ جهت بعدی ندارید و در
نتیجه جذب بستهشدن تمام کاری است که باید انجام شود. بدنهای که فقط از دستورهای
discard تشکیل شده باشد، حالت آخر را از یک خطا متمایز نمیکند.
جذبکنندههای مجاز فعلی عبارتاند از BlackHole، یعنی مقصد انتهای chain که هر
line را قرض میگیرد؛ PacketReceiver، یعنی انتهای chainای که با timer و فایل
گزارش میدهد؛ و handler مربوط به downstream در PacketSender، یعنی دستهٔ ۳ بالا.
تست tests/line_ownership_policy_test.py همهٔ فایلهای
tunnels/*/{upstream,downstream}/fin.c را پیمایش میکند و handlerای را که تمام
بدنهاش فقط دستورهای discard است رد میکند، مگر اینکه همراه با دلیلی مکتوب در
SILENT_FINISH_ALLOWED آمده باشد. به این ترتیب، forwarding جاافتاده یا بستهنشدن
line بهدست مالک نمیتواند ظاهری شبیه یک عملیات تهی عمدی پیدا کند.
پس پرسش درست هرگز فقط «آیا این یک packet line است؟» یا «آیا این تونل در انتهای chain است؟» نیست. باید بپرسید: «این handler دقیق و این جهت چه نقشی دارد، تونل چه چیزی را روی این line نگه میدارد و چه کسی هنوز منتظر آن است؟»
handlerهای دومنظوره باید ابتدا نقش دقیق line را بررسی کنند
برخی nodeها همزمان هم یک packet line پایدار میبینند و هم مالک lineهای عادی هستند. handler باید پیش از انجام هر کاری تشخیص دهد کدامیک را دریافت کرده است:
WireGuardDeviceدر سمت packet خود تونلی ساختهشده باpackettunnelCreate()است، اما برای سمت رمزگذاریشده نیز بهازای هر worker یک line انتقال عادی میسازد.Finishروی line انتقال، close عادیِ line تحت مالکیت است: slot را نابود کنید وlineDestroy()را فراخوانی کنید.Finishروی packet line نقض قرارداد است و باید abort کند.PacketsToStreamوPacketsToConnection، state مربوط به Bridge را روی packet line نگه میدارند و در عین حال مالک lineهای عادیای هستند که پشت آن میسازند.
نقش را با کمک chain تشخیص دهید، نه بر اساس نام یا حدس:
if (tunnelchainIsWorkerPacketLine(tunnelGetChain(t), l))
{
LOGF("MyNode: unexpected Finish on worker packet line %u", (unsigned int) lineGetWID(l));
abortProgramNow(1);
}
/* از اینجا به بعد یک Line عادی است؛ شرط پس از اجرای مالک در بخش ۲ را اعمال کنید. */
هر دستهبندی عمدی که با نقش line یا جهت تغییر میکند باید کنار handler مستند شود و یک تست regression آن را پوشش دهد.
packet lineها state مشترک worker هستند
از آنجا که یک packet line در هر worker به تعداد زیادی packet نامرتبط سرویس میدهد، هر چیزی که روی آن ذخیره شود scratch state محلی worker است، نه هویت مختص اتصال. بهویژه مراقب این موارد باشید:
routing_context.src_ctxوrouting_context.dest_ctx- هر line state مربوط به تونل که روی packet line ذخیره شده است
این فیلدها با هر packet بازنویسی میشوند. آنها را peer، flow یا session پایدار در نظر نگیرید. state ذخیرهشده روی packet line باید در یکی از این دستهها معنا داشته باشد:
- state مختص worker؛
- scratch state مربوط به packet pipeline؛
- state پل که به سمت packet در worker متصل است.
اگر پشت ترافیک packet واقعاً به چرخهی عمر مختص flow یا اتصال نیاز دارید، در پشت سمت packet، lineهای عادی بسازید؛ packet line را وادار نکنید مانند اتصال رفتار کند.
درخواست checksum متعلق به buffer است
درخواست ترمیم checksum روی sbuf_t همان packet قرار دارد، نه روی packet line
مشترک worker. از sbufSetRecalculateChecksum() و sbufGetRecalculateChecksum()
استفاده کنید؛ نگهداشتن packet علامتخورده یا ارسال آن به worker دیگر نباید درخواست
packet نامرتبطی را تغییر دهد. کپی کامل و رشد buffer درخواست را حفظ میکنند؛ کپی
صرف بایتها به تخصیص دیگر به انتقال صریح درخواست یا ترمیم پیش از copy نیاز دارد.
قواعد دقیق نمایش معمولی، reset، کپی و بازهها در بخش ۳ آمدهاند.
packettunnelTakeChecksumRequest(buf) درخواست همان buffer را میگیرد و پاک
میکند. packettunnelConsumeChecksumRequest(buf) آن را در مرز نهایی مصرف و در
صورت نیاز checksum را اصلاح میکند. مرز نهایی میتواند پیش از ترمیم flag را پاک
کند، چون packet ناموفق حذف میشود. helperهای مشترک حتی بدون درخواست، ساختار دقیق
یک packet IPv4 را بررسی میکنند؛ IPv6، دادهٔ ناقص و بایتهای اضافه پذیرفته نمیشوند.
false فقط نبود درخواست است و اعتبار checksum را تضمین نمیکند.
TunDevice و RawSocket درخواست buffer واقعی را مصرف میکنند. پلهای packet به
stream پیش از تبدیل به بایتهای stream ترمیم را کامل میکنند؛ این flag روی wire
ارسال نمیشود. PTC درخواست را با کپی aligned مرتبط نگه میدارد؛ CTP ترمیم را روی
کپی بایتها پیش از صفگذاری کامل میکند. Ping پیش از encapsulation درخواست packet
داخلی و پیش از validation/decapsulation درخواست packet بیرونی را مصرف میکند؛
درخواست بیرونی به packet داخلی انتقال نمییابد.
decoded affinity خود buffer را همراه درخواست آن جابهجا میکند و snapshot جداگانه روی line بازنمیگرداند. fragment دارای درخواست پیش از staging ترمیم میشود؛ این ترمیم checksum header IPv4 را پوشش میدهد و transport checksum کل datagram را از یک fragment نمیسازد. مالکیت، ترتیب، worker affinity و cancellation ثابتاند.
ترمیم incremental فقط تغییر همان transform را پوشش میدهد و درخواست قدیمی را حفظ میکند. ProtoSwap پیش از encoding، ترمیم در پروتکل بومی را کامل و درخواست را پاک میکند؛ refusal یا no-match بایتها و درخواست را دستنخورده میگذارند. درخواست قدیمی را پس از encoding دوباره تنظیم نکنید. packetهای دارای checksum عمداً خراب میتوانند بدون درخواست باقی بمانند. این flag از تضمین checksum در metadata خصوصی TUN، حالت trusted و offload/GSO مستقل است؛ trusted بودن درخواست صریح را حذف نمیکند و درخواست نیز اعتبارسنجی ساختار یا شروط offload را دور نمیزند.
اندازهگذاری صف کرنل هنگام startup
در TunDevice و RawSocket، گزینهٔ kernel-queue-tune فقط boolean میپذیرد و
پیشفرض آن در Linux برابر true و در دیگر پلتفرمها false است. دیگر پلتفرمها
تنظیمات معتبر را میپذیرند ولی صفهایشان را تغییر نمیدهند. overrideهای عددی باید
عدد صحیح در بازهٔ 1..INT_MAX باشند؛ حذف گزینه به معنی خودکار است و صفر، عدد
منفی یا کسری، string، boolean و null رد میشوند. حتی با tuning یا capture
غیرفعال و روی پلتفرم غیر Linux نیز تنظیمات باید اعتبارسنجی شوند.
تعداد نهایی صفهای فعال پس از fallback مبنای محاسبه است و مقدار موجودِ بزرگتر
هرگز کاهش نمییابد. kernel-queue-length در TunDevice تعداد packet در هر
صف است. kernel-queue-packets و kernel-receive-buffer در RawSocket مقدار
کل برای هر دستگاه هستند که میان صفهای فعال تقسیم میشوند؛ گزینهٔ دوم تعداد
بایت در مقیاس درخواست SO_RCVBUF است. هر override جای هدف خودکار همان منبع را
میگیرد. سقف خودکار، مقدار صریح یا مقدار موجودِ بزرگتر را محدود نمیکند.
| RAM profile تنظیمشده | packet در هر صف TUN | packet ترجیحی در هر صف Capture | درخواست receive buffer ترجیحی در هر صف Capture |
|---|---|---|---|
| S1 / S2 | 1,024 | 1,024 | 1 MiB |
M1 (client) | 2,048 | 2,048 | 2 MiB |
M2 (client-larger) | 4,096 | 2,048 | 2 MiB |
L1 / L2 (server) | 8,192 | 4,096 | 4 MiB |
با N صف فعال، هدف خودکار TUN برابر min(profile_target, 131072 / N) است.
Capture بودجهٔ پایهٔ 65,536 packet و 64 MiB درخواست receive buffer را برای هر
دستگاه حفظ میکند. برای هر منبع، هدف خودکار هر صف برابر
max(baseline_total / N, min(profile_preferred, automatic_total_cap / N))
است؛ سقف خودکار کل 131,072 packet و 128 MiB است. مقدار کل صریح، هدف
max(baseline_total / N, override_total / N) را انتخاب میکند. تقسیم صحیح است.
این سیاست از RAM profile تنظیمشده استفاده میکند، نه تشخیص RAM میزبان یا تغییر
پویا با load. هر دستگاه بودجهٔ جدا دارد.
با tuning غیرفعال، طول صف موجود TUN دستنخورده میماند و Capture تنظیم پایه را
اجرا میکند؛ override معتبر اعمال نمیشود. این گزینه از skip-sysctl در Capture
و misc.tcp-tune در core مستقل است و sysctl سراسری تازهای نمینویسد. FIFO
مشترک userspace با بودجهٔ 512 packet و 8 MiB allocation charge، quantum هر
callback و backlog خروجی منابع جدا هستند.
خطای tuning یا query باید هشدار بدهد و صف قابلاستفاده را حفظ کند. اگر مقدار قبلی
TUN قابلخواندن نیست، نباید تغییری اعمال شود که احتمال کاهش ناشناخته داشته باشد.
startup حتی با tuning غیرفعال باید تعداد واقعی صفها و منبع
auto/override/disabled را ثبت کند. TUN مقدار قدیم، درخواستی و خواندهشدهٔ
txqueuelen را گزارش میکند؛ مقدار غیرقابلخواندن مشخص میشود. Capture سقف
NFQUEUE تأییدشده با ACK را گزارش میکند و مقدار تأییدنشده unknown میماند؛
SO_RCVBUF درخواستی و خواندهشده نیز ثبت میشود. Linux مقدار حسابداری
SO_RCVBUF را دوبرابر گزارش میکند و سقفهای socket میزبان ممکن است درخواست
را محدود کنند. این اعداد RSS واقعی یا تعداد بایت payload نیستند. صف بزرگتر
میتواند burst را جذب کند ولی تأخیر و مصرف حافظه را افزایش دهد؛ بهبود throughput
از این تنظیم بهتنهایی نتیجه نمیشود.
پیش از افزایش SO_RCVBUF، درخواست را روی یک socket موقت در همان network
namespace اعمال کنید و buffer مؤثر آن را بخوانید. تنها اگر probe افزایش را
تأیید کرد buffer صف را تغییر دهید؛ در غیر این صورت مقدار موجود را حفظ کنید.
socket موقت در همهٔ نتیجهها باید بسته شود. این روش به در دسترس بودن فایل
sysctl میزبان در namespace وابسته نیست و مجوز تغییر sysctl یا استفاده از
SO_RCVBUFFORCE نمیدهد.
دو نوع تونل packet
همهی تونلهایی که با packet line سروکار دارند از یک نوع نیستند. دو دستهی متمایز با قواعد متفاوت وجود دارد.
۱. تونلهای packet خالص با packettunnelCreate
برخی تونلها بهجای tunnelCreate() با packettunnelCreate() ساخته میشوند.
کد ww/net/packet_tunnel.c:
tunnel_t *packettunnelCreate(node_t *node, size_t tstate_size, size_t lstate_size)
{
if (UNLIKELY(lstate_size != 0))
{
return NULL; // تونلهای Packet خالص State مختص Line ندارند
}
tunnel_t *t = tunnelCreate(node, tstate_size, 0);
if (! t)
{
return NULL; // سرریز اندازهٔ State یا شکست تخصیص
}
// ... Callbackهای پیشفرض الزامی Payload مربوط به Packet را نصب میکند ...
return t;
}
نکات اصلی:
packettunnelCreate()الزام میکندlstate_size == 0باشد و در صورت نقض این قرارداد، نامعتبر بودن اندازهٔ state یا شکست تخصیص حافظه مقدارNULLبرمیگرداند. تونلهای packet خالص state مختص line ندارند و سازندهٔ آنها باید پیش از تعیین callbackها یا دسترسی به state تونل، نتیجه را بررسی کند.- انتظار میرود این تونلها callbackهای payload مربوط به packet را جایگزین کنند.
- callbackهای پیشفرض payload عمداً نامعتبرند:
LOGF(...)و سپسabortProgramNow(1)را فراخوانی میکنند تا فراموشکردن این جایگزینی با خطایی آشکار برنامه را متوقف کند:
static void packettunnelDefaultUpStreamPayload(tunnel_t *self, line_t *line, sbuf_t *payload)
{
LOGF("Unexpected call to default up stream payload for a packet tunnel, "
"this function must be overridden");
abortProgramNow(1);
}
Init، Est، Pause، ResumeوFinishهمان callbackهای عبوری استاندارد و همجهت چرخهٔ عمر را به ارث میبرند. هر تونل فقط زمانی یکی از آنها را جایگزین میکند که نقش دقیق handler آن، مطابق دستهبندی بالا، به ردکردن یک close ناممکن در نقش لنگر، forwarding صریح یا جذب انتهایی عمدی نیاز داشته باشد.
نمونهها: IpOverrider، IpManipulator، PingClient، PingServer و
WireGuardDevice.
برای تونلهای packet خالص:
- بیدلیل منطق چرخهی عمر line شبیه اتصال را اضافه نکنید.
-
lstate_size == 0بهمعنای «فقط packet line» نیست. یک تونل packet از نوعkNodeLayerAnything، lineهای عادی اتصال را نیز عبور میدهد. - برخی callbackهای پیشفرض عمداً برنامه را متوقف میکنند؛ دقیقاً بدانید کدامها را باید جایگزین کنید.
۲. پلهای متکی به packet line با tunnelCreate
تونلهای دیگری با tunnelCreate() معمولی ساخته میشوند، اما packet line مربوط
به worker را بهعنوان لنگر برای state پل در همان worker به کار میگیرند. این
تونلها از مرز زمان اجرای مبتنی بر packet و زمان اجرای مبتنی بر اتصال عبور
میکنند.
نمونهها و stateای که نگه میدارند:
| پل | نقش |
|---|---|
PacketsToStream | اتصال packet line به یک line سمت stream و مختص worker؛ همان line و یک read buffer را ذخیره میکند. |
StreamToPackets | اتصال یک stream line به packet line؛ stream line فعال و read buffer را بهازای هر worker ذخیره میکند. |
PacketsToConnection | انتقال ترافیک packet از طریق lwIP به lineهای عادی اتصال WaterWall در پشت سمت packet. |
PacketSplitStream | تقسیم ترافیک packet میان مسیرهای stream. |
state ذخیرهشده در اینجا نیز state پل و محلی worker است، نه state مختص اتصال. اگر در پشت ترافیک packet به رفتار قابلبستن و مختص اتصال نیاز دارید، بهجای بستن packet line، lineهای عادی را پشت آن بسازید یا مدیریت کنید.
مقداردهی اولیهی packet line
مقداردهی packet line با line اتصال متفاوت است.
در chainای که ابتدای آن یک node از نوع packet در layer 3 است، مدیر node
هنگام راهاندازی روی packet line هر worker یک Init در upstream میفرستد؛ یک
بار برای هر worker، نه یک بار برای هر packet. بنابراین Init مربوط به packet
line یک رویداد راهاندازی اولیهٔ worker است، نه بازشدن اتصال.
اما اگر پلی به packet lineها متکی باشد و ابتدای chain خودش node از نوع
layer 3 نباشد، Init خودکار رخ نمیدهد و Bridge باید آن را فعال کند.
StreamToPackets دقیقاً همین کار را در onStart() انجام میدهد: راهاندازی اولیه
را در صف همهٔ workerها میگذارد تا تونلهای سمت packet، Init خود را پس از
راهاندازی ببینند، نه از راه بازورودِ درجا به مسیرهای Init:
static void streamtopacketsQueueWorkerPacketInit(void *worker, void *arg1, void *arg2, void *arg3)
{
discard worker;
discard arg2;
discard arg3;
tunnel_t *t = arg1;
line_t *l = tunnelchainGetWorkerPacketLine(tunnelGetChain(t), getCurrentWorkerWID());
if (UNLIKELY(! lineCallWithRef(l, tunnelNextUpStreamInit, t)))
{
LOGF("StreamToPackets: worker packet line died during packet-side init");
abortProgramNow(1);
}
}
void streamtopacketsTunnelOnStart(tunnel_t *t)
{
for (wid_t wi = 0; wi < getWorkersCount(); wi++)
{
if (UNLIKELY(
sendWorkerMessageForceQueueWithCleanup(wi, streamtopacketsQueueWorkerPacketInit, NULL, t, NULL, NULL) !=
kWorkerMessageSubmitAccepted))
{
LOGF("StreamToPackets: failed to admit required packet-side Init on worker %u", (unsigned int) wi);
startupFailureRecord(1);
return;
}
}
}
بنابراین هنگام ساخت یا تغییر Bridge مربوط به packet، فرض نکنید packet line از
قبل بهصورت خودکار Init شده است. بررسی کنید آیا Init را مدیر node میفرستد، چون
ابتدای chain از نوع layer 3 است، یا تونل شما باید در onStart() آن را فعال کند.
جهت تونلهای جفت: قاعدهی PingClient / PingServer
نقش transform و جهت forward شدن callback دو تصمیم جدا هستند. Ping wire v2 دو نشست مستقل Echo دارد:
دادهٔ Client: PingClient -- Echo Request --> PingServer
PingClient <-- Echo Reply دقیق -- PingServer
دادهٔ Server: PingClient <-- Echo Request -- PingServer
PingClient -- Echo Reply دقیق --> PingServer
- مسیر upstream در
PingClientو مسیر downstream درPingServer، دادهٔ محلی خود را در Echo request نوع8قرار میدهند. همتای آنها request را در callback متناظر decode میکند و packet داخلی را در همان جهت callback ادامه میدهد. - Reply نوع
0مربوط به request دریافتشده، پیش از تحویل request اصلی به فرستنده بازمیگردد. Reply فقط نقش تأیید را دارد؛ فرستنده Reply منطبق را مصرف میکند و payload آن را تحویل نمیدهد. - correlation و جلوگیری از replay بر عهدهٔ یک tracker همگامشده و دارای ظرفیت محدود برای هر node از نوع Ping است. flow affinity دستگاه تعیین میکند کدام packet line یک callback را اجرا کند؛ WID آن line هویت correlation میان request و Reply نیست.
- دریافتکنندهٔ request دو buffer مجزا منتشر میکند. clone ساختهشده برای Reply بهسمت فرستندهٔ request برمیگردد، درحالیکه نسخهٔ اصلی decodeشده در همان جهتی ادامه مییابد که request از آن دریافت شده بود.
- بنابراین handler مربوط به request در upstream در
PingServer، clone مربوط به Reply را باtunnelPrevDownStreamPayload()و نسخهٔ اصلی decodeشده را باtunnelNextUpStreamPayload()میفرستد. handler مربوط به request در downstream درPingClient، clone را باtunnelNextUpStreamPayload()و نسخهٔ اصلی decodeشده را باtunnelPrevDownStreamPayload()میفرستد.
بازگرداندن خود request دریافتشده بهسمت فرستنده، بازتابی نامعتبر است. ارسال
Echo Reply دقیق و جداگانهای که در جهت مخالف ساخته شده، الزام wire protocol است.
به همین ترتیب، encapsulateکردن نسخهٔ اصلی در callback مربوط به upstream در
PingServer باعث میشود داده دو بار transform شود.
روش کار برای هر تونل packet:
- جریان packet را همراه با پیکانها رسم کنید.
- مرحلهی encode/decode را در جهت callback متناسب با جریان قرار دهید.
- فقط برای forward در upstream از
tunnelNextUpStreamPayload()و فقط برای forward در downstream ازtunnelPrevDownStreamPayload()استفاده کنید. - packet line را line مختص اتصال در نظر نگیرید.
- در runtime روی packet line تابع
lineDestroy()را صدا نزنید. - به تونل packet خالص چرخهی عمر stream-style اضافه نکنید، مگر آنکه طراحیاش واقعاً به آن نیاز داشته باشد.
تست تونلهای packet
تست تونل packet باید جهت واقعی موردنظر را پوشش دهد. توپولوژی استاندارد تست Ping یک chain مستقیم است:
TesterClient(packet-mode=true)
-> PingClient
-> PingServer
-> TesterServer(packet-mode=true)
درخواست upstream باید ابتدا Reply دقیق بگیرد و سپس پیش از رسیدن به
TesterServer بازیابی شود. دادهٔ جهت معکوس باید Echo request جداگانه باشد، Reply
دقیق بگیرد و پیش از رسیدن به TesterClient بازیابی شود. پوشش تستیای اضافه
کنید که اگر PingServer در upstream بهجای decode دوباره encapsulate کند، یک Reply
دو بار تحویل شود یا دادهٔ server در Reply مربوط به request اصلی قرار گیرد،
با شکست روبهرو شود.
قرارداد Finish در packet line پوشش تستی جداگانهای دارد و هر handler جدید
در سمت packet باید به آن افزوده شود:
tests/unittests/tunnels/abort/tunnels_abort_runtime_test.c، handlerهایFinishمربوط به adapterهای لنگر را واقعاً در یک فرایند فرزند فراخوانی میکند و الزام میکند فرایند فرزند بهجای بازگشت از handler، متوقف شود.tests/unittests/tunnels/packet_io/packetsender_orderly_shutdown_test.cثابت میکند وقتی یک payload بهشکل re-entrant یکFinishدر downstream تولید میکند،PacketSenderبدون نابودکردن packet line همان worker، timer در انتظار را لغو میکند و دستهٔ آمادهای شامل چند packet را متوقف میسازد.tests/line_ownership_policy_test.pyدر صورتی با شکست روبهرو میشود که یک فایلfin.cبه بازگشتی بدون توضیح تبدیل شود یا ثبت CTest مربوط به یکی از تستهای قرارداد حذف شود.
خلاصهٔ قواعد تغییرات مبتنی بر packet
- packet lineها را هنگام اجرا نبندید و هرگز روی آنها
lineDestroy()را فراخوانی نکنید. - شرط پس از اجرای
Finishبرای مالک که در بخش ۲ آمده، برای packet lineها صدق نمیکند. - هر handler و جهت مربوط به
Finishدر packet line را در یکی از سه نقش قرار دهید: anchor یک close ناممکن را باLOGFوabortProgramNow(1)رد میکند؛ transform میانی شفاف آن را در همان جهت forward میکند؛ و جذبکنندهٔ انتهایی عمدی توضیح میدهد چرا چیزی برای forwardکردن یا آزادکردن وجود ندارد. - رفتار
PacketSenderبه جهت بستگی دارد: downstream، close مجاز رسیده از یک مقصد packet line مانندBlackHoleفعال را جذب میکند، اما upstream همچنان fatal است.PacketReceiverنیز یک terminal absorber با رفتار مستند است. - handlerای که ممکن است هم packet line و هم line عادی دریافت کند، باید پیش از هر
cleanup بر اساس نقش دقیق آنها با
tunnelchainIsWorkerPacketLine()شاخهبندی کند. packettunnelCreate()فقط بهمعنایlstate_size == 0است. چند node ساختهشده با آن از نوعkNodeLayerAnythingهستند و lineهای عادی اتصال را نیز عبور میدهند؛ به همین دلیل callbackهای چرخهٔ عمر آن، callbackهای عبوری ساده باقی میمانند.- handler مربوط به
Finishکه کاری انجام نمیدهد باید دلیل آن را روشن کند:Finish/بستهشدن را forward کنید، منابع را آزاد کنید، نقض قرارداد را گزارش دهید، یا مستند کنید که هیچ state مختص line و هیچ جهت بعدی ندارید. - منطق عادی
Finishمختص اتصال را روی packet line اجرا نکنید، مگر آنکه قرارداد نقش دقیق handler صریحاً آن را الزام کند. - state مربوط به packet line را state محلی و مشترک worker در نظر بگیرید.
- routing context را scratch state تغییرپذیر worker بدانید، نه هویت پایدار session.
- درخواست checksum را فقط روی buffer همان packet نگه دارید؛ نگهداری یا تحویل packet نباید درخواست buffer دیگری را تغییر دهد.
- برای چرخهی عمر مختص flow، پشت سمت packet یک line عادی بسازید.
- تونل ساختهشده با
packettunnelCreate()هیچ state مختص line ندارد وlstate_size == 0است. - پلی که state را روی packet line متصل میکند، آن state را در طول پردازش تعداد زیادی packet همان worker نگه میدارد.
- اگر کد شما به Init مربوط به packet line وابسته است، منشأ این Init را بررسی کنید.
مهمترین اشتباهی که باید از آن دوری کنید:
استفاده از Packet Lineها بهگونهای که گویی Lineهای عادی اتصال با چرخهٔ عمر معمول
Init -> Payload -> Finish برای هر اتصالاند. چنین نیستند.
ادامه: بخش ۵: ساختار یک تونل و روند کار.