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

بخش ۳: buffer‌ها، padding و shift buffer‌ها

اگر روی framing داده‌ها، اضافه‌کردن header، بازنویسی درجا یا تخصیص حافظه‌ی کاری تغییر می‌دهید، باید sbuf_t، ‏buffer pool‌ها و قرارداد padding سمت چپ در کل chain را بشناسید. اشتباه در این بخش ممکن است باعث شود تونل با یک چینش درست کار کند، اما با عوض‌شدن chain حافظه را خراب کند.

مراجع اصلی: ww/bufio/shiftbuffer.h، ‏ww/bufio/buffer_pool.h و فیلد required_padding_left در ww/objects/node.h.

ساختار sbuf_t​

payload‌های WaterWall در sbuf_t، مخفف shift buffer، جابه‌جا می‌شوند؛ buffer‌ای دارای padding که می‌توان نقطهٔ شروع دادهٔ آن را جابه‌جا کرد. header آن دقیقاً ۳۲ بایت است و ناحیهٔ داده نیز روی مرز ۳۲ بایتی هم‌تراز می‌شود تا copy‌های AVX2 هم‌تراز بمانند:

struct sbuf_s
{
uint32_t curpos; // Offset ابتدای Payload فعلی درون buf[]
uint32_t len; // طول Payload فعلی بر حسب بایت
uint32_t capacity; // کل تخصیص buf[]؛ در طول عمر بافر ثابت است
uint16_t l_pad; // Padding چپ رزروشده؛ پس از ساخت ثابت است
uint16_t flags;
uint8_t buf[]; // داده با هم‌ترازی ۳۲ بایتی
};

برای درک بهتر، یک cursor را درون فضایی در نظر بگیرید که اندازه‌ی آن پس از تخصیص ثابت می‌ماند:

   |<-- left capacity -->|<------- payload (len) ------->|<-- writeable tail -->|
+---------------------+-------------------------------+----------------------+
buf[0] curpos curpos+len capacity
|<------ l_pad ------>|
(reserved padding)

در شکل، left capacity ظرفیت سمت چپ، payload داده‌ی معتبر، writeable tail انتهای قابل‌نوشتن و reserved padding فضای رزروشده برای padding است.

  • هنگام افزودن داده به ابتدا (prepend یا shift به چپ)، curpos به چپ می‌رود؛ هنگام مصرف داده از ابتدای buffer، به راست حرکت می‌کند.
  • len تعداد بایت‌های معتبر payload از محل curpos به بعد است.
  • ظرفیت سمت چپ (sbufGetLeftCapacity = curpos) تعداد بایت‌هایی است که هنوز می‌توانید با shift به چپ به ابتدای buffer بیفزایید.
  • بخش قابل‌نوشتن انتهایی (sbufGetMaximumWriteableSize = capacity - curpos) مقدار فضایی است که از cursor به بعد می‌توان به آن دسترسی داشت. توجه کنید که این مقدار، فضای خالی پس از payload فعلی نیست؛ برای افزودن extra بایت باید آن را با len + extra مقایسه کنید.

توابع دسترسی اصلی​

فراخوانیمقدار بازگشتی
sbufGetLength(b)طول فعلی payload یا len.
sbufGetRawPtr(b) / sbufGetMutablePtr(b)pointer ابتدای payload یا buf + curpos.
sbufGetLeftCapacity(b)تعداد بایت‌های در دسترس برای prepend یا curpos.
sbufGetMaximumWriteableSize(b)تعداد بایت‌های قابل دسترسی از cursor یا capacity - curpos.
sbufGetTotalCapacity(b)کل فضای تخصیص‌یافته که ثابت است.
sbufGetLeftPadding(b)مقدار اولیه‌ی l_pad.

خواندن و نوشتن​

برای خواندن و نوشتن فیلدهای integer در header، هم helper با الزام alignment داریم و هم helper بدون این الزام:

sbufWriteUI16(b, port);            // نوشتن هم‌تراز ۱۶ بیتی در Cursor
sbufWriteUnAlignedUI32(b, value); // نوشتن ناهم‌تراز ۳۲ بیتی
uint16_t v; sbufReadUI16(b, &v); // خواندن هم‌تراز
sbufWrite(b, src, n); // کپی خام n بایت در Cursor
sbufConsume(b, n); // کاهش طول Payload به‌اندازه‌ی n بایت از انتها
sbufShiftRight(b, n); // جلو‌بردن Cursor و مصرف n بایت از ابتدا

درخواست ترمیم checksum در هر packet​

بیت kSbufFlagRecalculateChecksum در sbuf_t.flags یعنی همان packet یک درخواست انجام‌نشده برای ترمیم کامل checksum دارد. با sbufSetRecalculateChecksum(buf, requested) آن را تنظیم یا پاک کنید و با sbufGetRecalculateChecksum(buf) بخوانید. مقدار false تضمین معتبر بودن checksum نیست؛ ممکن است checksum عمداً نامعتبر باشد. setter فقط همین بیت را تغییر می‌دهد. درخواست true فقط برای buffer معمولی با بایت‌های مقیم حافظه معتبر است؛ علامت‌گذاری wrapper از نوع splice خطای برنامه‌نویسی است و در Debug با assert رد می‌شود.

  • تخصیص تازه، sbufReset() و بازیافت معمولی در pool درخواست را پاک می‌کنند.
  • sbufDuplicateTo()، sbufDuplicate() و sbufDuplicateByPool() درخواست کل payload را مستقل کپی می‌کنند. منبع بدون درخواست، درخواست قبلی مقصد را پاک می‌کند. اگر preflight ظرفیت در sbufDuplicateTo() رد شود، بایت‌ها، هندسه و flagهای مقصد تغییر نمی‌کنند.
  • sbufReserveSpace() درخواست را همراه payload حفظ می‌کند؛ پس از رشد فقط pointer بازگشتی معتبر و در مالکیت caller است. مسیر بدون رشد نیز درخواست را تغییر نمی‌دهد.
  • sbufWriteBuf() و copyهای عمومی فقط بایت‌ها را کپی می‌کنند. move، slice و استخراج بازه برای چند بایت metadata مختص packet استنباط نمی‌کنند؛ buffer تازهٔ یک بازه بدون درخواست شروع می‌شود و منبع درخواست خود را نگه می‌دارد.
  • concat، append و merge درخواست موجود مقصد را حتی هنگام رشد حفظ می‌کنند و درخواست منبع را با آن OR نمی‌کنند. تغییر cursor، طول یا نوشتن بایت نیز خودکار درخواست را تغییر نمی‌دهد؛ caller مسئول پیامد آن برای checksum است.
  • سازندهٔ packet کامل یا خروجی segmentation/fragmentation باید درخواست هر خروجی را طبق نیاز همان packet تعیین کند. ترمیم را پیش از از دست رفتن مرز packet کامل کنید یا درخواست را صریحاً به یک خروجی کامل منتقل کنید.
  • wrapperهای splice بدون درخواست می‌مانند. materialization، flagهای نمایش و مالکیت pipe یا درخواست checksum را به buffer معمولی منتقل نمی‌کند؛ بازیافت splice مالکیت انحصاری pipe را حفظ می‌کند.

درخواست همراه مالکیت buffer نگه‌داری یا بین workerها منتقل می‌شود. پس از forward یا recycle، sender دیگر حق خواندن یا تغییر buffer، از جمله flagهای آن، را ندارد.

افزودن header در ابتدا با sbufShiftLeft​

دلیل وجود l_pad این است که تونل بتواند header پروتکل را بدون تخصیص مجدد یا جابه‌جایی payload با memmove در ابتدای آن قرار دهد. کافی است cursor را به چپ ببرید و در فضای تازه‌آزادشده بنویسید:

// برای Header جا رزرو کنید و Cursor را به‌اندازه‌ی header_len بایت عقب ببرید.
assert(sbufGetLeftCapacity(buf) >= header_len);
sbufShiftLeft(buf, header_len); // curpos -= header_len; len += header_len

// اکنون Header را در ابتدای جدید بنویسید.
sbufWriteUI16(buf, htons(payload_len));
// ... بقیهٔ Header را پر کنید ...

sbufShiftLeft با assert بررسی می‌کند که ظرفیت سمت چپ کافی باشد:

static inline void sbufShiftLeft(sbuf_t *const b, const uint32_t bytes)
{
assert(sbufGetLeftCapacity(b) >= bytes);
b->curpos -= bytes;
b->len += bytes;
}

اگر assert فعال شود، یا در نسخهٔ Release از ابتدای buffer به چپ‌تر بروید، بیش از بودجهٔ padding خود prepend کرده‌اید؛ موضوع بخش بعد.

قرارداد padding: ‏required_padding_left​

هر تونل فقط می‌تواند به padding سمت چپی تکیه کند که در node.c خود اعلام کرده است:

.required_padding_left = kMyProtocolHeaderSize,

هنگام نهایی‌شدن chain، runtime مقدار required_padding_left همهٔ node‌های chain را جمع می‌کند و همین مقدار padding سمت چپ را در buffer‌های تحویلی رزرو می‌کند؛ tunnelchainInsert مقدار sum_padding_left را افزایش می‌دهد و tunnelchainFinalize تابع globalstateUpdateAllocationPadding را صدا می‌زند. به بیان دیگر:

ظرفیت سمت چپ در دسترس تونل هنگام runtime، برابر مجموع بودجهٔ padding تونل شما و همه‌ی تونل‌هایی است که از آن به adapter سازنده‌ی buffer نزدیک‌ترند. اگر بایت‌هایی را prepend کنید که اعلام نکرده‌اید، بودجهٔ تونل دیگری را خرج کرده‌اید؛ بودجه‌ای که ممکن است در chain‌ای دیگر وجود نداشته باشد.

به همین دلیل یک تونل framing که «روی سیستم من کار می‌کند» ممکن است با قرارگرفتن node دیگری پیش از آن حافظه را خراب کند. هر مقداری را که prepend می‌کنید اعلام کنید.

قواعد:

  • فقط زمانی sbufShiftLeft() را فراخوانی کنید که sbufGetLeftCapacity() دست‌کم به‌اندازه‌ی header موردنیاز باشد.
  • هر prepend را در محدوده‌ی required_padding_left اعلام‌شده‌ی همین تونل نگه دارید.
  • اگر باید بیش از بودجه‌ی خود prepend کنید، required_padding_left را متناسب افزایش دهید یا به‌جای shiftکردن، یک buffer تازه بسازید؛ بخش بعد را ببینید.

buffer pool‌ها​

برای buffer‌های معمول در زمان اجرا مستقیماً از malloc استفاده نکنید. buffer pool محلی هر worker امکان استفادهٔ دوباره از حافظه‌های تخصیص‌یافته را فراهم می‌کند و padding درست را نیز در اختیار می‌گذارد. از طریق هر line می‌توان به pool دسترسی داشت:

buffer_pool_t *pool = lineGetBufferPool(line);     // Pool مربوط به Worker این Line

sbuf_t *big = bufferpoolGetLargeBuffer(pool); // بافر کاری بزرگ
sbuf_t *small = bufferpoolGetSmallBuffer(pool); // بافر کاری کوچک

// ... از بافر استفاده کنید ...

bufferpoolReuseBuffer(pool, buf); // بازگرداندن به Pool
lineReuseBuffer(line, buf); // میان‌بُر: Pool مربوط به Worker همان Line

pool‌ها مختص هر worker هستند. هر line دقیقاً به یک worker تعلق دارد که با lineGetWID مشخص می‌شود؛ پس همیشه pool همان line را به کار ببرید و هرگز از pool worker دیگری استفاده نکنید. buffer‌های گرفته‌شده از pool، ‏padding سمت چپ رزروشدهٔ chain را از قبل دارند؛ بنابراین جای درستی برای ساخت payload‌ای هستند که قرار است چیزی به ابتدای آن افزوده شود.

اگر برای append به buffer‌ای بزرگ‌تر از buffer فعلی نیاز دارید، به‌جای پیاده‌سازی دستی رشد buffer از sbufReserveSpace() استفاده کنید. این تابع فقط در صورت نیاز حافظه را دوباره تخصیص می‌دهد و داده را copy می‌کند.

مالکیت buffer​

خطاهای مربوط به طول عمر buffer به‌اندازهٔ خطاهای طول عمر line رایج‌اند. قواعد:

  • callback مالکیت buffer‌ای را که به آن می‌دهید تحویل می‌گیرد. پس از tunnelNextUpStreamPayload(t, line, buf) یا شکل downstream آن، دیگر مالک buf نیستید. آن را نخوانید، دوباره استفاده نکنید و آزاد نکنید.
  • اگر buffer را نگه می‌دارید، مالک آن هستید. مسئولید در نهایت با bufferpoolReuseBuffer / lineReuseBuffer آن را برای استفادهٔ دوباره به pool برگردانید یا به callback دیگری تحویل دهید.
  • در مسیر خطایی که هنوز مالک buffer هستید، پیش از close آن را به pool یا line برگردانید. اجازه ندهید buffer نشت کند و پس از مرگ line به آن دست نزنید.
  • اگر callback نوع re-entrant، ‏line را از بین برده است، یعنی lineCallWithRef() / lineCallWithRefWithBuf() مقدار false داده، فقط buffer‌هایی را به pool برگردانید که همچنان در اختیار دارید؛ برای این کار به state مردهٔ line دست نزنید.

buffer منتقل‌شده به line task​

فراخوانی lineScheduleTaskWithBuf() یا lineScheduleDelayedTaskWithBuf() در همهٔ نتیجه‌ها، حتی RejectedSettled همگام، مالکیت buffer را منتقل می‌کند. فراخواننده پس از call نباید آن را بخواند، destroy یا recycle کند. اگر task اجرا شود مالکیت به callback task می‌رسد؛ در غیر این صورت scheduler پس از notification اختیاری آن را settle می‌کند. cleanup در context همان owner event worker—از جمله teardown متصل به owner—buffer را به pool برمی‌گرداند و foreign/unbound cleanup آن را destroy می‌کند.

cancellation buffer را دریافت یا مالک نمی‌شود. پیش از آزادسازی داخلی و در حالی اجرا می‌شود که reference فیزیکی scheduler برقرار است. callback ‏NULL فقط notification را حذف می‌کند؛ refusal، مرگ line، quiescence و teardown همچنان buffer را دقیقاً یک بار settle می‌کنند.

در build‌های Debug، ‏BUFFER_WONT_BE_REUSED(x) buffer را با یک نسخهٔ تازه عوض و نسخهٔ اصلی را نابود می‌کند تا pointer قدیمی به buffer قبلی سریعاً شناسایی شود. دیدن این macro در تونل مرجع یعنی «از این نقطه به بعد نباید دوباره به این buffer دست زد».

نمونه‌ی کامل prepend​

همهٔ نکات را کنار هم بگذاریم: تونلی برای framing که در مسیر upstream طول payload را به‌صورت big-endian و در ۲ بایت ابتدای آن می‌نویسد و در node.c مقدار .required_padding_left = 2 را اعلام کرده است:

void myTunnelUpStreamPayload(tunnel_t *t, line_t *l, sbuf_t *buf)
{
const uint16_t payload_len = (uint16_t) sbufGetLength(buf);

// دو بایت Padding چپ اعلام کرده‌ایم؛ پس جا شدن این داده تضمین شده است.
assert(sbufGetLeftCapacity(buf) >= sizeof(uint16_t));
sbufShiftLeft(buf, sizeof(uint16_t));
sbufWriteUI16(buf, payload_len); // در صورت نیاز Helper ترتیب بایت پروژه را به‌کار ببرید

// بافر را به upstream تحویل دهید؛ دیگر مالک آن نیستیم.
tunnelNextUpStreamPayload(t, l, buf);
}

در سمت downstream، تونل جفت header را با sbufReadUI16 و sbufShiftRight می‌خواند و حذف می‌کند، سپس با tunnelPrevDownStreamPayload آن را forward می‌کند.

اشتباه‌های رایج در کار با buffer​

  • افزودن داده در ابتدا بدون اعلام padding و تکیه بر ظرفیت چپی که فقط در یک چیدمان خاص chain وجود دارد.
  • استفاده از sbufShiftLeft با ظرفیت ناکافی در سمت چپ؛ assert در Debug به شما کمک می‌کند، اما نسخهٔ Release حافظه را خراب خواهد کرد.
  • خواندن یا استفادهٔ دوباره از buffer پس از تحویل آن به callback مربوط به forwarding.
  • نشت buffer در مسیر error یا close.
  • استفاده از pool یک worker دیگر به‌جای worker متعلق به line.
  • درنظرگرفتن sbufGetMaximumWriteableSize به‌عنوان «فضای خالی پس از payload»؛ این مقدار فضای موجود از cursor است، پس ابتدا طول فعلی را از آن کم کنید.

ادامه: بخش ۴: packet line‌ها و تونل‌های packet.

صف و تحویل UserController​

صف stream و datagram می‌تواند ordinary یا splice باشد. طول از sbufGetLength() و split دقیق stream از sbufMoveRangeTo() می‌آید؛ pipe خصوصی مستقل و fallback عادی حفظ می‌شوند. datagram هرگز split نمی‌شود؛ طول صفر یک بار و در جای FIFO خود، بدون نرخ یا سهمیه، تحویل می‌شود. سقف عمومی ۶۵٬۵۳۵ بایت و سقف کوچک‌تر پروتکل برقرار است.

بودجهٔ مجموع دو جهت هر line برابر 1 MiB منطقی، 2 MiB تخصیص و 256 buffer و دو ticket است؛ هر user در همان client 4 MiB، 8 MiB، 1024 buffer و 1024 ticket؛ و کل AuthenticationClient برابر 64 MiB، 128 MiB، 16384 buffer و 16384 ticket. این‌ها سقف منابع‌اند. صف و ticket lazy هستند. budget محلی cross-thread نمی‌شود؛ reservation مشترک قفل و rollback صریح دارد. output آماده یا جداشده تا handoff یا discard همچنان جزو هزینه است. مقصد split و continuation لازم پیش از commit رزرو می‌شوند؛ preview رزرو اعتبار نیست. پس از commit موفق هیچ کار fallible پیش از callback نیست.

رد stream buffer را بازیافت و line را می‌بندد؛ overflow در datagram فقط ورودی تازه را حذف می‌کند و FIFO پذیرفته‌شده سالم می‌ماند. رد timer/continuation لازم هر دو mode را می‌بندد. buffer تحت مالکیت با pool همان line بازیافت می‌شود و buffer واگذارشده هرگز محلی بازیافت نمی‌شود. هویت reservation شامل runtime/ticket یکتا است تا release دیرهنگام به user حذف‌شده و دوباره‌ساخته‌شده با همان ID هزینه کم نکند. هزینهٔ کل client تا آزاد شدن buffer قدیمی باقی می‌ماند.

حامل و نگهداری خام QUIC​

carrier-mode در QuicClient/QuicServer صریح است. datagram هر Payload حامل را به یک دیتاگرام کامل QUIC بدون پوشش اضافی نگاشت می‌کند. stream طول چهار‌بایتی unsigned با ترتیب big-endian و سپس بدنه دارد. parser مشترک، header ثابت چهار‌بایتی و بدنهٔ ناقص محدود دارد؛ طول صفر یا بالاتر از ۱٬۴۷۲ را پیش از allocation بدنه رد می‌کند. fragmentation، coalescing و reentry سری‌شده پشتیبانی می‌شوند. نوع adapter برای تشخیص خودکار حامل بررسی نمی‌شود. سمت برنامه بایت‌ها را حفظ می‌کند، نه تعداد Payload.

ورودی QUICHE فقط در مدت فراخوانی قرضی است. packet خروجی و plaintext decodeشده در eventهای owned مستقل با سقف بایت و entry stage و پس از driver تحویل می‌شوند. بافرها از pool دقیق line با padding کامل chain هستند. frame خروجی prefix خودش را می‌سازد؛ هر دو نود padding اضافی صفر و kNodeFlagBlocksSplice دارند.

پیش‌فرض هر backlog برابر ۲ MiB و ۱۰۲۴ entry است. retention برنامهٔ queued و فعال را با send پذیرفته‌شده ولی تأییدنشدهٔ engine، شامل بایت opening، با هم حساب کنید. chunk ارسال حداکثر ۱۶ KiB است و ledger به‌ترتیب FIFO offset، طول و charge allocation را نگه می‌دارد؛ فقط entry کاملاً تأییدشدهٔ جلوی صف credit را آزاد می‌کند. ACK جزئی یا خارج از ترتیب از gap قبلی عبور نمی‌کند. charge allocation متعلق به WaterWall برای هر backlog حداکثر دو برابر بودجهٔ بایت است. overflow ارتباط را می‌بندد و بایت قابل‌اعتماد را بی‌صدا حذف نمی‌کند؛ این سقف، محدودیت کل heap/RSS در QUICHE نیست.

پنجرهٔ ثابت stream و connection از ۱۶ KiB تا ۱۶ MiB و حداکثر بودجهٔ دریافت است؛ autotuning خاموش است. فقط stream برنامهٔ صفر وجود دارد: سرور یک bidirectional ورودی، کلاینت صفر و هر دو صفر unidirectional برنامه اعلام می‌کنند. سقف packet غیرقابل‌رمزگشایی ۸ است. QUICHE پس از پردازش هر frame، بیش از ۱۶ KiB دادهٔ دریافت‌شدهٔ CRYPTO را که در بافر هر encryption level مانده باشد رد می‌کند. بررسی فعالِ crypto-send رشد بیش از ۱۶ KiB را فقط وقتی رد می‌کند که همان level از قبل دادهٔ ارسال‌نشده داشته باشد؛ وقتی بافر ارسال‌نشده خالی است این بررسی انجام نمی‌شود و دادهٔ ارسال‌شده ولی تأییدنشده هم در آن حساب نمی‌شود. این مقدار سقف کل retention مربوط به TLS/CRYPTO نیست. سقف CID فعالِ اعلام‌شده ۲ و ضریب سخت anti-amplification پیش از اعتبارسنجی ۳ است؛ migration خاموش است. کتابخانهٔ pin‌شده، CIDهای صادرشدهٔ فعال و درحال بازنشستگی را جداگانه تا ۱۰ نگه می‌دارد و ممکن است original destination connection ID نیز بماند؛ سقف اعلام‌شده، کل حافظهٔ CIDها را توصیف نمی‌کند. حداکثر اندازهٔ datagram خروجی قابل‌تنظیم بین ۱۲۰۰ تا ۱۴۵۲ بایت است؛ packetهای معمول QUIC می‌توانند کوچک‌تر باشند. datagram ورودی در حالت framed به‌طور مستقل حداکثر ۱٬۴۷۲ بایت است. با تغییر pin library یا admission، این محدودیت‌ها را با engine تولید هماهنگ نگه دارید.

ring ثابت descriptor شرط (max-buffered-buffers + 1) * 24 <= max-buffered-bytes دارد. backing ledger ارسال برای هر entry تنظیم‌شده ۱۶ بایت و هر backlog از نوع wire/plaintext برای هر entry تعداد ۲۴ بایت، حتی در ring خالی، charge می‌شود.

اندازه و مرز دیتاگرام‌های Trojan​

TrojanClient و TrojanServer بدنهٔ UDP از صفر تا 65,535 بایت را در هر دو جهت می‌پذیرند؛ فیلد طول دو‌بایتی فقط بدنه را می‌شمارد. هدر آدرس، port، طول و CRLF حداکثر 263 بایت است، پس بزرگ‌ترین فریم 65,798 بایت می‌شود. ورودی UDP نگهداری‌شده، شامل head فعال و metadata cache‌شده، حداکثر 2,162,950 بایت است: پایهٔ 2 MiB به‌علاوه یک فریم حداکثری. سقف مونتاژ درخواست اولیهٔ TrojanServer هم همین مقدار است. برابری با سقف پذیرفته می‌شود؛ عبور از بودجه همان association را می‌بندد.

کلاینت دیتاگرام محلی بزرگ‌تر از 65,535 بایت را بدون شروع درخواست حذف می‌کند. دیتاگرام خالی معتبر است. decoder آدرس، port غیرصفر و CRLF را پیش از استخراج بدنه بررسی می‌کند و تمام دیتاگرام‌های کامل ورودی پذیرفته‌شده را حتی پس از Pause تحویل می‌دهد؛ فقط قطعهٔ ناقص یا مانع واقعی ترتیب نگه‌داری می‌شود. فرستندهٔ پاسخ سرور نیز همین سقف بدنه را دارد و حداکثر 263 بایت prepend می‌کند.

این سقف framing تضمین ظرفیت socket UDP، مسیر یا peer نیست. برای نمونه، UDP روی IPv4 معمولی و بدون option حداکثر 65,507 بایت payload دارد. هر بدنهٔ پذیرفته‌شده یک datagram می‌ماند؛ فریم می‌تواند چند read حامل را دربرگیرد، ولی هیچ endpoint بدنهٔ برنامه را کوتاه یا به چند دیتاگرام جدا تقسیم نمی‌کند.

codec مشترک و رکوردهای معمولی VMess​

tunnels/Internals/VmessCommon مالک KDF، کلیدهای آماده UUID، عملیات AuthID/header، اعتبارسنجی mode، مشتق‌سازی پاسخ و codec پایدار بدنه است. vector اعتبارنامه، جستجوی نامزدها، pump احراز هویت و جدول replay در VmessServer می‌مانند. timestamp و entropy صریح، vector مستقل و قطعی کلاینت را ممکن می‌کنند؛ production مواد تازه درخواست را هنگام اولین تعهد ارسال می‌گیرد. context متغیر cipher/SHAKE متعلق به worker line است. کلید موقت، نشست و node در مرز پایان عمر خود پاک می‌شود.

plaintext درخواست حداکثر ۳۱۶ و envelope کامل حداکثر ۳۷۴ بایت است. کلاینت ابتدا طول ثابت ۱۸ بایتی پاسخ را احراز می‌کند و سپس plaintext بین ۴ تا ۲۵۹ بایت، یعنی حداکثر ۲۹۳ بایت wire با دو tag را نگه می‌دارد. نشانگر مورد انتظار و طول دقیق فرمان بیرونی را بررسی و option/فرمان محدود را بدون تغییر حساب مصرف کنید؛ بدنه هم‌رسیده باید حفظ شود. پاسخ عادی سرور همچنان ۳۸ بایت است. key و nonce طول احرازشده پاسخ از key/IV درخواست می‌آیند؛ رمزنگاری بدنه و SHAKE از key/IV مشتق‌شده پاسخ استفاده می‌کنند، مطابق peer مستقل pin‌شده.

VmessClient برای chain گسترش‌یافته splice را مسدود و padding چپ لازم را صفر اعلام می‌کند. خروجی معمولی با padding کامل onward تخصیص دهید؛ درخواست و اولین رکورد در یک buffer قرار می‌گیرند. plaintext هر رکورد TCP حداکثر ۱۶ KiB است؛ هر datagram UDP یک رکورد است؛ datagram محلی خالی فقط در packetaddr حفظ می‌شود. محاسبه حداکثر frame را از codec با tag، offset طول احرازشده و بدترین padding بگیرید. UDP بومی خالی و UDP بزرگ بدون تقسیم یا شروع درخواست حذف می‌شود. رکورد framed خالی پایان پروتکل است و tag آن باید پیش از EOF تأیید شود.

در هر جهت کلاینت سقف فراگیر ۲ MiB بایت منطقی، ۱٬۰۲۴ entry و ۸ MiB هزینه تخصیص برقرار است. buffer فعال pop‌شده و fragment ناقص reservation خود را نگه می‌دارند؛ fragmentها را وقتی حافظه نگهداری‌شده کمتر می‌شود compact کنید. buffer و reservation در handoff، refusal یا close دقیقاً یک‌بار تسویه می‌شوند. guardهای TX/RX فقط برای parse ناقص یا ترتیب FIFO ورودی نگه می‌دارند؛ صف decoded-output صرفاً برای فشار وجود ندارد. سقف احرازنشده سرور همچنان ۶۴ KiB است و burst زودهنگام بعد از framing ممکن است هنگام شناسایی از آن عبور کند. barrier تأیید پاسخ، pacing ضمنی یا افزایش سقف سرور نباید این refusal را پنهان کند.

هر endpoint سیاست ثابت encrypted-counter-policy را صریح به هر body context می‌دهد. پیش‌فرض wrap داده را در همهٔ ordinalهای 0..65,535 می‌پذیرد و با همان کلید از 65,535 به صفر ادامه می‌دهد. این رفتار برای سازگاری عمداً nonce شانزده‌بیتی AEAD را تکرار می‌کند؛ rekey نیست و یکتایی nonce را حفظ نمی‌کند. strict خروجی 65,535 را برای EOF رزرو و دادهٔ معتبر ورودی در آن ordinal را تحویل می‌دهد، سپس پیش از استفادهٔ بعدی nonce می‌بندد. NONE/zero بی‌تأثیرند. سیاست محلی است و negotiation ندارد؛ strict می‌تواند همتای wrap را در حد خودش ببندد.

پیشروی فقط یک‌بار پس از رکورد موفق است؛ بدنه و authenticated length همان ordinal را دارند. state طول/padding در ورودی ناقص حفظ می‌شود و SHAKE در rollover بدون reset کلید، IV یا parse پاسخ ادامه می‌یابد. EOF و failure نهایی‌اند و retry با ordinal یا سیاست دیگر مجاز نیست. wrap به پیمانهٔ 65,536 محدود است و strict sentinel برابر 65,536 دارد. reconnect/replay خودکار ایجاد نمی‌شود.

Packetaddr فرمان UDP شمارهٔ ۲ با دامنهٔ دقیق sp.packet-addr.v2fly.arpa و port صفر است؛ تنها استثنای port صفر همین مورد است و این نشانه هرگز به DNS/connector نمی‌رود. هر بدنهٔ احرازشده یک خانوادهٔ 0x01/IPv4 یا 0x02/IPv6، آدرس network-order، port غیرصفر big-endian و یک datagram دارد. پوشش ۷ یا ۱۹ بایتی از حداکثر payload برنامه کم می‌شود. دامنه در این پوشش پشتیبانی نمی‌شود. پوشش کامل پیش از routing بررسی می‌شود و datagram تقسیم نمی‌شود. پوشش بدون payload یک datagram خالی برنامه است؛ رکورد بیرونی framed خالی همچنان EOF و قواعد empty-drop بومی ثابت‌اند.

کلاینت یک IP/port ثابت و carrier متعلق به خود دارد. DNS محلی صریح می‌تواند IP بدهد؛ دامنهٔ resolveنشده پیش از Init transport رد می‌شود. مبدأ نامنتظر پاسخ حذف و شمارش می‌شود. سرور برای هر مقصد immutable یک backend در جدول محدود دارد؛ پاسخ با همان مقصد کد می‌شود و بایت‌های صف مستقل از backend هستند. سقف byte/entry/ allocation هر جهت با سقف تعداد مقصد افزایش نمی‌یابد.

پاسخ سرور دقیقاً یک header رمز‌شده پیش از بدنه دارد و هر دو جهت از سیاست بالا پیروی می‌کنند. UDP بومی پاسخ خالی را حذف و packetaddr آن را در پوشش غیرخالی حفظ می‌کند؛ پاسخ بزرگ هرگز تقسیم نمی‌شود. send-termination=false فقط EOF خروجی اختیاری را حذف می‌کند؛ EOF ورودی معتبر همیشه احراز و پذیرفته می‌شود و zero خام EOF framed ندارد. full-close تحویل دادهٔ هنوز ارسال‌نشده در strict exhaustion را وعده نمی‌دهد.