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

AuthenticationClient

AuthenticationClient بخش frontend پروتکل پایگاه داده احراز هویت WaterWall است. این نود روی worker شماره 0 یک line کنترلی داخلی می‌سازد و آن را در جهت upstream به نود next می‌فرستد. سپس در AuthenticationServer احراز هویت می‌شود، جدول مرجع کاربران را می‌گیرد و با ارسال دوره‌ای پیام‌ها، session را زنده نگه می‌دارد و شمارنده‌های ترافیک محلی را به سرور می‌فرستد.

AuthenticationClient تونلی برای عبور داده نیست. نودهایی که ترافیک کاربران را سرویس می‌دهند، از API داخلی آن برای خواندن اطلاعات کاربران و ثبت مصرف محلی در جدول دریافت‌شده استفاده می‌کنند.

جایگاه رایج

AuthenticationClient -> ... -> AuthenticationServer

برای اتصال به سرور راه دور، chain سمت کلاینت معمولاً چنین شکلی دارد:

AuthenticationClient -> TcpConnector

مسیر connector باید در سمت سرور به chainای برسد که به AuthenticationServer ختم می‌شود.

این نود چه می‌کند؟

  • روی worker شماره 0 یک line کنترلی داخلی و بلندمدت می‌سازد.
  • با مشخصات کلاینت که در پیکربندی آمده، درخواست Authenticate را به AuthenticationServer می‌فرستد.
  • session token 64 بایتی بازگشتی را ذخیره می‌کند.
  • پس از احراز هویت، جدول کامل کاربران را با GetAllUsers می‌گیرد.
  • یک نسخه محلی و محافظت‌شده با lock از جدول کاربران را برای تونل‌های دیگر نگه می‌دارد.
  • شمارنده‌های ترافیک محلی که هنوز sync نشده‌اند را هنگام جایگزینی کامل جدول حفظ می‌کند.
  • درخواست‌های Ping authenticated دوره‌ای می‌فرستد.
  • درخواست‌های دوره‌ای PushUserStats را همراه با اطلاعات شمارنده‌های ترافیک می‌فرستد.
  • درخواست‌های GetAllUsers را به‌صورت دوره‌ای یا بر اساس تغییر revision می‌فرستد.
  • اگر transport در جهت downstream بسته شود، دوباره متصل می‌شود و احراز هویت می‌کند.
  • helperهای جست‌وجوی کاربر، خروجی JSON، حسابداری ترافیک و admission را از طریق AuthenticationClient/interface.h در اختیار بخش‌های دیگر می‌گذارد.

از آنجا که line کنترلی را خودش می‌سازد، مسئول آزاد کردن آن نیز هست.

نمونه تنظیم

نمونه حداقلی برای احراز هویت درون همان process:

{
"name": "auth-client",
"type": "AuthenticationClient",
"settings": {
"name": "edge-1",
"secret": "long-random-secret"
},
"next": "auth-db"
}

chain اتصال به سرور احراز هویت راه دور:

{
"name": "auth-client",
"type": "AuthenticationClient",
"settings": {
"name": "edge-1",
"secret": "long-random-secret",
"ping-interval-ms": 60000,
"pull-interval-ms": 300000,
"push-interval-ms": 300000,
"reconnect-interval-ms": 5000,
"request-timeout-ms": 120000,
"max-pending-requests": 64,
"verbose": false
},
"next": "auth-server-connector"
}
{
"name": "auth-server-connector",
"type": "TcpConnector",
"settings": {
"address": "127.0.0.1",
"port": 9000,
"nodelay": true
}
}

مقادیر settings.name و settings.secret باید با یکی از ورودی‌های settings.auth-clients در AuthenticationServer یکسان باشند:

{
"name": "edge-1",
"secret": "long-random-secret",
"allow-user-pull": true,
"allow-stats-push": true,
"allow-user-write": false
}

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

فیلدهای سطح اصلی:

فیلدنوعتوضیح
namestringنام انتخابی نود. نودهای دیگر با این نام به auth client ارجاع می‌دهند.
typestringباید دقیقاً "AuthenticationClient" باشد.
nextstringاجباری. نود بعدی باید به یک AuthenticationServer منتهی شود.
settingsobjectمشخصات احراز هویت و تنظیمات timerها.

فیلدهای اجباری داخل settings:

فیلدنوعتوضیح
namenon-empty stringنام کلاینت control-plane که برای AuthenticationServer فرستاده می‌شود.
secretnon-empty stringsecret مربوط به control-plane که همراه settings.name فرستاده می‌شود.

name و secret مربوط به control-plane هستند و مشخصات ورود کاربران ترافیک به شمار نمی‌آیند.

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

گزینهنوعپیش‌فرضتوضیح
ping-interval-msصفر یا عدد مثبت60000Ping احرازشده می‌فرستد. اگر اتصال برقرار باشد ولی احراز هویت انجام نشده باشد، Authenticate را نیز دوباره امتحان می‌کند.
pull-interval-msصفر یا عدد مثبت300000فاصله ارسال دوره‌ای GetAllUsers. مقدار 0 دریافت‌های مبتنی بر timer را غیرفعال می‌کند.
push-interval-msصفر یا عدد مثبت300000فاصله ارسال دوره‌ای PushUserStats. مقدار 0 این ارسال‌های دوره‌ای را غیرفعال می‌کند.
reconnect-interval-msصفر یا عدد مثبت5000مدت انتظار پیش از اتصال دوباره، بعد از بسته شدن transport کنترلی.
request-timeout-msصفر یا عدد مثبت120000اگر یک درخواست pending بیش از این مدت بی‌پاسخ بماند، اتصال از نو برقرار می‌شود. مقدار 0 بررسی timeout درخواست‌ها را غیرفعال می‌کند.
max-pending-requestsعدد مثبت64بیشترین تعداد درخواست‌های هم‌زمانِ در حال پردازش در پروتکل.
verbosebooleanfalseلاگ‌های debug مربوط به line کنترلی، timerها، درخواست‌ها و پاسخ‌ها را فعال می‌کند.

pull-interval-ms فقط دریافت‌های زمان‌بندی‌شده با timer را غیرفعال می‌کند. کلاینت همچنان پس از احراز هویت موفق، یک بار جدول را می‌گیرد؛ همچنین هرگاه پاسخ سرور نشان دهد نسخه محلی باید به‌روز شود، دوباره آن را دریافت می‌کند.

push-interval-ms فقط ارسال‌های دوره‌ای را غیرفعال می‌کند. تونل‌های دیگر همچنان می‌توانند از طریق API داخلی درخواست ارسال آمار بدهند.

استفاده از سایر نودها

معمولاً نودهای دیگر با نام نود به AuthenticationClient ارجاع می‌دهند. برای نمونه، Socks5Server می‌تواند به شکل زیر از یک AuthenticationClient موجود استفاده کند:

{
"name": "socks-server",
"type": "Socks5Server",
"settings": {
"auth-client-node-name": "auth-client",
"connect": true,
"udp": false
},
"next": "outbound-tcp"
}

AuthenticationClient در مسیر داده Socks5Server قرار نمی‌گیرد. این نود اتصال کنترلی جداگانه‌ای دارد و جدول کاربران دریافت‌شده را از طریق API داخلی در اختیار نودهای دیگر می‌گذارد.

مدل ارتباطی

کلاینت یک line کنترلی بلندمدت دارد؛ برای هر درخواست line جداگانه‌ای نمی‌سازد.

هنگام راه‌اندازی، worker شماره 0 یک line_t داخلی می‌سازد و callback مربوط به init را در جهت upstream به تونل بعدی می‌فرستد. در chainای مانند AuthenticationClient -> TcpConnector، همین line در WaterWall به یک اتصال transport خروجی به chain سمت سرور تبدیل می‌شود.

همه درخواست‌های احراز هویت، ping، دریافت و ارسال روی همان line فریم‌بندی می‌شوند. پاسخ هر درخواست نیز با correlation ID متناظر آن تشخیص داده می‌شود.

اگر transport بسته شود، callbackِ downstream finish لاین کنترلی تحت مالکیت کلاینت را می‌بندد و آزاد می‌کند، token فعال و وضعیت درخواست‌های pending را پاک می‌کند و، مگر هنگام توقف تونل، اتصال دوباره را زمان‌بندی می‌کند. اتصال بعدی با یک Authenticate تازه آغاز می‌شود و token قبلی دوباره به کار نمی‌رود.

تا وقتی اتصال قطع است یا احراز هویت انجام نشده، درخواست تازه‌ای در پروتکل فرستاده نمی‌شود. جدول کاربران محلی همچنان به‌عنوان cache در دسترس است، اما همگام‌سازی تنها پس از اتصال مجدد، دریافت est از transport و احراز هویت موفق از سر گرفته می‌شود.

جریان Startup

در onStart، تونل کارهای راه‌اندازی را در صف worker شماره 0 می‌گذارد.

سپس worker 0:

  1. کلاینت را در حالت started قرار می‌دهد.
  2. اگر ping timer فعال باشد، آن را راه می‌اندازد.
  3. اگر دریافت یا ارسال دوره‌ای فعال باشد، sync timer را شروع می‌کند.
  4. line کنترلی تحت مالکیت خود را می‌سازد.
  5. state مخصوص این تونل را برای line مقداردهی اولیه می‌کند.
  6. تابع tunnelNextUpStreamInit() را فراخوانی می‌کند.

وقتی سمت بعدی transport را برقرار کند و est را در جهت downstream بفرستد، کلاینت line کنترلی را متصل در نظر می‌گیرد و Authenticate را با یک session token صفر ارسال می‌کند.

وقتی Authenticate پاسخ session را برگرداند، token شصت‌وچهاربایتی به token فعال درخواست‌های بعدی تبدیل می‌شود و کلاینت بلافاصله GetAllUsers را می‌فرستد.

Timerها

همه timerهای پروتکل روی worker شماره 0 اجرا می‌شوند؛ همان worker که line کنترلی را در اختیار دارد.

Timerرفتار
ping_timerپس از احراز هویت Ping می‌فرستد؛ اگر اتصال برقرار باشد اما احراز هویت نشده باشد، Authenticate را دوباره امتحان می‌کند.
sync_timerدریافت جدول کاربران و ارسال آمار را با کوتاه‌ترین بازه فعال میان pull و push زمان‌بندی می‌کند.
reconnect_timerپس از بسته شدن transport در جهت downstream، یک line کنترلی تازه باز می‌کند.

در هر tick همگام‌سازی، ابتدا اگر زمان ارسال PushUserStats رسیده باشد این درخواست امتحان می‌شود. سپس، اگر revisionها متفاوت باشند، یا زمان pull رسیده باشد و پاسخ تازه‌تری با revision برابر، به‌روز بودن جدول محلی را تأیید نکرده باشد، GetAllUsers فرستاده می‌شود.

Timerها در onWorkerStop مربوط به worker شماره 0 حذف می‌شوند، زیرا حذف هر timer باید در همان event loopای انجام شود که مالک آن است.

جدول کاربران محلی

کلاینت یک جدول محلی فعال از کاربران نگه می‌دارد که یک wrwlock_t در سطح تونل از آن محافظت می‌کند.

از دید تونل‌های دیگر، جایگزینی جدول در جریان GetAllUsers به‌صورت atomic انجام می‌شود:

  1. پاسخ JSON خوانده و به یک users_t تازه تبدیل می‌شود.
  2. جدول تازه اعتبارسنجی می‌شود؛ از جمله اینکه rangeهای هم‌پوشان در wireguard-allowed-ips پذیرفته نمی‌شوند.
  3. deltaهای ترافیک محلیِ sync‌نشده و شمارنده‌های runtime مخصوص همان process، با durable user id منتقل می‌شوند؛ برای کاربران قدیمی از SHA-256 به‌عنوان fallback استفاده می‌شود.
  4. deadline محلی انقضای هر کاربر از فیلدهای زمانی متعلق به سرور و مقدار server-time-ms در پاسخ محاسبه می‌شود.
  5. اشاره‌گر جدول فعال زیر write lock مربوط به rwlock جابه‌جا می‌شود.
  6. شمارنده generation افزایش می‌یابد.
  7. جدول قدیمی پس از جابه‌جایی آزاد می‌شود.

Readerها هنگام کپی JSON یا آمار و نیز هنگام به‌روزرسانی ترافیک با SHA-256، read lock همان rwlock را نگه می‌دارند. به این ترتیب هیچ اشاره‌گری داخل users_t فعلی در میانه عملیات نامعتبر نمی‌شود.

جدول محلی هم نقش cache را دارد و هم شمارنده‌های محلی را تجمیع می‌کند. مرجع اصلی پیکربندی کامل هر کاربر، از جمله wireguard-allowed-ips اختیاری و first-usage-at-ms، سرور است.

API داخلی

سایر tunnelها include می‌کنند:

#include "AuthenticationClient/interface.h"

این API داخلی WaterWall با API خارجی تونل در api.c تفاوت دارد.

تونل‌های دیگر نباید اشاره‌گر خام user_t * را از AuthenticationClient بگیرند یا cache کنند. در عوض باید از user_handle_t استفاده کنند؛ شناسه‌ای مقداری که شامل موارد زیر است:

sha256(password)
users_generation
user_id

user_id از رکورد sync‌شده کاربر کپی می‌شود و برای کاربران قدیمی ممکن است 0 باشد. اگر این شناسه موجود باشد، helperهای اعمال محدودیت زنده ابتدا کاربر را با durable user id پیدا می‌کنند و در صورت نیاز به SHA-256 برمی‌گردند.

Helperهای رایج وضعیت:

authenticationclientGetState(t)
authenticationclientIsReady(t)
authenticationclientUsersGeneration(t)

Helperهای جست‌وجو:

authenticationclientGetUserByPassword(t, password, &handle)
authenticationclientGetUserByPasswordWithResult(t, password, &handle)
authenticationclientGetUserBySHA224(t, sha224, &handle)
authenticationclientGetUserBySHA256(t, sha256, &handle)
authenticationclientGetUserByUUID(t, uuid, &handle)
authenticationclientGetUserByWireGuardPublicKey(t, publickey, &handle)
authenticationclientGetUserByPasswordWithProfile(t, password, &handle, &profile)
authenticationclientGetUserBySHA224WithProfile(t, sha224, &handle, &profile)
authenticationclientGetUserByUUIDWithProfile(t, uuid, &handle, &profile)
authenticationclientGetUserByWireGuardPublicKeyWithProfile(t, publickey, &handle, &profile)
authenticationclientUserProfileClear(&profile)

Helperهای کپی خروجی و آمار:

authenticationclientUserHandleIsLive(t, &handle)
authenticationclientUserToJson(t, &handle)
authenticationclientUsersToJson(t)
authenticationclientUserGetStats(t, &handle, &stats)
authenticationclientUserAddTraffic(t, &handle, upload, download)

Helperهای اعمال زنده محدودیت‌ها:

authenticationclientUserTryAdmitConnection(t, &handle, &ip_key, now_ms)
authenticationclientUserReleaseConnection(t, &handle, &ip_key)
authenticationclientUserAccountTraffic(t, &handle, upload, download, now_ms)
authenticationclientUserShouldClose(t, &handle, now_ms)

Helperهای همگام‌سازی دستی:

authenticationclientRequestPull(t)
authenticationclientRequestPush(t)

authenticationclientUserToJson() و authenticationclientUsersToJson() آبجکت‌های تازه cJSON برمی‌گردانند و caller مالک آن‌هاست. آمار نیز در فضای ذخیره‌سازی caller کپی می‌شود. نودهایی که ترافیک را سرویس می‌دهند نباید user_t را مستقیم تغییر دهند.

آرگومان now_ms در helperهای اعمال محدودیت، بر پایه ساعت monotonic محلی کلاینت است. هنگام نصب نتیجه GetAllUsers، فیلدهای انقضای متعلق به سرور به همین مبنای زمانی تبدیل می‌شوند.

حالت‌های Client

authenticationclientGetState() برمی‌گرداند:

حالتمعنی
kAuthenticationClientStateStoppedکلاینت متوقف است یا در دسترس نیست.
kAuthenticationClientStateConnectingکلاینت راه افتاده، اما هنوز session کنترلی احرازشده ندارد.
kAuthenticationClientStateAuthenticatingtransport متصل است یا token وجود دارد، اما جدول کامل کاربران هنوز بارگذاری نشده است.
kAuthenticationClientStateReadyکلاینت احراز هویت شده و جدول کاربران را نصب کرده است.

authenticationclientIsReady() فقط در kAuthenticationClientStateReady مقدار true دارد.

جست‌وجوی password که نتیجه تفصیلی می‌دهد، ممکن است یکی از این مقادیر را برگرداند:

نتیجهمعنی
oklookup موفق بود.
invalid password lookupآرگومان نامعتبر، password خالی یا نبودن handle خروجی.
password hash failedمحاسبه SHA-256 شکست خورد.
users table unavailableجدول محلی کاربران هنوز بارگذاری نشده است.
user not foundکاربری با این password پیدا نشد.
password mismatchhash منطبق بود، اما بررسی password به‌صورت plaintext موفق نشد.
user disabledکاربر پیدا شده غیرفعال است.
user expiredکاربر پیدا شده بر اساس ساعت محلی کلاینت منقضی شده است.
user limit reachedکاربر پیدا شده به یکی از محدودیت‌های تعیین‌شده رسیده است.

پروتکل Wire

پروتکل با AuthenticationServer تطابق دارد.

پیام request:

u32 body_len
u8[64] session_token
request_frame

فریم request:

u8 request_type
u32 correlation_id
u32 request_data_len
u8[request_data_len] request_data

پیام response:

u32 body_len
u64 config_revision
u64 stats_revision
response_frame...

فریم response:

u8 response_type
u32 correlation_id
u32 response_data_len
u8[response_data_len] response_data

همه فیلدهای integer با ترتیب big-endian نوشته می‌شوند. کلاینت در هر پیام یک فریم درخواست می‌فرستد. parser می‌تواند هر تعداد فریم پاسخ را در یک پیام پاسخ بخواند و هر فریم را با correlation ID به درخواستش متصل کند.

revisionهای پاسخ به‌عنوان آخرین revisionهای سرور ذخیره می‌شوند و پیش از دریافت‌های زمان‌بندی‌شده، با revision جدول کاربران محلی مقایسه می‌شوند.

Stats Push

PushUserStats فقط یک به‌روزرسانی جزئی و راهنماست؛ کلاینت تنها فیلدهای مورد نیاز سرور را می‌فرستد:

{
"users": [
{
"password": "user-password",
"stats": {
"traffic": {
"up": "12345",
"down": "67890"
}
}
}
]
}

کاربرانی که شمارنده ترافیکشان تغییری نکرده، در آن ارسال قرار نمی‌گیرند.

سرور این شمارنده‌ها را با baseline همان session که GetAllUsers ساخته مقایسه می‌کند و فقط deltaها را اعمال می‌کند. کلاینت time.first-usage-at-ms را نمی‌فرستد. با رسیدن نخستین delta غیرصفر برای کاربری که زمان مرجع اولین استفاده‌اش ثبت نشده، سرور آن فیلد را با ساعت خودش مقداردهی می‌کند.

اگر زمان اولین استفاده هنوز ثبت نشده باشد، authenticationclientUserAccountTraffic() با اولین حسابداری محلی غیرصفر یک flag مخصوص runtime برای همان کاربر می‌گذارد. کلاینت تلاش برای PushUserStats را روی worker شماره 0 در صف قرار می‌دهد. اگر ارسال آمار دیگری در جریان باشد، ارسال بعدی را به تعویق می‌اندازد و فراخوانی‌های بعدی payload را با هم ادغام می‌کند تا زمانی که آمار ارسال‌شده با نمای تازه‌ای از GetAllUsers دنبال شود یا تلاش ارسال شکست بخورد.

اگر سرور needs-pull: true برگرداند، کلاینت دوباره GetAllUsers را درخواست می‌کند تا cache محلی و baseline مربوط به session در سمت سرور به‌روز شوند.

رفتار Lifecycle و جهت‌ها

AuthenticationClient یک نود کنترلی در ابتدای chain است و درخواست‌ها را در جهت upstream می‌فرستد:

tunnelNextUpStreamInit()
tunnelNextUpStreamPayload()
tunnelNextUpStreamFinish()

پاسخ‌های سرور از طریق callbackهای downstream دریافت می‌شوند:

DownStreamEst
DownStreamPayload
DownStreamPause
DownStreamResume
DownStreamFinish

Pause و resume به تونل قبلی بازگردانده نمی‌شوند، چون پیش از AuthenticationClient تولیدکننده داده‌ای وجود ندارد. این callbackها فقط مشخص می‌کنند که آیا می‌توان درخواست تازه‌ای را فوراً در پروتکل فرستاد یا نه.

state مربوط به line کنترلی، stream خواندن پاسخ‌های downstream را نگه می‌دارد. این state پیش از tunnelNextUpStreamInit() مقداردهی اولیه می‌شود و قبل از انتشار finish در جهت upstream یا آزاد شدن line تحت مالکیت کلاینت، از بین می‌رود.

با دریافت finish در جهت downstream، کلاینت:

  1. اشاره‌گر line کنترلی فعال را زیر mutex کنترلی پاک می‌کند.
  2. session token، وضعیت احراز هویت، flagهای in-flight و درخواست‌های pending را پاک می‌کند.
  3. state این تونل برای line را از بین می‌برد.
  4. line کنترلی تحت مالکیت خود را آزاد می‌کند.
  5. مگر هنگام توقف تونل، اتصال دوباره را زمان‌بندی می‌کند.

وقتی کلاینت خودش line کنترلی را می‌بندد، برای مثال هنگام خاموش شدن worker شماره 0 یا پردازش یک پاسخ malformed، ابتدا state محلی line را از بین می‌برد، tunnelNextUpStreamFinish() را می‌فرستد و سپس، اگر line هنوز زنده باشد، آن را آزاد می‌کند.

نکته‌ها

  • AuthenticationClient نیاز به نود next دارد.
  • یک نود control-plane است، نه نودی برای عبور داده.
  • فقط مالک line کنترلی داخلی است که خودش ساخته و تنها همان را آزاد می‌کند.
  • از packet line استفاده نمی‌کند و packet tunnel نیست.
  • سقف اندازه داده درخواست، payload پاسخ و payload پیام بیرونی 16 MiB است.
  • secretها، session tokenها، passwordها و raw users JSON عمداً در verbose logging چاپ نمی‌شوند.
  • املای نام ماژول UpdateUserTraficStatsDiff عمداً همان املای فعلی در کد منبع است.

عملکرد پایگاه داده کاربران

  • جست‌وجوی password متنی فقط یک بار SHA-256 را محاسبه می‌کند و بدون fallback scan از طریق index هش SHA-256 نتیجه را پیدا می‌کند؛ بنابراین هزینه متوسط hit و miss برابر O(1) است. سپس password متنی candidate به‌صورت دقیق مقایسه می‌شود تا حتی در صورت برخورد فرضی SHA-256، احراز هویت به‌شکل fail-closed انجام شود.
  • baseline همگام‌سازی محلی با deep copy بومی usersCopy() تکثیر می‌شود، نه با رفت‌وبرگشت از طریق JSON. فقط پاسخ شبکه‌ای دریافت‌شده برای GetAllUsers همچنان به یک بار parse کردن JSON در جدول فعال نیاز دارد.