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 تازهای نمیسازد.
فیلدهای اجباری
| فیلد | نوع | توضیح |
|---|---|---|
type | string | باید UserController باشد. |
settings | object | باید object غیرخالی باشد. |
settings.auth-client-node-name | string | نام یک AuthenticationClient موجود. نباید به خود UserController اشاره کند. |
settings.payload-mode | string | برای instance دستی اجباری است: stream، datagram یا dest_context->protocol. سازندهٔ داخلی resolver تغییرناپذیر میدهد. |
تنظیمات اختیاری
| گزینه | پیشفرض | توضیح |
|---|---|---|
sweep-interval-ms | 1000 | فاصله بررسی lineهای مدیریتشده روی هر worker، حتی در حالت idle؛ باید >= 1 باشد. |
verbose | false | جزئیات بیشتری در warning مربوط به رد یا بسته شدن lineهای مدیریتشده ثبت میکند. |
limitهایی که enforce میشود
UserController محدودیتهای user را از جدول محلی AuthenticationClient میخواند.
| محدوده | رفتار |
|---|---|
enabled | user غیرفعال نمیتواند 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 برابر
۶۵٬۵۳۵ بایت است؛ سقف کوچکتر خود پروتکل همچنان برقرار است.
| محدوده، مجموع دو جهت | بایت منطقی | هزینهٔ تخصیص | buffer | ticket صف جهت |
|---|---|---|---|---|
| هر line مدیریتشده | 1 MiB | 2 MiB | 256 | 2 |
| هر user در AuthenticationClient | 4 MiB | 8 MiB | 1024 | 1024 |
| هر AuthenticationClient | 64 MiB | 128 MiB | 16384 | 16384 |
اینها سقف منابعاند، نه تنظیم نرخ. رد پذیرش 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 origin | Initiated by | upstream payload counts as | downstream payload counts as |
|---|---|---|---|
From prev (upstream Init) | یک head/prev adapter، مثلاً UdpStatelessSocket در ابتدای chain | upload | download |
From next (downstream Init) | یک tail/next adapter، مثلاً UdpStatelessSocket در انتهای chain | download | upload |
ردیف اول حالت رایج در 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
شروع شده):
- line state آماده و جهت شروع line ذخیره میشود؛ payload mode تعیین و ذخیره میشود و اگر تعیین نشود حتی line ناشناس رد میشود
- user handle فعلی line خوانده میشود
- lineهای anonymous یا دارای handle نامعتبر بدون اعمال محدودیت عبور میکنند و میتوان بعداً آنها را promote کرد
- lineهای authenticated از طریق
AuthenticationClientبررسی میشوند - برای lineهای پذیرفتهشده connection/IP slot رزرو میشود و line در sweep ثبت خواهد شد
- 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ای که از آن استفاده میکند باید:
- pointer مربوط به instance node
UserControllerرا نگه دارد؛ با همان الگویی کهSocks5Serverبرای ساختUserControllerداخلی خود باnodeUserControllerGet()وcreateHandleبه کار میبرد. - peer را authenticate و user را با
lineAddUser()روی line ثبت کند. - تابع
usercontrollerTunnelTryManageLine(user_controller_instance, line)را فراخوانی کند.
مقدارهای برگشتی:
| Result | معنی |
|---|---|
kUserAdmissionOk | line اکنون 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 nodeUserControllerعبور کرده است؛ این 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 flags | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 0 بایت |
پشتیبانی splice
دادههای مجاز و مدیریتنشده در هر دو جهت بدون بررسی بدنه عبور میکنند. حسابداری ترافیک بر اساس طول منطقی payload، شامل prefix مقیم حافظه، انجام میشود؛ نگاشت معکوس upload/download برای line آغازشده از next حفظ میشود. هنگام رد ترافیک، buffer هنوز تحت مالکیت node بازیافت و دادهٔ pipe خصوصی تخلیه میشود، سپس مسیر فعلی بستن line اجرا میشود.
bufferهای عادی در مسیر fallback همچنان پذیرفته میشوند. splice زمانی فعال میشود که build لینوکس از آن پشتیبانی کند، misc.splice فعال باشد، chain هیچ node از نوع packet نداشته باشد و همهٔ nodeهای chain نهایی پس از گسترش و ادغام از splice پشتیبانی کنند. اعلام flag این قابلیت جای این بررسیها را نمیگیرد. بهبود کارایی باید با اندازهگیری تأیید شود.