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 نشدهاند را هنگام جایگزینی کامل جدول حفظ میکند.
- درخواستهای
Pingauthenticated دورهای میفرستد. - درخواستهای دورهای
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
}
فیلدهای اجباری
فیلدهای سطح اصلی:
| فیلد | نوع | توضیح |
|---|---|---|
name | string | نام انتخابی نود. نودهای دیگر با این نام به auth client ارجاع میدهند. |
type | string | باید دقیقاً "AuthenticationClient" باشد. |
next | string | اجباری. نود بعدی باید به یک AuthenticationServer منتهی شود. |
settings | object | مشخصات احراز هویت و تنظیمات timerها. |
فیلدهای اجباری داخل settings:
| فیلد | نوع | توضیح |
|---|---|---|
name | non-empty string | نام کلاینت control-plane که برای AuthenticationServer فرستاده میشود. |
secret | non-empty string | secret مربوط به control-plane که همراه settings.name فرستاده میشود. |
name و secret مربوط به control-plane هستند و مشخصات ورود کاربران ترافیک به شمار نمیآیند.
تنظیمات اختیاری
| گزینه | نوع | پیشفرض | توضیح |
|---|---|---|---|
ping-interval-ms | صفر یا عدد مثبت | 60000 | Ping احرازشده میفرستد. اگر اتصال برقرار باشد ولی احراز هویت انجام نشده باشد، 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 | بیشترین تعداد درخواستهای همزمانِ در حال پردازش در پروتکل. |
verbose | boolean | false | لاگهای 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:
- کلاینت را در حالت started قرار میدهد.
- اگر ping timer فعال باشد، آن را راه میاندازد.
- اگر دریافت یا ارسال دورهای فعال باشد، sync timer را شروع میکند.
- line کنترلی تحت مالکیت خود را میسازد.
- state مخصوص این تونل را برای line مقداردهی اولیه میکند.
- تابع
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 انجام میشود:
- پاسخ JSON خوانده و به یک
users_tتازه تبدیل میشود. - جدول تازه اعتبارسنجی میشود؛ از جمله اینکه rangeهای همپوشان در
wireguard-allowed-ipsپذیرفته نمیشوند. - deltaهای ترافیک محلیِ syncنشده و شمارندههای runtime مخصوص همان process، با durable user id منتقل میشوند؛ برای کاربران قدیمی از SHA-256 بهعنوان fallback استفاده میشود.
- deadline محلی انقضای هر کاربر از فیلدهای زمانی متعلق به سرور و مقدار
server-time-msدر پاسخ محاسبه میشود. - اشارهگر جدول فعال زیر write lock مربوط به rwlock جابهجا میشود.
- شمارنده generation افزایش مییابد.
- جدول قدیمی پس از جابهجایی آزاد میشود.
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 کنترلی احرازشده ندارد. |
kAuthenticationClientStateAuthenticating | transport متصل است یا token وجود دارد، اما جدول کامل کاربران هنوز بارگذاری نشده است. |
kAuthenticationClientStateReady | کلاینت احراز هویت شده و جدول کاربران را نصب کرده است. |
authenticationclientIsReady() فقط در kAuthenticationClientStateReady مقدار true دارد.
جستوجوی password که نتیجه تفصیلی میدهد، ممکن است یکی از این مقادیر را برگرداند:
| نتیجه | معنی |
|---|---|
ok | lookup موفق بود. |
invalid password lookup | آرگومان نامعتبر، password خالی یا نبودن handle خروجی. |
password hash failed | محاسبه SHA-256 شکست خورد. |
users table unavailable | جدول محلی کاربران هنوز بارگذاری نشده است. |
user not found | کاربری با این password پیدا نشد. |
password mismatch | hash منطبق بود، اما بررسی 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، کلاینت:
- اشارهگر line کنترلی فعال را زیر mutex کنترلی پاک میکند.
- session token، وضعیت احراز هویت، flagهای in-flight و درخواستهای pending را پاک میکند.
- state این تونل برای line را از بین میبرد.
- line کنترلی تحت مالکیت خود را آزاد میکند.
- مگر هنگام توقف تونل، اتصال دوباره را زمانبندی میکند.
وقتی کلاینت خودش 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 در جدول فعال نیاز دارد.