بخش ۵: ساختار یک تونل و روند کار
در این بخش میبینید قراردادهای بخشهای ۱ تا ۴ در کدام فایلها پیادهسازی میشوند. با شناخت مسئولیت هر فایل، میتوانید تغییر را در جای درست انجام دهید: کوچکترین تغییری که علت اصلی مشکل را کامل برطرف کند و قرارداد چرخهی عمر را هم حفظ کند.
چیدمان دایرکتوری
بیشتر تونلها از ساختار زیر پیروی میکنند. این چیدمان واقعی 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.c | node_t توصیفکنندهی نوع تونل را برمیگرداند: type، version، createHandle، flags، required_padding_left، layer_group و محدودیت لایهی همسایهها. |
instance/create.c | tunnelCreate(node, sizeof(tstate), sizeof(lstate)) را فراخوانی میکند، دوازده callback جریان و hookهای چرخهی عمر را تنظیم میکند، تنظیمات JSON را parse و اعتبارسنجی میکند، منابع هر worker را تخصیص میدهد و در هر مسیر شکست پاکسازی میکند. |
instance/chain.c / index.c | hookهای onChain / onIndex. بیشتر تونلها پیشفرضهای framework را نگه میدارند. توپولوژی داخلی ایستایی که پیش از حل معلوم است در onChain تعریف میشود. تونلی که توپولوژی خصوصیاش به لایههای حلشدهی مجاور بستگی دارد، میتواند onSolvedTopology اختیاری را نیز در create.c تنظیم کند. این hook snapshot حلشدهی فعلی را دریافت میکند که لزوماً نهایی نیست. ممکن است چند بار فراخوانی شود و فقط و فقط هنگامی true برمیگرداند که توپولوژی را تغییر داده باشد؛ در این صورت snapshot دیگر معتبر نیست و مدیر پیش از فراخوانی hookای دیگر دوباره مسئله را حل میکند. وقتی یک گذر کامل هیچ تغییری گزارش نکند، onIndex یک بار روی توپولوژی تغییرناپذیر اجرا میشود و میتواند لایههای نهاییِ حلشده را مصرف کند. |
instance/prepair.c / start.c / stop.c | hookهای onPrepare / onStart و hookهای shutdown lifecycle-v2. راهاندازی اولیه را در onStart انجام دهید؛ پذیرش را در onQuiesceRequest ببندید؛ کارهای وابسته به worker را در onWorkerQuiesce جدا کنید؛ در onQuiesceWait منتظر تولیدکنندههای خارجی بمانید؛ lineهای تحت مالکیت را در onWorkerStop تخلیه کنید؛ و منابع مشترک باقیمانده را در onStop آزاد کنید. |
instance/destroy.c | hook onDestroy: تنظیمات، contextهای SSL، poolها و آرایههای thread-local را آزاد میکند و سپس tunnelDestroy(t) را فرا میخواند. |
instance/api.c | ورودی runtime برای tunnelApi. یک پیام sbuf_t دریافت میکند، باید آن را بازیافت کند و یک api_result_t برگرداند. |
common/line_state.c | LinestateInitialize که از 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.h | prototypeهای 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:
- اجرای
tunnel_t *t = tunnelCreate(node, sizeof(tstate), sizeof(lstate)); - بلافاصله
tرا بررسی کنید و اگر ساخت شکست خورده بودNULLبرگردانید. - هر دوازده callback جریان و hookهای چرخهی عمر را تنظیم کنید.
- تکتک تنظیمات را از
node->node_settings_jsonبخوانید و اعتبارسنجی کنید. - منابع هر worker را تخصیص دهید، برای مثال یک context SSL بهازای هر worker.
- در صورت هر خطای بعدی، پاکسازی و خروج: 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 باشد:
onQuiesceRequestروی main thread اجرا میشود، پذیرش محلی را میبندد، درخواست توقف producerها را صادر میکند و نباید منتظر بماند.onWorkerQuiesceروی هر worker مالک اجرا میشود و timerها و watcherهای همان worker و handleهای مختص مؤلفه را که روی همان worker صف شدهاند جدا میکند.onQuiesceWaitپس از quiesceشدن همهی workerها روی main thread اجرا میشود و ریشههای callback خارجیِ از قبل پذیرفتهشده را join یا detach میکند.onWorkerStopهنگام drain روی هر worker مالک اجرا میشود. هر مالک source یا line عادی داخلی، هنگام رسیدن نوبتش inventory مرجع خود را میبندد؛ hook دیگری نباید فرض کند مالک همسایه قبلاً اجرا شده یا خالیبودن registry قرضی را الزام کند.onStopپس از تخلیهشدن همهی workerها روی main thread اجرا میشود و منابع مشترک باقیمانده را آزاد میکند.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 1submit کند و 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 را نتوان از کد و مستندات تعیین کرد. برای جزئیات کمخطر پیادهسازی، بهجای واگذارکردن تصمیم به نگهدارنده، فرضهای معقولی در نظر بگیرید و ثبت کنید.
- جریان chain را رسم کنید. Up-end و down-end را علامت بزنید و مشخص کنید کدام جهت callback مالک transform است.
- مالک line را مشخص کنید. کدام تونل line را میسازد و نابود میکند؟
- تونل موجود و همسایههای مستقیمش را بخوانید.
- قراردادها را بخوانید:
ww/net/line.h، ww/net/tunnel.hوww/net/chain.c. - اگر framing یا prepend در کار است، فایلهای
ww/bufio/shiftbuffer.hوbuffer_pool.hو همچنین padding موجود درnode.cتونلهای نزدیک را بخوانید. - الگوهای مرتبطِ تونلهای بالغ را مقایسه کنید، اما بهجای کپی کورکورانه، آنها را با قرارداد، callerها و همسایهها بسنجید.
- کوچکترین اصلاح کامل علت ریشهای را پیادهسازی کنید که قابلیت ترکیب را حفظ کند. برای کوچککردن diff، عیب شناختهشده را نگه ندارید، منطق ناامن را تکرار نکنید و فقط نشانهی مشکل را وصله نکنید.
- تصمیم بگیرید آیا پوشش regression متمرکز لازم است. برای رفتار تازه، باگی تکرارپذیر و مستعد بازگشت، حالت مرزی مهم یا تغییر قرارداد معمولاً لازم است؛ بهویژه وقتی test بتواند با وارونهشدن جهت شکست بخورد. اگر پوشش مستقیم موجود کافی است از همان استفاده کنید و برای کار مکانیکی یا صرفاً مستندسازی، test رفتاری ساختگی نسازید.
- با 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 کاربر/مقصد استفاده میکند.