work@hossein-assar.devپاسخ در کمتر از ۲۴ ساعتEnglish
حسین عصارحسین عصارطراحی و توسعه وب
بازگشت به نمونه‌کارها
سند مهارت فنی · فول‌استک

پیوند — وی‌پی‌ان معکوس

یک سرویس تجاری فعال که به ایرانیان خارج از کشور آی‌پی ایران می‌دهد، تا بتوانند به بانک‌ها و سامانه‌های دولتی که اتصال از بیرون را رد می‌کنند دسترسی داشته باشند

Node.js · MariaDB · FreeRADIUS · Vercel
صفحه اصلی سایت پیوند

پیوند نه سایت آکادمیک است، نه فروشگاه، نه سایت شرکت تولیدی — و عمداً این‌جاست. آن سه چیزی که می‌فروشم روی کاری بنا شده‌اند که کارفرما هیچ‌وقت نمی‌بیندش: احراز هویت، پرداخت، و داده‌ای که نباید غلط باشد. این پروژه جایی است که آن کار دیده می‌شود. اگر می‌خواهید بدانید کسی که سایت شما را می‌سازد به وب‌هوک پرداخت یا مهاجرت پایگاه داده قابل‌اعتماد است یا نه، همین یکی را بخوانید.

درباره کارفرما

بیشتر وی‌پی‌ان‌ها به آدم‌ها کمک می‌کنند از یک شبکه محدودشده بیرون بروند. پیوند کار برعکس را می‌کند. بانک‌های ایرانی، سامانه ثنا، پورتال‌های دانشگاهی و سامانه‌های بیمه و مالیات، اتصال از آی‌پی خارجی را رد می‌کنند — پس یک ایرانی ساکن خارج نمی‌تواند وارد حساب بانکی خودش شود. پیوند به او آی‌پی ایران می‌دهد تا بتواند. این یک سرویس تجاری واقعی با مشتری پرداخت‌کننده است، نه یک نمونه آزمایشی.

مسئله

وسط ساختن لایه تجاری فهمیدم سرویس‌های شخص‌سومی که داشتم به آن‌ها وصل می‌شدم، از روی سرور در دسترس نیستند. ایران api.telegram.org را در سطح کشور مسدود می‌کند — این را حدس نزدم، بررسی کردم: DNS روی سرور، تلگرام را به 10.10.34.36 ترجمه می‌کرد، همان آدرسی که سامانه فیلترینگ به‌جای آدرس واقعی برمی‌گرداند، در حالی که بقیه اینترنت درست جواب می‌داد. پی‌پال از سمت خودش آی‌پی ایران را مسدود می‌کند. و زرین‌پال از بیرون ایران بالا نمی‌آید — که یک حلقه می‌سازد: مشتری برای خریدن وی‌پی‌ان به وی‌پی‌ان نیاز دارد. سه محدودیت جدا، همه بعد از نوشتن کد کشف شدند.

دامنه پروژه و نقش من

همه‌اش را خودم ساختم: ocserv و FreeRADIUS روی سرور ایران، یک API تخصیص اشتراک با Node و Express، فرانت‌اند و پنل کاربری دوزبانه، یک ربات فروش تلگرام با گردش کار تأیید ادمین، و لایه پرداخت که بعد از فهمیدن این‌که هر ماشین به چه چیزی دسترسی دارد، از صفر بازطراحی شد.

قابلیت‌های اصلی

۰۱سیستم بر اساس این‌که هر بخش به چه چیزی دسترسی دارد تقسیم شده. ایران داده را نگه می‌دارد — دیتابیس، RADIUS، سرویس وی‌پی‌ان و منطق تخصیص اشتراک — و فقط یک اندپوینت احراز هویت‌شده به بیرون می‌دهد. یک لایه لبه در خارج، سرویس‌هایی را نگه می‌دارد که باید با پی‌پال و تلگرام حرف بزنند و درخواست‌های امضاشده را به داخل می‌فرستد.
۰۲ربات تلگرام از long polling به webhook منتقل شد. polling یعنی باز نگه داشتن اتصال به دامنه‌ای که مسدود است؛ webhook جهت را برعکس می‌کند و همین باعث شد نیاز به یک ماشین همیشه‌روشن کاملاً حذف شود — نه سرور دوم، نه تونل، نه هزینه ماهانه.
۰۳تخصیص اشتراک idempotent است. یک تابع، تنها نقطه ورود همه روش‌های پرداخت است: حساب جدید و تمدید را با هم مدیریت می‌کند، از تاریخ انقضای فعلی امتداد می‌دهد نه از صفر، و اگر شناسه تراکنشی را قبلاً دیده باشد کاری نمی‌کند. اضافه کردن یک درگاه پرداخت جدید یعنی نوشتن یک آداپتور و دست‌نزدن به بقیه.
۰۴فرانت‌اند دوزبانه فارسی و انگلیسی با جابه‌جایی کامل راست‌به‌چپ و چپ‌به‌راست، روی یک مجموعه توکن طراحی مشترک بین سایت معرفی و پنل کاربری.
۰۵پنل کاربری با Node و Express و نشست‌های httpOnly، که وضعیت اشتراک و تاریخ سفارش‌ها را نشان می‌دهد.
۰۶ocserv روی پورت ۴۴۳ اجرا می‌شود تا ترافیک شبیه HTTPS معمولی به نظر برسد؛ همین است که اجازه می‌دهد از فایروال‌های سخت‌گیر رد شود.

چالش‌های اصلی

امضای درخواست، تمام مرز امنیتی است

سرور ایران نمی‌تواند هیچ‌چیز را مستقل تأیید کند — نمی‌تواند از پی‌پال بپرسد آیا پرداخت انجام شد، چون پی‌پال مسدود است. باید حرف لایه لبه را باور کند. پس آن یک اندپوینت چهار لایه دارد، نه یکی: HMAC-SHA256 روی زمان، یک nonce و بایت‌های خام دقیق درخواست؛ تازگی زمان، تا درخواست ضبط‌شده بعد از پنج دقیقه منقضی شود؛ nonce یک‌بارمصرف که با کلید یکتای دیتابیس تضمین می‌شود نه با بررسی در کد، تا دو تکرار همزمان نتوانند هر دو موفق شوند؛ و مقایسه در زمان ثابت، با بررسی طول به‌صورت جداگانه و پیش از آن، چون تابع مقایسه در صورت اختلاف طول خطا می‌دهد و همان خطا طول را لو می‌دهد.

پرداختی که دو بار خرج نمی‌شود

امضای webhook تأیید می‌شود، چون آدرس آن عمومی است و بدون تأیید هرکسی می‌تواند «پرداخت انجام شد» بفرستد. مبلغ در سمت سرور با جدول قیمت خودِ سرور مقایسه می‌شود، نه از بدنه درخواست خوانده شود. و تخصیص اشتراک بر اساس شناسه تراکنش درگاه idempotent است، تا تلاش مجدد و webhook تکراری نتوانند یک اشتراک را دو بار تحویل بدهند.

کد خودم را بازبینی کردم و هفت مشکل پیدا کردم، دو تا جدی

یک webhook اگر یک متغیر محیطی جا افتاده بود، کاملاً بدون احراز هویت اجرا می‌شد — کتابخانه در آن حالت بی‌صدا بررسی را رد می‌کند، نه این‌که خطا بدهد. مشکل دوم اجازه می‌داد هرکسی حساب تلگرام خودش را بدون اثبات مالکیت به حساب وی‌پی‌ان یک غریبه وصل کند. هر دو رفع شدند؛ اتصال حالا رمز می‌خواهد، در زمان ثابت مقایسه می‌شود، و پیام خطا عامدانه یکسان است تا نتوان از آن برای فهمیدن این‌که چه نام‌کاربری‌هایی وجود دارند استفاده کرد.

توابع serverless هیچ‌چیز را در حافظه نگه نمی‌دارند

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

فیلتر fail2ban که با لاگ واقعی اعتبارسنجی شد

نه با آن‌چه فکر می‌کردم لاگ می‌نویسد. همین باعث شد حالتی پیدا شود که در آن، نام‌کاربری حاوی متن «auth failed» می‌توانست یک ورود موفق را به‌عنوان ورود ناموفق ثبت کند.

نتیجه

یک سرویس تجاری فعال با مشتری پرداخت‌کننده، که از دو کشور اداره می‌شود. حدود ۱۰۰ تست خودکار، با وزن بیشتر روی حمله‌ها تا مسیرهای موفق: درخواست تکرارشده، داده دست‌کاری‌شده، زمان منقضی و زمان آینده، امضای جعلی، انتقال بدون رمزنگاری با امضای درست، سرقت حساب، و حمله جست‌وجوی فراگیر روی مسیر ورود. راه‌اندازی زرین‌پال، پرداخت با USDT، مسیریابی split-tunnel تا فقط ترافیک بانکی ایران از سرویس بگذرد، و سرور دوم برای پشتیبان‌گیری، در برنامه هستند.

تصاویر پروژه

تصاویر این پروژه در حال آماده‌سازی است.

صفحه اصلی فارسی در peyvand.tech
۰۱ — صفحه فارسی سایت
نسخه انگلیسی، که جابه‌جایی راست‌به‌چپ و چپ‌به‌راست را نشان می‌دهد
۰۲ — همان صفحه به انگلیسی
پنل کاربری با وضعیت اشتراک و تاریخ سفارش‌ها
۰۳ — پنل کاربری
رسیدی که در ربات می‌آید، با دکمه‌های تأیید و رد
۰۴ — گردش تأیید تلگرام