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

UserController

UserController node‌ای داخلی و پیشرفته است که محدودیت‌های کاربران احراز هویت‌شده را در chain‌های سرویس‌دهنده اعمال می‌کند.

معمولاً لازم نیست این node را دستی در chain قرار دهید. node‌های server-side مانند Socks5Server، TrojanServer و VlessServer در حالت authentication مبتنی بر database، خودشان یک UserController داخلی می‌سازند. فقط زمانی سراغ تنظیم دستی بروید که tunnel سفارشی شما lineAddUser() را فراخوانی می‌کند و lifecycle مربوط به user handle را می‌شناسد.

پذیرش از cache محلی​

پس از نخستین همگام‌سازی معتبر جدول و مبنای ساعت، authenticationclientCanAuthenticate() در قطع شبکه و احراز هویت دوباره از آخرین cache حافظه استفاده می‌کند؛ cache مهلت کهنگی ندارد. enabled، انقضا، سهمیه ترافیک و reservation محلی اتصال/IP همچنان اعمال می‌شوند. نبود cache، توقف، خطای نهایی continuity یا رد صریح اعتبارنامه خود کلاینت، پذیرش جدید را می‌بندد. فقط پاسخ موفق و معتبر Session رد صریح را پاک می‌کند؛ timeout، قطع اتصال، پاسخ خراب و authentication-required چنین اثری ندارند. اتصال‌های قبلاً مجاز به ثبت مصرف و اجرای محدودیت‌های محلی ادامه می‌دهند. هر درخواست HTTP روی اتصال reused پذیرش تازه است؛ exchange جاری و CONNECT ادامه می‌یابند. ویرایش مدیر پس از refresh معتبر می‌رسد. دو کلاینت قطع‌شده با مصرف شناخته‌شده ۸۰ از سهمیه ۱۰۰ گیگابایت می‌توانند هر کدام ۲۰ مصرف کنند و مجموع نهایی ۱۲۰ شود؛ سهمیه و reservation محلی، تضمین سراسری نیستند.

کاری که انجام می‌دهد​

UserController پس از node‌ای قرار می‌گیرد که user را احراز هویت کرده و یک user_handle_t معتبر روی line گذاشته است. برای هر line مدیریت‌شده، این node:

  • فعال بودن user را بررسی می‌کند
  • تاریخ انقضای user را می‌سنجد
  • یک connection slot را رزرو می‌کند
  • در صورت وجود source IP، یک IP slot را هم رزرو می‌کند
  • بایت‌های upstream و downstream را در آمار ترافیک حساب می‌کند
  • اگر user غیرفعال، منقضی، حذف یا over-quota شود line‌های فعال او را می‌بندد
  • پس از finish شدن line، connection slot و IP slot را آزاد می‌کند

این node login protocol را parse نمی‌کند و database اصلی کاربران را هم نگه نمی‌دارد. lookup و accounting از طریق یک AuthenticationClient موجود انجام می‌شوند.

جایگاه رایج​

برای server node‌های عادی دارای authentication، آن را دستی اضافه نکنید:

TcpListener -> Socks5Server -> TcpConnector

در حالت authenticated، Socks5Server خودش یک UserController داخلی میان خود و node outbound قرار می‌دهد.

جایگذاری دستی فقط برای chain‌های پیشرفته و سفارشی است که node قبلی یک user handle معتبر ثبت می‌کند:

CustomAuthNode -> UserController -> TcpConnector

line‌هایی که user handle معتبر ندارند بدون اعمال محدودیت عبور می‌کنند.

بعضی node‌ها user را فقط بعد از باز شدن line authenticate می‌کنند و سمت next آن‌ها packet line است، یعنی یک line برای هر worker، نه یک line جدا برای هر user. به همین دلیل نمی‌توانند مثل Socks5Server یا TrojanServer یک UserController بعد از خودشان قرار دهند. مثال اصلی این حالت، WireGuardDevice در حالت database-backed peer authentication است؛ در این مدل UserController روی سمتی قرار می‌گیرد که هنوز per-peer line دارد. بسته به اینکه کدام adapter سر chain باشد، آن per-peer line می‌تواند از هر کدام از دو سمت UserController شروع شود:

UdpStatelessSocket -> UserController -> WireGuardDevice -> TunDevice   (line starts from prev)
TunDevice -> WireGuardDevice -> UserController -> UdpStatelessSocket (line starts from next)

در هر دو چیدمان، per-peer line ابتدا بدون مدیریت از UserController عبور می‌کند، چون هنوز user روی آن ثبت نشده است. WireGuardDevice اکنون از peerهای تنظیم‌شده استفاده می‌کند و user پایگاه داده را روی line نمی‌گذارد. integration سفارشی می‌تواند با lineAddUser() و Programmatic Promotion API line را مدیریت کند.

UserController در هر دو جهت کار می‌کند: چه line از prev با upstream Init آغاز شود و چه از next با downstream Init. مبدأ هر line ذخیره می‌شود تا upload و download در جهت درست محاسبه شوند؛ بخش direction Awareness را ببینید.

نمونه تنظیمات​

{
"name": "user-controller",
"type": "UserController",
"settings": {
"auth-client-node-name": "auth-client",
"payload-mode": "stream",
"sweep-interval-ms": 1000,
"verbose": false
},
"next": "outbound"
}

auth-client باید نام یک AuthenticationClient در همان config باشد. UserController تنها به آن node اشاره می‌کند و auth client تازه‌ای نمی‌سازد.

فیلدهای اجباری​

فیلدنوعتوضیح
typestringباید UserController باشد.
settingsobjectباید object غیرخالی باشد.
settings.auth-client-node-namestringنام یک AuthenticationClient موجود. نباید به خود UserController اشاره کند.
settings.payload-modestringبرای instance دستی اجباری است: stream، datagram یا dest_context->protocol. سازندهٔ داخلی resolver تغییرناپذیر می‌دهد.

تنظیمات اختیاری​

گزینهپیش‌فرضتوضیح
sweep-interval-ms1000فاصله بررسی line‌های مدیریت‌شده روی هر worker، حتی در حالت idle؛ باید >= 1 باشد.
verbosefalseجزئیات بیشتری در warning مربوط به رد یا بسته شدن line‌های مدیریت‌شده ثبت می‌کند.

limit‌هایی که enforce می‌شود​

UserController محدودیت‌های user را از جدول محلی AuthenticationClient می‌خواند.

محدودهرفتار
enableduser غیرفعال نمی‌تواند line managed جدید باز کند و line‌های موجودش بسته می‌شوند.
expiry زمانیuser منقضی‌شده نمی‌تواند line مدیریت‌شده جدید باز کند و line‌های موجود او بسته می‌شوند.
traffic.up, traffic.down, traffic.totalبایت واقعی payload به AuthenticationClient داده می‌شود؛ ضریب traffic-multiplier یک بار اعمال و مصرف قابل‌کسر به counterها اضافه می‌شود؛ user over-quota رد یا بسته خواهد شد.
connections-outحداکثر تعداد line‌های outbound هم‌زمان برای user.
ipsحداکثر تعداد source IP‌های هم‌زمان برای user. استفاده دوباره از IP قبلاً شمرده‌شده مجاز است.

این node connections-in یا occupancy زندهٔ devices را اعمال نمی‌کند. فیلدهای ذخیره/صادرشده و gaugeهای تشخیصی دستگاه حفظ می‌شوند ولی reservation زندهٔ دستگاه را محدود نمی‌کنند. پهنای باند محلی را طبق قرارداد بخش بعد اعمال می‌کند.

نبودن یک limit یا مقدار صفر آن یعنی فیلد مربوطه نامحدود است.

پهنای باند و مرزبندی payload​

limit.bandwidth.u یا up و .d یا down تعداد بایت منطقی واقعی در ثانیه هستند؛ صفر یعنی نامحدود و کل بازهٔ unsigned 64-bit معتبر است. همهٔ اتصال‌ها، controllerها و workerهای یک user در همان AuthenticationClient سهم مشترک دارند؛ userها، جهت‌ها و instanceهای دیگر مستقل‌اند. هماهنگی بین ماشین‌ها وجود ندارد. traffic-multiplier فقط مصرف سهمیه را تغییر می‌دهد؛ نرخ، burst، اندازهٔ صف و ساعت را تغییر نمی‌دهد.

ظرفیت burst عادی برای نرخ R برابر max(1, ceil(R / 10)) بایت است؛ تقریباً ۱۰۰ میلی‌ثانیه با حداقل یک بایت در نرخ خیلی کم. صف‌های واجد شرایط به نوبت‌های محدود FIFO سرویس می‌گیرند؛ datagram کامل هنگام جمع کردن اعتبار نوبت خود را نگه می‌دارد، بنابراین fairness تضمین تأخیر برابر نیست. stream می‌تواند به قطعه‌های دقیق تقسیم شود؛ datagram کامل و به ترتیب ارسال می‌شود. datagram خالی جای خود را در FIFO حفظ می‌کند و نرخ یا سهمیه مصرف نمی‌کند. بستهٔ بزرگ‌تر از burst ابتدا کل اعتبار خود را جمع می‌کند و سپس یک‌جا می‌رود؛ بنابراین burst لحظه‌ای استثنای یک datagram دارد، حتی با نرخ ۱ بایت در ثانیه. idle پیش از فعال شدن head فقط اعتبار عادی می‌دهد. لغو یا Pause نوبت head را آزاد و اعتبار قابل انتقال را به ظرفیت عادی محدود می‌کند. سقف datagram برابر ۶۵٬۵۳۵ بایت است؛ سقف کوچک‌تر خود پروتکل همچنان برقرار است.

محدوده، مجموع دو جهتبایت منطقیهزینهٔ تخصیصbufferticket صف جهت
هر line مدیریت‌شده1 MiB2 MiB2562
هر user در AuthenticationClient4 MiB8 MiB10241024
هر AuthenticationClient64 MiB128 MiB1638416384

این‌ها سقف منابع‌اند، نه تنظیم نرخ. رد پذیرش stream، line را می‌بندد و صف را بدون هزینه دور می‌ریزد. overflow در datagram فقط بستهٔ تازهٔ ردشده را حذف می‌کند و FIFO پذیرفته‌شده سالم می‌ماند. رد timer یا continuation لازم، line را در هر دو حالت می‌بندد. Pause مصرف‌کننده drain و نوبت آن جهت را متوقف می‌کند؛ Resume پیش از اعلان بیرونی پیشرفت را برقرار می‌کند. برای stream، فشار محلی و فشار مصرف‌کننده مستقل‌اند. backlog محلی datagram به‌تنهایی Pause/Resume به تولیدکننده نمی‌فرستد، زیرا تولیدکنندهٔ UDP ممکن است هنگام Pause بسته‌ها را پیش از پر شدن صف این node حذف کند. datagram هنگام انتظار برای اعتبار نرخ همچنان وارد صف محدود می‌شود؛ Pause/Resume صریح مصرف‌کننده همچنان اعمال و به تولیدکننده منتقل می‌شود.

نخستین ورود با طول مثبت، پیش از queue شدن، مشاهدهٔ first-use، گزارش و انقضا را حتی offline و با ضریب تخفیف آغاز می‌کند. صفر بایت آغاز استفاده نیست. هزینه فقط یک بار، با ضریب، درست پیش از واگذاری مالکیت Payload اعمال می‌شود. آخرین قطعهٔ مجاز که سهمیه را تمام یا رد می‌کند یک بار تحویل و سپس line زنده بسته می‌شود؛ user از قبل over-quota grant جدید نمی‌گیرد. دادهٔ صف‌شده یا دورریخته‌شده هزینه ندارد. handoff تأیید تحویل socket نیست.

refresh با ID یکسان، حتی تعویض رمز، اعتبار bucket، کسر refill، timestamp و هویت ticket را حفظ می‌کند. تغییر نرخ ابتدا اعتبار نرخ قدیم را محاسبه و به ظرفیت تازه محدود می‌کند؛ refresh بدون تغییر burst تازه نمی‌دهد. گذار به نامحدود FIFO را حفظ و گذار از نامحدود به محدود فقط یک burst عادی ایجاد می‌کند. اتصال مدیریت‌شده در outage عادی یا رد credential کلاینت اجرای cache را ادامه می‌دهد.

instance دستی همیشه به payload-mode صریح نیاز دارد، حتی وقتی userهای فعلی نامحدودند:

مقداررفتار payload
streamبایت‌ها می‌توانند بین چند تحویل تقسیم شوند.
datagramهر payload یک datagram کامل باقی می‌ماند.
dest_context->protocolپروتکل مقصد یک بار در Init خوانده می‌شود: فقط TCP یعنی stream و فقط UDP یعنی datagram.

در حالت destination context، پروتکل غایب، مبهم یا پشتیبانی‌نشده باعث رد line می‌شود. این گزینه تنها وقتی مناسب است که پروتکل مقصد پیش از Init مشخص باشد و نوع payload در همین محل chain را توصیف کند. تبدیل transport می‌تواند metadata مسیریابی UDP را روی line دارای بایت‌های stream رمزگشایی‌شده باقی بگذارد؛ بنابراین حذف setting هرگز به‌صورت خودکار این تشخیص را فعال نمی‌کند.

سازندهٔ داخلی بلافاصله پس از ساخت و پیش از prepare، usercontrollerSetPayloadModeResolver(controller, owner, resolver) را bind می‌کند؛ setting و resolver هم‌زمان پذیرفته نیستند. resolver پرس‌وجوی خالص metadata روی worker مالک است؛ هر منبع mode در Init هر جهت، پیش از admission یا forwarding، یک بار تعیین و ذخیره می‌شود. promotion دیرهنگام هم از همان mode استفاده می‌کند و تغییر destination آن را عوض نمی‌کند. SOCKS/Trojan/VLESS نقش TCP و UDP remote را از state آماده می‌خوانند؛ HTTP همواره stream و transport معمولی WireGuard در هر دو جهت datagram است. packet line وارد این node نمی‌شود. این قابلیت احراز هویت database برای WireGuard اضافه نمی‌کند؛ رفتار unmanaged آن حفظ می‌شود.

direction Awareness​

UserController جهت شروع هر line را ذخیره می‌کند و با همان اطلاعات ترافیک را به counter‌های upload یا download نسبت می‌دهد. «upload» ترافیکی است که از user به سمت دیگر مسیر می‌رود و «download» ترافیکی است که به user برمی‌گردد. user همیشه در سمتی قرار دارد که line از آن آغاز شده است.

line originInitiated byupstream payload counts asdownstream payload counts as
From prev (upstream Init)یک head/prev adapter، مثلاً UdpStatelessSocket در ابتدای chainuploaddownload
From next (downstream Init)یک tail/next adapter، مثلاً UdpStatelessSocket در انتهای chaindownloadupload

ردیف اول حالت رایج در Socks5Server، TrojanServer و VlessServer است. ردیف دوم زمانی پیش می‌آید که adapter سمت user در انتهای chain باشد؛ مانند TunDevice -> WireGuardDevice -> UserController -> UdpStatelessSocket. در این چیدمان packet‌های user از next وارد می‌شوند، بنابراین downstream payload همان upload کاربر است. quota در هر دو حالت به یک شکل اعمال می‌شود.

جریان اجرا​

در Init از هر سمت (upstream Init برای line‌ای که از prev شروع شده، downstream Init برای line‌ای که از next شروع شده):

  1. line state آماده و جهت شروع line ذخیره می‌شود؛ payload mode تعیین و ذخیره می‌شود و اگر تعیین نشود حتی line ناشناس رد می‌شود
  2. user handle فعلی line خوانده می‌شود
  3. line‌های anonymous یا دارای handle نامعتبر بدون اعمال محدودیت عبور می‌کنند و می‌توان بعداً آن‌ها را promote کرد
  4. line‌های authenticated از طریق AuthenticationClient بررسی می‌شوند
  5. برای line‌های پذیرفته‌شده connection/IP slot رزرو می‌شود و line در sweep ثبت خواهد شد
  6. line ردشده در همان سمتی که آن را شروع کرده finish می‌شود و سمت مقابل راه نمی‌افتد

ورود مثبت first-use را ثبت می‌کند. برای تحویل، mode ذخیره‌شده و جهت مبدأ تعیین‌کننده‌اند؛ bandwidth و quota پس از آماده‌سازی و درست پیش از handoff ثبت می‌شوند. قطعهٔ نهایی مجاز یک بار ارسال و سپس line بسته می‌شود؛ صف و حذف هزینه ندارند.

در Finish، node accounting state را آزاد و state خودش را پاک می‌کند؛ سپس طبق قواعد directional finish، Finish را فقط به جهت مقابل می‌فرستد.

Programmatic Promotion API​

بعضی node‌ها user را پس از باز شدن line authenticate می‌کنند؛ نمونه‌های آن در بخش جایگاه رایج آمده است. UserController برای این node‌ها تابع زیر را در interface.h عمومی در دسترس می‌گذارد:

WW_EXPORT user_admission_result_t usercontrollerTunnelTryManageLine(tunnel_t *t, line_t *l);

این تابع همان admission مسیر upstream Init را در لحظه اجرا می‌کند: همه limit‌ها بررسی می‌شوند و در صورت پذیرش، connection slot و IP slot رزرو خواهند شد. node‌ای که از آن استفاده می‌کند باید:

  1. pointer مربوط به instance node UserController را نگه دارد؛ با همان الگویی که Socks5Server برای ساخت UserController داخلی خود با nodeUserControllerGet() و createHandle به کار می‌برد.
  2. peer را authenticate و user را با lineAddUser() روی line ثبت کند.
  3. تابع usercontrollerTunnelTryManageLine(user_controller_instance, line) را فراخوانی کند.

مقدارهای برگشتی:

Resultمعنی
kUserAdmissionOkline اکنون managed شده، یا از قبل managed بوده و call idempotent بوده، یا user ندارد و به‌صورت unmanaged passthrough رها شده است. ادامه دادن مجاز است.
هر مقدار دیگر kUserAdmission*user پذیرفته نشده است؛ مثلاً disabled یا expired است، quota را رد کرده یا به connection/IP limit رسیده است. line همچنان بدون مدیریت و بدون تغییر می‌ماند.

قرارداد استفاده:

  • اگر نتیجه non-OK باشد، این تابع نه Finish می‌فرستد و نه line state را از بین می‌برد. رسیدگی به اتصال ردشده بر عهده caller است؛ caller باید خودش line را ببندد یا کنار بگذارد و نباید آن را پذیرفته‌شده فرض کند. فراخوانی ناموفق هیچ slotی رزرو نمی‌کند، بنابراین تلاش دوباره در آینده امن است.
  • تابع را روی worker مالک line فراخوانی کنید؛ این شرط را با lineIsOnCurrentWorker(l) بررسی کنید. line باید یک normal line باشد که پیش‌تر از همین instance node UserController عبور کرده است؛ این API برای packet line نیست.
  • وقتی line با این روش تحت مدیریت قرار گرفت، traffic accounting، idle sweep و آزاد شدن slot‌ها هنگام Finish درست مانند line‌ای عمل می‌کنند که در Init پذیرفته شده است؛ caller به teardown اضافه‌ای نیاز ندارد.

نمونه کد درون node authenticating، پس از handshake موفق روی line:

lineAddUser(line, &resolved_handle, username, password);

user_admission_result_t admission = usercontrollerTunnelTryManageLine(ts->user_controller_tunnel, line);
if (admission != kUserAdmissionOk)
{
// limits exceeded (for example IP limit): reject this peer / close the line here.
return;
}
// admitted: continue bringing the peer up.

Sweep Timer​

هر worker یک sweep timer دوره‌ای دارد. timer، line‌های مدیریت‌شده را بررسی می‌کند تا user غیرفعال، منقضی، حذف‌شده یا over-quota حتی بدون payload جدید هم قطع شود.

sweep registry تا زمان مدیریت شدن line‌ها reference آن‌ها را نگه می‌دارد. با بسته شدن line یا توقف worker، reference‌ها آزاد می‌شوند.

اولین بایت واقعی در helper حسابداری AuthenticationClient مشاهده محلی و countdown را حتی offline شروع می‌کند. همان مسیر payload و sweep بیکار، کاربر منقضی را می‌بندند و slotها را آزاد می‌کنند؛ رد صریح credential کلاینت ثبت مصرف اتصال موجود را متوقف نمی‌کند. refresh سن و origin را با reservationها از روی ID منتقل می‌کند و انقضای نسبی و مطلق را جدا از policy جدید محاسبه می‌کند. login و صفر بایت شروع استفاده نیستند و فعال‌کردن مدت بعد از مصرف نامحدود از همان سن اصلی استفاده می‌کند.

نکته‌های عملیاتی​

  • جایگذاری دستی نیازمند node قبلی است که lineAddUser() را با user handle معتبر صدا بزند.
  • Socks5Server، TrojanServer و VlessServer در authenticated mode این node را داخلی می‌سازند؛ در آن حالت یک UserController دیگر مستقیم بعد از آن‌ها قرار ندهید.
  • UserController هیچ‌وقت config user مثل password، enabled state یا limit‌ها را نمی‌نویسد.
  • live connection/IP counters state محلی پردازش هستند و از طریق helper‌های AuthenticationClient نگه‌داری می‌شوند.
  • مصرف قابل‌کسر به stats عادی user اضافه می‌شود. ضریب traffic-multiplier روی AuthenticationClient تنظیم می‌شود؛ اندازهٔ واقعی payload، bandwidth و سرعت تغییر نمی‌کنند.
  • نخستین ترافیک واقعی حتی با هزینهٔ گرد‌شدهٔ صفر، زمان انقضا را شروع می‌کند. باقی‌ماندهٔ کسری هر جهت با ID پایدار در refresh منتقل و در reconnect حفظ می‌شود؛ این state فقط در حافظه است.
  • این node روی normal line با payload از نوع stream یا datagram کار می‌کند و packet tunnel نیست.

متادیتای node​

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

ویژگیمقدار
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left0 بایت

پشتیبانی splice​

داده‌های مجاز و مدیریت‌نشده در هر دو جهت بدون بررسی بدنه عبور می‌کنند. حسابداری ترافیک بر اساس طول منطقی payload، شامل prefix مقیم حافظه، انجام می‌شود؛ نگاشت معکوس upload/download برای line آغازشده از next حفظ می‌شود. هنگام رد ترافیک، buffer هنوز تحت مالکیت node بازیافت و دادهٔ pipe خصوصی تخلیه می‌شود، سپس مسیر فعلی بستن line اجرا می‌شود.

buffer‌های عادی در مسیر fallback همچنان پذیرفته می‌شوند. splice زمانی فعال می‌شود که build لینوکس از آن پشتیبانی کند، misc.splice فعال باشد، chain هیچ node از نوع packet نداشته باشد و همهٔ node‌های chain نهایی پس از گسترش و ادغام از splice پشتیبانی کنند. اعلام flag این قابلیت جای این بررسی‌ها را نمی‌گیرد. بهبود کارایی باید با اندازه‌گیری تأیید شود.