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

بخش ۵: ساختار یک تونل و روند کار

در این بخش می‌بینید قراردادهای بخش‌های ۱ تا ۴ در کدام فایل‌ها پیاده‌سازی می‌شوند. با شناخت مسئولیت هر فایل، می‌توانید تغییر را در جای درست انجام دهید: کوچک‌ترین تغییری که علت اصلی مشکل را کامل برطرف کند و قرارداد چرخه‌ی عمر را هم حفظ کند.

چیدمان دایرکتوری​

بیشتر تونل‌ها از ساختار زیر پیروی می‌کنند. این چیدمان واقعی TcpListener، TlsClient، ‏MuxClient و template است:

tunnels/MyTunnel/
CMakeLists.txt
description.md
include/MyTunnel/
interface.h # API عمومی WW_EXPORT و اعلان همهٔ Callbackها
structure.h # Structهای tstate/lstate، Enumهای اندازه و توابع کمکی
instance/
create.c # tunnelCreate()، تعیین Callbackها، Parse و اعتبارسنجی تنظیمات
node.c # فرادادهٔ Node: نوع، Flagها، گروه لایه، Padding و createHandle
chain.c # قلاب onChain (معمولاً کار را به پیش‌فرض می‌سپارد)
index.c # قلاب onIndex (معمولاً کار را به پیش‌فرض می‌سپارد)
prepair.c # قلاب onPrepare
start.c # قلاب onStart
stop.c # قلاب‌های Quiesce، Drain و Stop در lifecycle-v2
destroy.c # قلاب onDestroy: آزادسازی State و سپس tunnelDestroy()
api.c # ورودی API زمان اجرا (tunnelApi)
common/
helpers.c # توابع کمکی مشترک پروتکل/ماشین حالت
line_state.c # مقداردهی و نابودی State مختص Line
upstream/
init.c est.c payload.c pause.c resume.c fin.c
downstream/
init.c est.c payload.c pause.c resume.c fin.c
چیدمان واقعی در مقایسه با خلاصه‌ی قدیمی

دو نکته در عمل مهم است: همه‌ی تونل‌ها فایل instance/api.c را دارند و نام فایل hook آماده‌سازی به‌شکل prepair.c نوشته می‌شود؛ نام تابع نیز ...OnPrepair است. فایل‌های پایان در upstream/downstream نیز fin.c هستند، نه finish.c.

قرارگرفتن هر ترکیب {direction}/{event} در فایلی جدا باعث می‌شود هر callback کوچک بماند. منطق مشترک پروتکل و ماشین حالت در common/helpers.c قرار می‌گیرد تا فایل‌های callback خوانا بمانند. این قاعده را رعایت کنید؛ بازبین‌ها انتظار همین ساختار را دارند.

در QuicClient و QuicServer، سازنده، hookهای چرخه‌ی عمر و callbackهای هر جهت در همین چیدمان قرار دارند. هر node ورودی application یا carrier را صریح انتخاب می‌کند و مسیر داده‌ی wire تولیدشده، داده‌ی application رمزگشایی‌شده و فشار روی application را در common/output.c تعیین می‌کند. Internals/QuicCommon اعتبارسنجی تنظیمات، framing و موتور پروتکل را به اشتراک می‌گذارد؛ runtime آن در C به بخش‌های context و طول عمر، ورودی، جریان و timer تقسیم شده است. driver مشترک در طول callbackهای بازگشتی reference مربوط به line قرضی را نگه می‌دارد و نابودی موتور را به تعویق می‌اندازد؛ ارسال Init و Finish در callback همان جهتِ node قابل مشاهده می‌ماند.

مسئولیت هر فایل​

فایلمسئولیت
instance/node.cnode_t توصیف‌کننده‌ی نوع تونل را برمی‌گرداند: type، ‏version، ‏createHandle، ‏flags، ‏required_padding_left، ‏layer_group و محدودیت لایه‌ی همسایه‌ها.
instance/create.ctunnelCreate(node, sizeof(tstate), sizeof(lstate)) را فراخوانی می‌کند، دوازده callback جریان و hook‌های چرخه‌ی عمر را تنظیم می‌کند، تنظیمات JSON را parse و اعتبارسنجی می‌کند، منابع هر worker را تخصیص می‌دهد و در هر مسیر شکست پاک‌سازی می‌کند.
instance/chain.c / index.chook‌های onChain / onIndex. بیشتر تونل‌ها پیش‌فرض‌های framework را نگه می‌دارند. توپولوژی داخلی ایستایی که پیش از حل معلوم است در onChain تعریف می‌شود. تونلی که توپولوژی خصوصی‌اش به لایه‌های حل‌شده‌ی مجاور بستگی دارد، می‌تواند onSolvedTopology اختیاری را نیز در create.c تنظیم کند. این hook snapshot حل‌شده‌ی فعلی را دریافت می‌کند که لزوماً نهایی نیست. ممکن است چند بار فراخوانی شود و فقط و فقط هنگامی true برمی‌گرداند که توپولوژی را تغییر داده باشد؛ در این صورت snapshot دیگر معتبر نیست و مدیر پیش از فراخوانی hook‌ای دیگر دوباره مسئله را حل می‌کند. وقتی یک گذر کامل هیچ تغییری گزارش نکند، onIndex یک بار روی توپولوژی تغییرناپذیر اجرا می‌شود و می‌تواند لایه‌های نهاییِ حل‌شده را مصرف کند.
instance/prepair.c / start.c / stop.chook‌های onPrepare / onStart و hook‌های shutdown lifecycle-v2. راه‌اندازی اولیه را در onStart انجام دهید؛ پذیرش را در onQuiesceRequest ببندید؛ کارهای وابسته به worker را در onWorkerQuiesce جدا کنید؛ در onQuiesceWait منتظر تولیدکننده‌های خارجی بمانید؛ line‌های تحت مالکیت را در onWorkerStop تخلیه کنید؛ و منابع مشترک باقی‌مانده را در onStop آزاد کنید.
instance/destroy.chook onDestroy: تنظیمات، context‌های SSL، ‏pool‌ها و آرایه‌های thread-local را آزاد می‌کند و سپس tunnelDestroy(t) را فرا می‌خواند.
instance/api.cورودی runtime برای tunnelApi. یک پیام sbuf_t دریافت می‌کند، باید آن را بازیافت کند و یک api_result_t برگرداند.
common/line_state.cLinestateInitialize که از Init فراخوانی می‌شود و LinestateDestroy که ناحیه‌ی هم‌تراز line state را صفر می‌کند.
common/helpers.cمنطق مشترک parsing و framing پروتکل و توابع کمکی close/Finish که فایل‌های callback به کار می‌برند.
upstream/*.c، downstream/*.cهر فایل یک callback جریان را در بر دارد. آن‌ها را کوچک نگه دارید و منطق را به helpers.c بسپارید.
include/MyTunnel/structure.hتعریف mytunnel_tstate_t، ‏mytunnel_lstate_t، ‏enum‌های اندازه و توابع کمکی inline کوچک.
include/MyTunnel/interface.hprototype‌های exportشده‌ی ساخت/نابودی/API با WW_EXPORT و prototype همه‌ی callback‌ها.

Nodeها اعلان‌های کتابخانهٔ ww را از wwapi.h، مستقیم یا از مسیر headerهای interface/structure خود، دریافت می‌کنند؛ headerهای جزئی ww را جداگانه include نکنید. header انتخاب logger همچنان در پیاده‌سازی node به‌طور صریح include می‌شود. آن را به header عمومی کتابخانه اضافه نکنید تا logger مصرف‌کننده تغییر نکند.

تخصیص حافظه و شکست heap​

تخصیص heap با اندازهٔ معتبر در runtime از memoryAllocate*() و آزادسازی متناظر memoryFree*() می‌گذرد. کمبود حافظه در همین مرز با abortProgramNow(1) فرایند را فوراً پایان می‌دهد؛ این مسیر logging معمول، retry یا پاک‌سازی orderly ندارد.

فایل‌های اجرایی runtime هر ۲۰ overload سراسری تخصیص و آزادسازی C++17 را جایگزین می‌کنند: new و delete تکی و آرایه‌ای، شامل صورت‌های nothrow، ‏sized-delete و aligned. حافظهٔ عادی از memoryAllocate()/memoryFree() و حافظهٔ aligned از memoryAllocateAligned()/memoryFreeAligned() استفاده می‌کند. در نتیجه QUICHE و containerهای C++ لینک‌شده نیز تابع backend انتخاب‌شدهٔ mimalloc یا ALLOCATOR_BYPASS هستند. کمبود واقعی heap حتی در صورت nothrow کشنده است؛ ردشدن هندسهٔ aligned مستقل می‌ماند: صورت throwing، ‏std::bad_alloc می‌دهد و صورت nothrow، ‏nullptr برمی‌گرداند. اگر constructor استثنا بدهد، آزادسازی متناظر همان allocator انجام می‌شود.

فراخوانی مستقیم allocatorهای C و allocator اختصاصی کلاس، قرارداد خود را دارند و با این جایگزینی سراسری رهگیری نمی‌شوند. allocator اختصاصی FEC در TcpOverUdp همچنان حافظهٔ objectها، vectorهای تو‌در‌تو و control blockهای shared pointer را از WaterWall می‌گیرد. launcher مستقل و ابزارهای host زمان build این override runtime را دریافت نمی‌کنند.

برای فیلدهایی که باید رشتهٔ C دقیق داشته باشند، پیش از مصرف از jsonObjectFieldStringsAreExact(object, key) استفاده کنید. این helper همهٔ عضوهای هم‌نام، حتی کلیدهای تکراری، را از نظر NUL داخلی در نام یا مقدار رشته‌ای و UTF-8 نامعتبر بررسی می‌کند. cJSON شواهد NUL رمزگشایی‌شده را هنگام duplication و ساخت reference نگه می‌دارد؛ جایگزینی نام یا مقدار با setter فقط شواهد همان رشته را پاک می‌کند. این بررسی اختیاری است: حضور، نوع، بازه و نام تنظیمات شناخته‌شده همچنان مسئولیت مصرف‌کننده‌اند. QUIC آن را در Internals/QuicCommon/config.c اعمال می‌کند؛ loader عمومی config هنگام جایگزینی متغیرها مقدار خام نامعتبر را بدون شناخت نام تنظیمات QUIC حفظ می‌کند. serialization شواهد NUL رمزگشایی‌شده را حفظ نمی‌کند؛ بنابراین پیش از print و parse دوبارهٔ درخت اعتبارسنجی کنید.

capability‌های typed داخلی در برابر پیام‌های runtime​

برای typed capability‌ای که مستقیماً میان tunnel‌های سازگار و حاضر در یک chain ردوبدل می‌شود، از include/<Tunnel>/interface.h استفاده کنید. برای نمونه، UdpListener یک provider مربوط به endpoint پویای UDP را به‌شکل API مبتنی بر value-handle عرضه می‌کند: فراخواننده endpoint را روی worker مالکِ اعتبارسنجی‌شده باز، فعال، query و close می‌کند. capability باید مالکیت، وابستگی به worker، رفتار handle قدیمی و مسیر callback را تعریف کند و نباید یک line_t * قابل‌استفاده از مالک دیگر را در معرض قرار دهد.

چنین capability محلی را به instance/api.c تبدیل نکنید. آن فایل ورودی پیام runtime خارجی است: یک sbuf_t می‌گیرد، آن را بازیافت می‌کند و api_result_t برمی‌گرداند؛ بنابراین قرارداد متفاوتی برای admission، serialization و lifecycle دارد. typed capabilityٔ داخلی همچنان فراخوانی عادی C است و باید chain معمول callback‌های tunnel را حفظ کند.

مالکیت metadata در address context​

address_context_t.optional_flags.detected_protocols دو دستهٔ منطقی metadata اختیاری دارد. bit‌های متعلق به Router/sniffer، protocol‌های application مشاهده‌شده را ثبت می‌کنند؛ bit‌های کنترل مسیر یک دستور داخلیِ یک‌بارمصرف، مانند endpoint مذاکره‌شدهٔ SOCKS UDP، را حمل می‌کنند. Router هنگام آغاز پنجرهٔ sniffing جدید باید addresscontextClearDetectedProtocols() را صدا بزند تا فقط bit‌های protocol مشاهده‌شده پاک شوند. addresscontextClearOptionalFlags() برای reset کامل زمانی است که caller واقعاً context را جایگزین می‌کند. adapter خروجیِ تعیین‌شده metadata کنترل مسیر را دقیقاً یک‌بار مصرف و پس از setup پاک می‌کند؛ rule‌ها هرگز نباید این bit‌ها را protocol مشاهده‌شده بدانند.

تحت lifecycle-v2، پیام‌های worker، ‏timer‌ها، ‏callback‌های WIO و درخواست‌های async DNS متعلق به framework پیش از نابودی component تسویه می‌شوند؛ بنابراین این context‌ها می‌توانند مستقیماً tunnel_t * را حمل کنند، مشروط به اینکه callback مربوطه worker مالکِ صریح خود را نیز اعتبارسنجی کند و همان را به‌کار بگیرد. این یک قانون عمومی برای طول عمر نیست: ریشه‌ی callback خارج از چرخه‌ی عمر worker، مانند thread خواننده‌ی device، باید پیش از بازپس‌گیری وضعیت تونل یا device، پذیرش محلی را ببندد و با سازوکاری مختص مالک مانند quiescence_gate_t منتظر پایان عملیات فعال بماند.

‏node.c: معرفی node​

تونل در node.c خود را به runtime معرفی می‌کند. نمونه‌ی زیر متعلق به TcpListener است و الگوی مناسبی برای یک adapter به شمار می‌رود:

node_t nodeTcpListenerGet(void)
{
const char *type_name = "TcpListener";
node_t node = {
.type = stringDuplicate(type_name),
.hash_type = calcHashBytes(type_name, stringLength(type_name)),
.version = 0001,
.createHandle = tcplistenerTunnelCreate,
.flags = kNodeFlagChainHead,
.required_padding_left = 0,
.layer_group = kNodeLayer4,
.layer_group_next_node = kNodeLayerAnything,
.layer_group_prev_node = kNodeLayerAnything,
.can_have_next = true,
.can_have_prev = true,
};
return node;
}

فیلدهایی که بیشتر تنظیم می‌کنید، همگی در ww/objects/node.h تعریف شده‌اند:

فیلدمفهوم
type / hash_typeرشته‌ی مورد استفاده در "type" فایل JSON و hash ازپیش‌محاسبه‌شده‌ی آن.
createHandleتابع TunnelCreateHandle که instance را می‌سازد.
flagsیک یا چند node_flags: ‏kNodeFlagChainHead، ‏kNodeFlagChainEnd، ‏kNodeFlagNoChain، ‏kNodeFlagSingleton.
required_padding_leftظرفیت padding سمت چپ که تونل اجازه دارد داده را در آن prepend کند؛ بخش ۳ را ببینید.
layer_groupدسته‌ی لایه‌ای که این node روی آن کار می‌کند: kNodeLayer3 (packet)، ‏kNodeLayer4 (stream)، ‏kNodeLayerAnything (پشتیبانی از هر دو) یا kNodeLayerNone. توجه: kNodeLayerSameAsNext، ‏kNodeLayerSameAsPrev، ‏kNodeLayerOppositeNext و kNodeLayerOppositePrev برای layer_group ممنوع‌اند و فقط در layer_group_next_node / layer_group_prev_node مجازند.
layer_group_next_node / layer_group_prev_nodeدسته‌ی لایه‌ی لازم برای همسایه‌ی بعدی/قبلی (kNodeLayer3، ‏kNodeLayer4، ‏kNodeLayerAnything، ‏kNodeLayerSameAsNext، ‏kNodeLayerSameAsPrev، ‏kNodeLayerOppositeNext، ‏kNodeLayerOppositePrev یا kNodeLayerNone). توجه: flag‌های Opposite باید با یک دسته‌ی لایه‌ی پایه ترکیب شوند، مانند `kNodeLayerAnything
can_have_next / can_have_prevمجاز بودن همسایه در هر سمت (true/false).

گروه‌های لایه و اعتبارسنجی دقیق توپولوژی​

در زمان راه‌اندازی chain، NodeManager از طریق validateTunnelChains و حل‌گرِ کران‌دار و یکنواخت قیدهای لایه (nodeLayerSolveChain)، قابلیت‌های node و قواعد لایه را با سخت‌گیری اعمال می‌کند:

  • قیدهای قابلیت: اگر node مقدار .can_have_next = false (یا .can_have_prev = false) را مشخص کند، وجود همسایه در آن سمت بی‌درنگ راه‌اندازی را شکست می‌دهد. node‌های خارج از chain که با kNodeFlagNoChain علامت خورده‌اند نباید همسایه‌ی مجاور داشته باشند.
  • اعتبارسنجی شکل metadata: مقدار layer_group باید مشخص و مجاز باشد: kNodeLayerNone، ‏kNodeLayer3، ‏kNodeLayer4 یا kNodeLayerAnything. flag‌های نسبی رو‌به‌جلو (kNodeLayerSameAsNext، ‏kNodeLayerOppositeNext) فقط در layer_group_prev_node و flag‌های نسبی رو‌به‌عقب (kNodeLayerSameAsPrev، ‏kNodeLayerOppositePrev) فقط در layer_group_next_node مجازند. flag‌های Opposite باید همیشه یک دسته‌ی لایه‌ی پایه را نیز داشته باشند، برای مثال kNodeLayerAnything | kNodeLayerOppositePrev.
  • انتشار قید در دامنه‌ی یال‌ها: زمان اجرا گراف جهت‌داری از یال‌ها می‌سازد که اتصال callback میان تونل‌های مجاور را نمایش می‌دهد. دامنه‌ی هر یال ({L3}، ‏{L4} یا {L3, L4}) با توجه به نیازهای لایه‌ی node‌های مشارکت‌کننده و همسایه‌ها مقدار اولیه می‌گیرد و رابطه‌های SameAs و Opposite به‌شکل یکنوا تا رسیدن به نقطه‌ی ثابت منتشر می‌شوند. اگر دامنه‌ی یالی به {} برسد، راه‌اندازی فوراً و با پیام روشنی که هر دو node و سمت‌های آن‌ها را مشخص می‌کند شکست می‌خورد.
  • SameAs شرطی در برابر Opposite سخت‌گیرانه: قیدهای نسبی SameAs (kNodeLayerSameAsPrev، ‏kNodeLayerSameAsNext) شرطی‌اند: اگر سمت مرجع وجود نداشته باشد، رابطه‌ی برابری افزوده نمی‌شود و flag‌های جای‌گذاری node (kNodeFlagChainHead، ‏kNodeFlagChainEnd، ‏kNodeFlagNoChain) تعیین می‌کنند که قرارگرفتن در مرز مجاز است یا نه. در مقابل، قیدهای Opposite (kNodeLayerOppositePrev، ‏kNodeLayerOppositeNext) سخت‌گیرانه‌اند و همیشه حضور هر دو سمت مشارکت‌کننده را لازم دارند.
  • رابطه‌های بین‌تونلیِ نگه‌داری‌شده در state: برای node‌هایی که مقصد callback آن‌ها هنگام اجرا به‌صورت پویا تعیین می‌شود، مانند تونل‌های جفت‌شده‌ی Bridge، تونل‌ها رابطه‌های منطقی لایه را مستقیماً با tunnelchainRegisterLayerRelation() روی chain ثبت می‌کنند. رابطه‌های ثبت‌شده در زمان اجرای حل‌گر دامنه‌ی یال‌های مشارکت‌کننده را به هم وابسته می‌کنند؛ برای مثال یال prev در Bridge A را با یال prev در Bridge B و یال next در Bridge A را با یال next در Bridge B برابر می‌گیرند. این رابطه‌ها پس از ترکیب chain‌ها با tunnelchainCombine() نیز باقی می‌مانند.
  • Bridge‌های ثابت و نسبی: Bridge‌هایی که سمت ثابت دارند، به‌جای ادعای kNodeLayerAnything نیاز لایه‌ی مشخصی را روی یال‌های مجاورشان اعلام می‌کنند. ConnectionToPackets در سمت prev لایه‌ی L4 و در سمت next لایه‌ی L3 است؛ PacketsToConnection در prev لایه‌ی L3 و در next لایه‌ی L4 است؛ StreamToPackets نیز در prev لایه‌ی L4 و در next لایه‌ی L3 است. Bridge‌های پویای دوطرفه مانند WireGuardDevice رابطه‌های Opposite اعلام می‌کنند تا بدون فروریختن زودهنگام chain‌های فرعی میانیِ هنوز حل‌نشده، دامنه‌ی یال‌های مخالف را به هم وابسته کنند.
  • تشخیص و راه‌اندازی chain packet: chain فقط وقتی برای worker‌ها packet line تخصیص می‌دهد و contains_packet_node = true را تنظیم می‌کند که دست‌کم یک یال به {L3} محدود شده باشد، نیاز یک node یا یال به L3 ثابت شده باشد، یا رابطه‌ی Opposite وجود داشته باشد. پیام‌های Init برای packet line‌های پایدار فقط وقتی در زمان اجرا در صف قرار می‌گیرند که یال بعدیِ سر chain به {L3} حل شده باشد.
  • معنای شکست: هر شکست در اعتبارسنجی یا حل، پیام خطای بحرانی (LOGF) ثبت می‌کند و با startupFailureRecord(1) شکست راه‌اندازی را ثبت می‌کند؛ در نتیجه WaterWall config را رد و بازگردانی تمیز راه‌اندازی را اجرا می‌کند.

flag‌های node​

flagمفهوم
kNodeFlagNoneهیچ قابلیت node انتخاب نشده است؛ به‌تنهایی استفاده شود.
kNodeFlagChainHeadمی‌تواند chain را آغاز کند؛ ingress adapter.
kNodeFlagChainEndمی‌تواند chain را پایان دهد؛ egress adapter.
kNodeFlagNoChainبدون حضور در chain کار می‌کند؛ مانند node مربوط به database/user-auth.
kNodeFlagSingletonدر کل فایل config فقط یک instance از آن مجاز است.

kNodeFlagNone را فقط وقتی به‌کار ببرید که هیچ flag دیگری لازم نیست. اگر قابلیت‌هایی انتخاب شده‌اند، flagهای همان قابلیت‌ها را بدون اضافه‌کردن kNodeFlagNone ترکیب کنید.

ABI node‌های خارجی​

node پویا باید symbol تاریخی waterwallNodeLifecycleAbiVersion() را export و WW_EXTERNAL_NODE_ABI_VERSION، فعلاً 7، را برگرداند. loader پیش از resolve یا فراخوانی getter، library دارای descriptor گمشده یا ناسازگار را رد می‌کند. library خارجی را با header‌های فعلی دوباره build کنید: ساختار socket_filter_option_t به‌صورت by-value ارسال می‌شود و دیگر فیلد shared_balance_table را ندارد. وضعیت balancing متعلق به filterهای خصوصی SocketManager است، نه ساختار عمومی تنظیمات. نام قدیمی WW_EXTERNAL_NODE_LIFECYCLE_ABI_VERSION alias سازگاری source برای همین gate کامل است و معنای callbackهای lifecycle تغییری نکرده است.

درخواست checksum در API عمومی متعلق به sbuf_t است؛ فیلد و accessorهای قدیمی line حذف شده‌اند. node خارجی باید با headerهای فعلی دوباره build شود و از sbufSetRecalculateChecksum() و sbufGetRecalculateChecksum() روی buffer همان packet استفاده کند. gate موجود، library کامپایل‌شده با ABI قبلی را پیش از اجرای getter رد می‌کند.

‏create.c: ساخت instance​

create.c تنها جایی است که تونل در آن تخصیص می‌یابد. الگوی برگرفته از TlsClient:

  1. اجرای tunnel_t *t = tunnelCreate(node, sizeof(tstate), sizeof(lstate));
  2. بلافاصله t را بررسی کنید و اگر ساخت شکست خورده بود NULL برگردانید.
  3. هر دوازده callback جریان و hook‌های چرخه‌ی عمر را تنظیم کنید.
  4. تک‌تک تنظیمات را از node->node_settings_json بخوانید و اعتبارسنجی کنید.
  5. منابع هر worker را تخصیص دهید، برای مثال یک context ‏SSL به‌ازای هر worker.
  6. در صورت هر خطای بعدی، پاک‌سازی و خروج: state ناقص و تونل را نابود و NULL برگردانید.

tunnelCreate()، ‏adapterCreate() و packettunnelCreate() طبق قرارداد ممکن است NULL برگردانند. اندازه‌ی نامعتبر یا سرریزکرده‌ی state و شکست تخصیص حافظه از علت‌های آن هستند. پیش از بررسی نتیجه، هرگز callback‌ای تنظیم نکنید و tunnelGetState() را فرا نخوانید.

نظم پاک‌سازی واقعی و ارزشمند است. شکل خلاصه‌شده‌ی آن چنین است:

tunnel_t *tlsclientTunnelCreate(node_t *node)
{
tunnel_t *t = tunnelCreate(node, sizeof(tlsclient_tstate_t), sizeof(tlsclient_lstate_t));
if (! t)
{
return NULL;
}

configureTunnelCallbacks(t);

tlsclient_tstate_t *ts = tunnelGetState(t);
const cJSON *settings = node->node_settings_json;

if (! getandvalidateSniSetting(ts, settings))
{
goto fail;
}
// ... تنظیمات اعتبارسنجی‌شدهٔ بیشتر ...

if (! createSslContextPool(&ts->threadlocal_ssl_contexts, /* ... */))
{
goto fail;
}

return t;

fail:
tlsclientTunnelstateDestroy(ts);
tunnelDestroy(t);
return NULL;
}

مراحل shutdown در lifecycle-v2​

مالکیت چرخه‌ی عمر صریح و دارای context است. shutdown پردازه این مرحله‌ها را برای هر تونل در همه‌ی chain‌های بارگذاری‌شده اجرا می‌کند. NodeManager درون هر chain از ترتیب ذخیره‌سازی استفاده می‌کند و تضمینی نیست این ترتیب source-to-tail باشد:

  1. onQuiesceRequest روی main thread اجرا می‌شود، پذیرش محلی را می‌بندد، درخواست توقف producer‌ها را صادر می‌کند و نباید منتظر بماند.
  2. onWorkerQuiesce روی هر worker مالک اجرا می‌شود و timer‌ها و watcher‌های همان worker و handle‌های مختص مؤلفه را که روی همان worker صف شده‌اند جدا می‌کند.
  3. onQuiesceWait پس از quiesceشدن همه‌ی worker‌ها روی main thread اجرا می‌شود و ریشه‌های callback خارجیِ از قبل پذیرفته‌شده را join یا detach می‌کند.
  4. onWorkerStop هنگام drain روی هر worker مالک اجرا می‌شود. هر مالک source یا line عادی داخلی، هنگام رسیدن نوبتش inventory مرجع خود را می‌بندد؛ hook دیگری نباید فرض کند مالک همسایه قبلاً اجرا شده یا خالی‌بودن registry قرضی را الزام کند.
  5. onStop پس از تخلیه‌شدن همه‌ی worker‌ها روی main thread اجرا می‌شود و منابع مشترک باقی‌مانده را آزاد می‌کند.
  6. onDestroy فقط وقتی روی main thread اجرا می‌شود که دیگر هیچ worker، ‏message، ‏line یا producer خارجی نتواند به instance ارجاع دهد.

همه‌ی callback‌ها یک ww_lifecycle_context_t فقط‌خواندنی دریافت می‌کنند. بازگردانی ساخت، برچیدن فرزند خصوصی به‌دست مالک و راه‌اندازی مجدد محلی device، ‏scope‌های مستقل خود و سیاست بستن abortive را دارند. component‌ها نباید با pollکردن وضعیت سراسری shutdown، ‏scope را حدس بزنند. حذف زنده‌ی configuration یک scope پشتیبانی‌شده‌ی چرخه‌ی عمر نیست.

از helper‌های JSON پروژه، مانند getStringFromJsonObject، getBoolFromJsonObjectOrDefault و getIntFromJsonObjectOrDefault استفاده کنید و برای config نادرست پیام روشنی با LOGF بنویسید. config اشتباه باید هنگام راه‌اندازی با خطایی روشن شکست بخورد، نه اینکه در زمان اجرا بی‌صدا باقی بماند.

‏structure.h: ‏tstate و lstate​

دو struct مربوط به state و اندازه‌هایشان در structure.h تعریف می‌شوند. تنظیمات و فیلدهای runtime را مانند TlsClient به‌وضوح گروه‌بندی کنید:

typedef struct tlsclient_tstate_s
{
// تنظیمات (یک بار در create.c Parse می‌شوند)
char *sni;
char *alpn;
bool verify;
// State زمان اجرا که میان همه‌ی Lineها مشترک است
SSL_CTX **threadlocal_ssl_contexts; // یکی به‌ازای هر Worker
} tlsclient_tstate_t;

typedef struct tlsclient_lstate_s
{
SSL *ssl;
BIO *rbio;
BIO *wbio;
buffer_queue_t bq;
bool handshake_completed;
bool resources_released;
} tlsclient_lstate_t;

آرایه‌های مختص worker، مانند threadlocal_ssl_contexts، در tstate قرار می‌گیرند و فقط با هویت worker که صریحاً اعتبارسنجی شده index می‌شوند: lineGetWID(line) در callback یک line، ‏worker->wid در callback پیام worker، یا getCurrentWorkerWID() بررسی‌شده هنگامی که قرارداد callback حضور روی event worker را تضمین می‌کند. هرگز آن‌ها را با getWID() خام index نکنید. state مختص هر اتصال در lstate قرار می‌گیرد.

‏line_state.c: مقداردهی و نابودی​

line state در Init مقداردهی و دقیقاً یک بار نابود می‌شود. تابع destroy باید کل ناحیه‌ی هم‌تراز را zero کند تا assert مربوط به آزادشدن line در حالت Debug، یعنی debugAssertZeroBuf، برقرار باشد؛ بخش ۲ را ببینید:

void templateLinestateInitialize(template_lstate_t *ls)
{
// State این اتصال را آماده کنید
}

void templateLinestateDestroy(template_lstate_t *ls)
{
memoryZeroAligned32(ls, tunnelGetCorrectAlignedLineStateSize(sizeof(template_lstate_t)));
}

قواعد بخش ۲، در همان جایی که کد را می‌نویسید:

  • در Init مقداردهی کنید؛ callback‌های بعدی وجود state را فرض می‌کنند.
  • فقط یک بار destroy کنید؛ پس از LinestateDestroy()، ‏state را مرده و zero‌شده در نظر بگیرید.
  • پس از نابودی state هیچ فیلدی از آن نخوانید.
  • یک boolean به نام initialized اضافه نکنید، مگر آنکه قرارداد صریح و مستندِ نقش line یا callback، وجود state پیش از Init عادی را لازم بداند.

API تغییر رمز AuthenticationServer​

عملیات writable به نام change-user-password روی worker فراخوان اجرا می‌شود و مجوز مستقل و دقیق می‌خواهد؛ مدیر رمز معمولاً فقط get-user و این عملیات را دارد. مجوز monitor، editor، creator یا credential تولیدکننده این اختیار را نمی‌دهد. بدنه دقیقاً id غیرصفر canonical به‌صورت رشتهٔ ده‌دهی، if_match با ۴۸ نویسهٔ hex کوچک و password غیرخالی با بایت‌های دقیق UTF-8 دارد. alias، فیلد اضافی/تکراری، NUL، encoding نامعتبر و مقدار تو‌در‌تو رد می‌شوند. پیش از ساخت درخت JSON، سقف بدنه ۳۲ KiB، عمق فقط شیء ریشه و ۱۶ مقدار lexical با احتساب کلیدها را اعمال کنید. زیر state lock، token پیش از lookup حساب بررسی می‌شود. حساب غیرفعال/منقضی/تمام‌شده قابل ویرایش است؛ رمز برابر هم config_revision را یک بار افزایش می‌دهد.

مشتق‌سازی عادی credential و انتقال lock-held را با لایهٔ object به اشتراک بگذارید. یک کپی owned رمز و hash/UUID/key ثابت را بیرون shared lock آماده و هر primitive را یک بار محاسبه کنید. alignment و جفت allocation/release را حفظ کنید؛ user کامل یا native lock را کپی نکنید. parser، prepared، scratch، result و هم‌پوشانی اوج را در working allowance حساب کنید. request و رشتهٔ decoded/prepared را در هر نتیجه پاک و plaintext قدیمی و material موقت معادل secret را secure-zero کنید. diagnostic ثابت نباید بدنه، credential یا رشتهٔ دلخواه ذخیره‌شده را چاپ کند.

ترتیب try-lock برابر server state write -> collection write -> target user configuration write است؛ stats lock، I/O و انتظار worker وارد transaction نمی‌شوند. چهار ایندکس credential را برای برخورد با حساب دیگر بررسی و کلیدهای خود target را مجاز بدانید. تبدیل متن به UUID، UUID به متن، UUID به UUID دیگر و تغییر case همان UUID باید validity/membership را درست نگه دارد. تغییر متن لزوماً هویت پروتکلیِ بدون تغییر را باطل نمی‌کند. رمز قدیمی را فقط تا ۳۲ KiB به‌علاوه یک بایت terminator بررسی کنید؛ رمز importشدهٔ بزرگ‌تر بدون mutation به resource_limit می‌رسد. رشته‌های نامرتبط یا کل جدول را پیمایش نکنید. hash همچنان average-case است و crypto یا probing بدترین‌حالت تضمین وقفه‌پذیری ندارند.

پیاده‌سازی object، storage/import/copy را در users.c، hash index را در users_index.c، بازهٔ Allowed-IP را در users_allowed_ip.c، transaction/preparation را در users_update.c و users_prepared.c، projection عمومی را در users_public.c و accounting/migration زمان اجرا را در users_runtime.c نگه می‌دارد. type و helper خصوصی با قفل مشخص در users_internal.h می‌مانند. چهار credential map با کلید بایتی ثابت یک template خصوصی typed دارند؛ ایندکس نام، ID، pointer و interval معنای اختصاصی خود را حفظ می‌کنند. state کاربر، preparation و snapshot کپی یک مقدار owned و aligned برای credential دارند؛ native lock آماده همیشه جدا ساخته می‌شود.

نبود مالکیت credential قدیمی، count ناممکن ایندکس یا نبود ظرفیت رزروشدهٔ insertion خرابی داخلی است: diagnostic ثابت fatal بدهید و مستقیم خاتمه دهید. duplicate و refusal عادی ظرفیت همچنان نتیجهٔ typed پیش از commit هستند. حذف از ایندکس خراب نباید پیش از خاتمه allocation یا rebuild انجام دهد.

ظرفیت هر credential map برای همه کاربران فعال، حتی بدون UUID، رزرو می‌شود. تعویض، occupancy نهایی را در ظرفیت موجود می‌سنجد و از erase بدون advance و insertion رزروشدهٔ بدون رشد استفاده می‌کند؛ reserve/rehash ندارد. همه قفل‌ها و بررسی uniqueness، ظرفیت، budget و deadline پیش از mutation کامل شوند. ACK موجودِ ۲۵۶ بایتی را با revision/token همین transaction در pool ازپیش‌پذیرفته‌شده بسازید. پس از آخرین deadline check، انتقال credential و انتشار membership commit غیرقابل‌شکست است و revision زیر state یک بار افزایش می‌یابد. پس از commit allocation، render، snapshot تازه یا خطای deadline ندهید. refusal هیچ state منطقی را تغییر نمی‌دهد؛ خرابی invariant در مرحلهٔ برگشت‌ناپذیر fatal است.

آدرس/ID کاربر، count/order/slot، تنظیمات/Allowed-IP، زمان ایجاد/انقضا/اولین استفاده، ترافیک، stats_revision، checkpoint و reservation حفظ می‌شوند. collision به credentials_exist و خرابی backend به password_change_failed نگاشت typed دارد. پیش از mutation پایان ظرفیت revision را رد کنید. موفقیت فقط حافظه را تأیید می‌کند؛ save دوره‌ای/نهایی و refresh عادی مستقل‌اند. ACK همان schema آزموده‌شده در envelope حداقل ۱۰۲۴ بایت است. خطای node یعنی همین invocation commit نشده؛ پاسخ گم‌شده نتیجهٔ نامعلوم دارد. read تازه token می‌دهد، اما فیلد عمومی یا revision جلوتر اثبات رمز نیست. ارسال مجدد صریح رمز مطلوبِ نگه‌داری‌شده می‌تواند آن را در commit تازه برقرار کند؛ token تازه و retry خودکار، request record، replay-success یا crash recovery اضافه نکنید.

قواعد مختص HTTP​

اگر روی HttpClient یا HttpServer کار می‌کنید، محدودیت‌های زیر که از source code به دست آمده‌اند برقرارند:

  • هدف WaterWall پشتیبانی از HTTP/1.x و HTTP/2 تک‌stream است. HTTP/3 اضافه نکنید. بیش از یک stream منطقی HTTP/2 را پشتیبانی نکنید. اگر peer، ‏stream‌های اضافه باز کرد، مطابق طراحی موجود آن‌ها را با رویکردی محافظه‌کارانه رد یا نادیده بگیرید.
  • در ارتقای h2c با nghttp2_session_upgrade2()، ‏stream شماره‌ی 1 همان request اصلی است که upgrade شده است. client نباید request ساختگی دومی را روی stream ‏1 submit کند و server نباید response ساختگی و جعلی را خودکار روی stream ‏1 بسازد.
  • در h2c همراه با request body، ارتقای request اصلی HTTP/1.1 که body دارد در طراحی تک‌stream به‌شکل امن پشتیبانی نمی‌شود. توصیه می‌شود client آن را رد کند؛ server نیز upgrade مربوط به request دارای body در HTTP/1.1 را نادیده بگیرد یا رد کند.
  • برای پایان تمیز (Clean finish)، دریافت یک Finish در جهت upstream برای سمت request ممکن است ابتدا نیازمند byte‌های پایانی HTTP باشد. آن‌ها را در حالی بفرستید که reference مربوط به line را نگه داشته‌اید، سپس state محلی را نابود و Finish واقعی را forward کنید؛ الگوی عمومی byte‌های پایانی در بخش ۲. پس از چنین Finish تمیزی، ‏line state مربوط به HTTP را زنده نگه ندارید، مگر آنکه عمداً برای این هدف reference مربوط به line را نگه داشته باشید.

روند پیاده‌سازی​

برای هر قابلیت یا اصلاح جدید، این ترتیب را دنبال کنید. ابتدا مخزن را بررسی کنید؛ فقط هنگامی پرسشی مشخص بپرسید که یک نیاز اثرگذار، انتخاب سازگاری یا مرز scope را نتوان از کد و مستندات تعیین کرد. برای جزئیات کم‌خطر پیاده‌سازی، به‌جای واگذارکردن تصمیم به نگه‌دارنده، فرض‌های معقولی در نظر بگیرید و ثبت کنید.

  1. جریان chain را رسم کنید. Up-end و down-end را علامت بزنید و مشخص کنید کدام جهت callback مالک transform است.
  2. مالک line را مشخص کنید. کدام تونل line را می‌سازد و نابود می‌کند؟
  3. تونل موجود و همسایه‌های مستقیمش را بخوانید.
  4. قراردادها را بخوانید: ww/net/line.h، ‏ww/net/tunnel.h و ww/net/chain.c.
  5. اگر framing یا prepend در کار است، فایل‌های ww/bufio/shiftbuffer.h و buffer_pool.h و همچنین padding موجود در node.c تونل‌های نزدیک را بخوانید.
  6. الگوهای مرتبطِ تونل‌های بالغ را مقایسه کنید، اما به‌جای کپی کورکورانه، آن‌ها را با قرارداد، caller‌ها و همسایه‌ها بسنجید.
  7. کوچک‌ترین اصلاح کامل علت ریشه‌ای را پیاده‌سازی کنید که قابلیت ترکیب را حفظ کند. برای کوچک‌کردن diff، عیب شناخته‌شده را نگه ندارید، منطق ناامن را تکرار نکنید و فقط نشانه‌ی مشکل را وصله نکنید.
  8. تصمیم بگیرید آیا پوشش regression متمرکز لازم است. برای رفتار تازه، باگی تکرارپذیر و مستعد بازگشت، حالت مرزی مهم یا تغییر قرارداد معمولاً لازم است؛ به‌ویژه وقتی test بتواند با وارونه‌شدن جهت شکست بخورد. اگر پوشش مستقیم موجود کافی است از همان استفاده کنید و برای کار مکانیکی یا صرفاً مستندسازی، test رفتاری ساختگی نسازید.
  9. با preset ساخت و test‌های مرتبط اعتبارسنجی کنید؛ بخش ۶ را ببینید.
از انتزاع‌سازی‌های مبتنی بر حدس دوری کنید

یک لایه‌ی عمومی تازه به‌ندرت راه‌حل اینجا است. قرارداد تثبیت‌شده‌ی چرخه‌ی عمر را حفظ کنید و یک اصلاح مستقیم و کامل را ترجیح دهید. اگر به نظر می‌رسد تونلی به مفهوم تازه‌ای در چرخه‌ی عمر نیاز دارد، دست نگه دارید و نیاز را در برابر قرارداد core و همه‌ی caller‌های تحت تأثیر بررسی کنید؛ یک تونل مرجع به‌تنهایی درستی ساختار قدیم یا جدید را ثابت نمی‌کند.


ادامه: بخش ۶: build، تست و review.

ممیزی مدیریت AuthenticationServer​

ممیزی مدیریت از صف node-local و file writer با خطای صریح استفاده می‌کند و از لاگر diagnostic جداست. رویداد committed را همان مرز افزایش revision زیر قفل state و پیش از ارسال پاسخ منتشر کنید؛ rejected فقط وقتی مجاز است که همان تلاش commit نشده باشد. metadata محدود actor/gateway و bit نام فیلدهای مجاز را کپی کنید؛ body، مقدار فیلد یا credential وارد رکورد نشود. mutex کوتاه صف فقط metadata را حفاظت می‌کند؛ انتظار برای ظرفیت، allocation، ساخت JSON و I/O فایل داخل آن ممنوع است. writer مستقل JSONL محدود می‌سازد، append/flush/rotation را بررسی می‌کند و بعد از خطای فایل، رکوردهای صف‌شده و بعدی را dropped می‌شمارد. اشباع صف یا خطای writer سلامت ممیزی را تغییر می‌دهد، نه نتیجهٔ عملیات حساب. status حتی در حالت غیرفعال object محدود audit را با نمونه‌برداری مستقل دارد. مرجع node تنظیمات، retention و محدودیت دوام را تعریف می‌کند.

قرارداد سازندهٔ UserController​

سازندهٔ داخلی SOCKS/Trojan/VLESS/HTTP/WireGuard بلافاصله پس از ساخت موفق controller و پیش از prepare، usercontrollerSetPayloadModeResolver(controller, owner, resolver) را bind می‌کند. resolver خالص و owner-worker است؛ callback، تغییر مالکیت یا ساخت tunnel ندارد. UserController هیچ structure خصوصی node پروتکل را include نمی‌کند. instance دستی payload-mode برابر stream، datagram یا dest_context->protocol لازم دارد و با resolver هم‌زمان مجاز نیست. گزینهٔ آخر فقط به‌صورت صریح، پروتکل مقصد را در Init می‌خواند: فقط TCP یعنی stream و فقط UDP یعنی datagram؛ مقدار غایب، مبهم یا پشتیبانی‌نشده line را رد می‌کند. mode در Init هر جهت ذخیره و در promotion حفظ می‌شود. normal line الزاماً stream نیست و پس از تبدیل transport ممکن است destination UDP نوع payload این hop را توصیف نکند؛ آن حالت به mode ثابت یا resolver نیاز دارد. packet line وارد controller نمی‌شود. WireGuard همچنان احراز هویت peer تنظیم‌شدهٔ فعلی را دارد.

UserController و SpeedLimit machinery مشترک shaping دارند؛ controller هیچ SpeedLimit داخلی ایجاد نمی‌کند. صف stream exact range move و ordinary fallback دارد و datagram کامل می‌ماند. جزئیات بودجه و handoff در بخش‌های ۲ و ۳ است.

قراردادهای سرور احراز هویت​

کد API-only create/update/password کنار operation قرار دارد و ACK مشترک پیش از commit ساخته می‌شود؛ typed commit و بررسی scratch باقی می‌مانند. renderer رشتهٔ عادی را chunk می‌کند و زمان را در ورود، حداکثر هر ۲۵۶ بایت emitted و پیش از موفقیت می‌سنجد. empty page و prefix قابل‌جاگیری فقط یک بار measure و همان view immutable نوشته می‌شود. صفحات credential-bearing producer با viewهای redacted مدیریتی یکی نیستند. جست‌وجوی تکی producer با password یا digest نیز هنگام کپی محدودشده، قفل collection را نگه می‌دارد. پیش از کپی رشته‌های دلخواه، اندازهٔ دقیق JSON فشرده باید در سقف payload پاسخ منهای header فریم جا شود. رکورد بزرگ‌تر با response-too-large و encoding نامعتبر رشتهٔ ذخیره‌شده با user-json-serialize-failed رد می‌شود.

بارگذاری دیتابیس AuthenticationServer و AddNewUser تولیدکننده باید کل بازهٔ بایت را پیش از import بررسی کنند: بایت اضافی غیر از whitespace، ‏UTF-8 نامعتبر، NUL خام یا decodeشده و کلید تکراری پس از decode رد می‌شوند. reader دیتابیس طول واقعی را نگه می‌دارد؛ عمق فعلی cJSON و layoutهای پشتیبانی‌شده حفظ می‌شوند و سقف بایت/تعداد مقدار envelopeهای API جایگزین آن‌ها نمی‌شود. configLexicalParseBoundedQuietWithCleanup() پیش از حذف درخت کامل ولی ردشده، رشته‌های تحت مالکیت را پاک می‌کند؛ cleanup درخت پذیرفته‌شده با caller است. jsonWipeStrings() از مالکیت cJSON پیروی می‌کند و value/childهای reference و کلیدهای ثابت را تغییر نمی‌دهد. درخت‌های ناقصی که خود cJSON هنگام خطای syntax حذف می‌کند و کپی‌های transport تحت مالکیت دیگر، بیرون این تضمین هستند.

مرز operation و timer در APIServer​

APIServer، line عادی را قرض می‌گیرد و هرگز آن را نابود نمی‌کند. ابتدا selector عمومی را میان همهٔ nodeهای سطح بالا حل کنید و سپس مجوز دقیق config/node/operation را بسنجید؛ فیلتر مجوز ابهام نام را حذف نمی‌کند. آماده‌سازی هر مجوز را به descriptor تغییرناپذیر وصل می‌کند و زمان اجرا پس از freeze رجیستری همان را به‌کار می‌برد. نبودن مجوز دقیق forbidden می‌دهد. read_only فقط metadata است و مجوز، rollback یا ایمنی retry را اعمال نمی‌کند.

مهلت frame از رسیدن نخستین fragment، شامل ماندن در صف، آغاز می‌شود. برای arm مجدد timer منتشرشده، زمان cacheشدهٔ loop مالک را تازه و wtimerReset() را اجرا کنید؛ مهلت مطلق زودترین کار را حفظ کنید و callback pending قدیمی را حذف کنید. امتناع reset از مسیر cleanup اتصال را می‌بندد و با add دوم پوشانده نمی‌شود. callback پیش از progress، slot خود را پاک می‌کند؛ quiescence timer را لغو می‌کند. خطاهای timeout و invalid_encoding در gateway اتصال را می‌بندند.

ساخت VmessClient و ماژول VmessCommon​

VmessClient و VmessServer دارای kNodeFlagBlocksSplice و padding چپ لازم صفر هستند. codec مشترک در Internals/VmessCommon است و هیچ endpoint به دیگری وابسته نیست. ساخت کلاینت یک کلید ثابت مشتق از UUID آماده می‌کند، نوع دقیق setting و alias غیرمبهم را بررسی می‌کند و اعتبارسنجی command/framing را با codec مشترک دارد. کلاینت middle عادی L4 با پشتیبانی prev و next است. برای DNS محلی، DomainResolver متعلق به node پیش از آن درج می‌شود؛ TLS/HTTP همچنان در nodeهای transport موجود تنظیم می‌شوند. ساخت فقط کلاینت به AuthenticationClient، UserController، replay table، vector اعتبارنامه یا pump احراز هویت نیاز ندارد. codec در NONE/zero هم فعال می‌ماند. قرارداد آغاز کار، مالکیت و حدود buffer معمولی در بخش‌های ۲ و ۳ است.

نرمال‌سازی شناسهٔ VMess خصوصی VmessCommon است. UUID خط‌تیره‌دار/فشرده همچنان دقیق parse می‌شود؛ رشتهٔ UTF-8 غیرخالی حداکثر ۳۰ بایت با UUIDv5، namespace صفر و SHA-1 provider نگاشت می‌شود. trim، تغییر بزرگی حروف دلخواه، نرمال‌سازی Unicode یا پذیرش UUID-sized خراب به‌عنوان شناسهٔ کوتاه مجاز نیست. تکراری‌های محلی سرور با UUID نرمال‌شده مقایسه می‌شوند. password در AuthenticationClient managed همچنان UUID canonical است؛ password خام را اسکن یا index UUID مشترک را تغییر ندهید.

security=auto کلاینت یک‌بار هنگام ساخت و با قابلیت اثبات‌شدهٔ CPU/OS برای AES و carryless multiplication و موجود بودن cipher در provider مستقیم resolve می‌شود؛ در نبود اثبات شتاب، ChaCha موجود ترجیح دارد. cipher صریح ناموجود ساخت را رد می‌کند. TLS کشف و none/zero خودکار انتخاب نمی‌شود. allowed-security سرور فهرست دقیق، غیرخالی و یکتای حالت واقعی بدنه است؛ فهرست بدون حالت قابل استفاده برای فرمان‌های فعال، ساخت را رد می‌کند. گزینه‌های جدید type/encoding/duplicate را دقیق بررسی می‌کنند و negotiation bit تازه ندارند.

هر دو endpoint عملیات فقط‌خواندنی stats را هنگام ساخت ثبت و پس از تسویهٔ callbackهای پذیرفته‌شده، پیش از آزادسازی target، unregister می‌کنند. TunnelApi تاریخی به‌تنهایی عملیات عمومی نمی‌سازد. فقط شیء خالی JSON محدود پذیرفته می‌شود؛ schema ثابت تنظیمات مؤثر و counterهای کلی را با uint64 رشتهٔ ده‌دهی دقیق برمی‌گرداند. counter منتشرشده همگام خوانده می‌شود؛ API worker هرگز line زنده یا جدول مقصد owner-only را پیمایش نمی‌کند. occupancy احراز/cache/replay سرور snapshot محدود و nonblocking دارد و در رقابت lock، busy برمی‌گرداند. deadline، بودجهٔ کار و charge واقعی result pool رعایت می‌شود. snapshot هنگام ترافیک تقریبی است. بایت برنامه پوشش packetaddr ندارد و بایت wire framing را شامل می‌شود. reset/mutation یا scheduler آمار اضافه نمی‌شود. verbose از sampling محدود node و دلیل ثابت، بدون اعتبارنامه، کلید، plaintext یا label کاربر/مقصد استفاده می‌کند.