Amirali Yaghoutiمهندس ارشد نرم‌افزار

مطالعه موردی مهندسی

A2 خروجی شبکه فایروال

wp-admin مرتب ده ثانیه یا بیشتر طول می‌کشید تا یک صفحه بالا بیاید و لاگ کوئری هم چیزی را توضیح نمی‌داد. زمان صرف این می‌شد که WordPress صبورانه منتظر فراخوان‌های HTTP خروجی بماند: سرور لایسنس افزونه‌ها، CDN فونت و اندپوینت‌های آپدیت که از محل میزبانی فروشگاه کند هستند یا اصلاً در دسترس نیستند، به‌علاوه‌ی درخواست‌هایی که WordPress برای cron و REST API به نام میزبان خودش می‌زند. این نوشته هم تشخیصی است که نشان داد زمان کجا می‌رود، هم فایروالی که از دل آن بیرون آمد.

مسئله تجاری

ادمین کند بود، دیتابیس گلوگاه نبود و لاگ کوئری‌های کند هم چیز خاصی نشان نمی‌داد. این ترکیب معمولاً یعنی درخواست پشت چیزی غیر از دیتابیس منتظر مانده. یک نصب WordPress در یک درخواست ادمین تعداد عجیبی فراخوان خروجیِ مسدودکننده می‌زند: بررسی آپدیت، اعتبارسنجی لایسنس، کتابخانه‌ی قالب‌ها، Gravatar، API فونت. علاوه بر این‌ها، برای WP-Cron و REST API و بررسی سلامت، با HTTP API آدرس خودش را هم صدا می‌زند؛ و یک قانون فایروال یا یک رفتار عجیب DNS یا یک پراکسی می‌تواند همین فراخوان داخلی را به تایم‌اوت کامل تبدیل کند. از یک میزبان ایرانی، چند تا از مقصدهای بیرونی به‌جای شکست سریع تایم‌اوت می‌شوند و هرکدام کل تایم‌اوتش را به زمان صفحه اضافه می‌کند. داشبورد خراب به‌نظر می‌رسید، در حالی که تک‌تک اجزا دقیقاً طبق طراحی کار می‌کردند.

آنچه تحویل دادم

  • ردیابی تک‌تک فراخوان‌های خروجی WordPress در یک درخواست ادمین، و جدا کردن فراخوان‌های واقعاً بیرونی از مواردی که سایت خودش را صدا می‌زند؛ تا اصلاح دقیقاً سراغ لایه‌ای برود که واقعاً کند بود.
  • a2-egress-firewall-adminspeed.php: یک بلاک‌لیست، نه اجازه‌لیست؛ تا با نصب افزونه چیزی که الان کار می‌کند نشکند.
  • فهرستی گزیده از مقصدهایی که از این میزبان معلوم است گیر می‌کنند: اندپوینت لایسنس و آپدیت افزونه‌ها، کتابخانه‌های قالب، APIهای فونت و آواتار، و یکی دو CDN.
  • یک مسیر شکست سریع برای هر چیزی که مستقیم بلاک نشده: تایم‌اوت اتصال ۱ ثانیه و تایم‌اوت کل ۲ ثانیه؛ یک توقف نامرئی تبدیل می‌شود به یک خطای سریع و دیدنی.
  • یک ماژول همراه برای فرانت، a2-egress-assets-firewall.php، که URL اسکریپت و استایل خارجی را در script_loader_src و style_loader_src می‌گیرد؛ به‌علاوه‌ی یک حالت پایش که فقط لاگ می‌کند و چیزی را نمی‌بندد.
  • یک کلید bypass و یک سوئیچ فقط-ادمین، تا وقتی چیزی واقعاً باید بیرون برود، همه‌ی این‌ها برای یک درخواست خاموش شود.

رویکرد فنی

  • سؤال اول این نبود چطور سریع‌ترش کنیم؛ این بود که درخواست منتظر چیست. جواب دادن به آن یعنی ابزارگذاری روی لایه‌ی HTTP، نه لایه‌ی کوئری. یافته‌ها هم به‌جای ماندن در یک گزارش، مستقیم وارد فایروال شدند تا تشخیص و اصلاح در یک مسیر کد بمانند.
  • بلاک در pre_http_request انجام می‌شود و یک پاسخ ساختگی برمی‌گرداند، پیش از آنکه اصلاً سوکتی باز شود. هیچ‌چیز منتظر نمی‌ماند.
  • تطبیق میزبان بر پایه‌ی پسوندِ هاست پارس‌شده است، نه جست‌وجوی زیررشته در URL؛ تا دامنه‌ی بلاک‌شده نتواند از داخل یک query string رد شود.
  • نام‌های میزبان خود سایت و هر چیزی که محلی resolve می‌شود صراحتاً معاف‌اند، چون WP-Cron و REST API با همین درخواست‌های loopback با خودشان حرف می‌زنند. تشخیص میزبان هر دو شکل www و بدون www آدرس‌های home و site را پوشش می‌دهد، به‌علاوه‌ی localhost و نشانی loopback؛ چون درخواست loopback به گونه‌ی اشتباهِ دامنه دقیقاً همان حالتی است که می‌شکند.
  • پیش‌فرض‌ها عمداً محافظه‌کارند: اعمال فقط در ادمین، لاگ خاموش، و فایروال اسِت در حالت پایش عرضه شد تا پیش از تصمیم به بستن، لاگ را بخوانم. بستن هر مقصد جداگانه و فقط وقتی لاگ‌ها آن را توجیه کنند فعال می‌شود.
  • یک مقصد در سورس مستند شده که عمداً بلاک نشده: میزبانی ایرانی که عادی جواب می‌دهد و بستنش یک یکپارچه‌سازی قیمت را می‌شکند.

نتیجه و شواهد

رد کندی به فراخوان‌های خروجی رسید، نه به WordPress و نه به دیتابیس؛ همین کل کار را از تیونینگ کوئری دور کرد، کاری که هیچ کمکی نمی‌کرد. صفحات ادمین دیگر روی فراخوان‌های خروجی گیر نمی‌کنند. میانگین تمیز قبل و بعد را اندازه نگرفتم، چون تأخیر ذاتاً متغیر بود و به این بستگی داشت که آن روز کدام اندپوینت‌ها در دسترس نیستند. چیزی که قابل دفاع است این است: خود این حالت شکست از بین رفته، چون مقصد بلاک‌شده حالا فوری جواب می‌دهد و یک تایم‌اوت کامل نمی‌خورد؛ و تشخیص میزبانِ امن برای loopback هم داخل همین فایروال در پروداکشن کار می‌کند.

اهمیت برای کارفرما

این فرق داشبوردی است که کارکنان از آن فرار می‌کنند با داشبوردی که واقعاً استفاده می‌شود. ضمناً وابستگی فروشگاه به در دسترس بودن سرویس‌های ثالث کم شد: قطعی یک فروشنده‌ی افزونه دیگر قطعی پنل ما نیست. وقتی هم که هدر نرفت به همان اندازه مهم است: بدون مرحله‌ی تشخیص، قدم بعدیِ بدیهی کار روی دیتابیسی بود که هیچ ایرادی نداشت.

خلاصه-اجرایی-پروژه

خلاصه اجرایی خوانا

خلاصه_اجرایی {
  پروژه: "A2 Egress Firewall"
  پرسش: "درخواست ادمین روی چه چیزی مسدود است؟"
  روش: "ابزارگذاری روی لایه‌ی HTTP، نه لایه‌ی کوئری"
  یافته: "فراخوان‌های خروجی، نه DB؛ loopback باید معاف بماند"
  فایل‌ها: "a2-egress-firewall-adminspeed.php (۲۲۴ خط)
            a2-egress-assets-firewall.php (۸۴ خط)"
  راهبرد: "بلاک‌لیست، نه اجازه‌لیست"
  هوک: "pre_http_request → پاسخ ساختگی، بدون سوکت"
  شکست_سریع: "اتصال ۱ثانیه / کل ۲ثانیه برای بقیه"
  دامنه: "فقط wp-admin به‌طور پیش‌فرض؛ کلید bypass موجود"
  فرانت: "script_loader_src + style_loader_src، حالت پایش"
  معاف: "هاست‌های home_url و site_url، گونه‌های www،
         localhost و loopback، همیشه"
}

ارزش حرفه‌ای این پروژه

تشخیص همان بخشی از کار کارایی است که معمولاً رد می‌شود، و رد کردنش دقیقاً همان‌جایی است که یک تیم یک ماه لایه‌ی اشتباه را بهینه می‌کند. ترجیح می‌دهم یک روز خرج اثبات اینکه زمان کجا می‌رود بکنم تا یک هفته خرج سریع‌کردن چیزی که اصلاً کند نبوده.

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