background blurbackground mobile blur

1/1/1970

چطور Foony را پشت پراکسی‌ها به کار انداختم

سلام! مدت‌هاست می‌دانم که پراکسی‌های وب برای وب‌سایت‌ها مشکلات سازگاری ایجاد می‌کنند. با این حال، پشتیبانی Foony در پراکسی‌ها به طرز بدنامی ضعیف بوده، و حل سازگاری Foony با پراکسی‌ها کار نسبتاً پیچیده‌ای بود.

این هم یک مشکل از نوع «Foony از APIهای عجیب و غریب استفاده می‌کند» نیست (هرچند که استفاده می‌کنیم). ترکیبی بود از:

  • پراکسی‌هایی که در جاهایی که مطلقاً نباید، بازنویسی تهاجمی رشته‌ها انجام می‌دهند.
  • پراکسی‌هایی که با دامنه اصلی سایت متفاوت از دامنه‌های «دیگر» (CDNها، میزبان‌های منابع و غیره) رفتار می‌کنند.
  • و این واقعیت تلخ که برخی پراکسی‌ها به سادگی نمی‌توانند از وب‌اپلیکیشن‌های مدرن پشتیبانی کنند (درستی HTTPS، WebSockets و غیره).

ما با هر پراکسی کار نمی‌کنیم، اما اکنون حداقل با croxyproxy و proxyorb کار می‌کنیم، که هدف هم همین بود.

در ادامه توضیح می‌دهم چه چیزی خراب شد، چرا خراب شد، و راه‌حل‌هایی که واقعاً مهم بودند.


مرحله ۱: شیدرهای Three.js معتبر اما خراب

علامت

وقتی croxyproxy را امتحان کردم، نتوانستم بیلیارد ۸ توپ یا هیچ‌یک از بازی‌های three.js دیگر Foony را بازی کنم. مرتب با خطای کامپایل شیدر در Three.js مواجه می‌شدم با خطاهایی مثل:

  • «Shader Error 1282 - VALIDATE_STATUS false»

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

اما چرا شیدرهای کاملاً معتبر three.js خراب می‌شدند؟ چه خبر است؟

علت واقعی: پراکسی‌هایی که layout(location = N) را خراب می‌کنند

Three.js کد GLSL را با کوالیفایرهای layout مانند زیر تولید می‌کند:

layout(location = 0) in vec3 position;

برخی پراکسی‌ها سعی می‌کنند هر چیزی را که شبیه به API جاوااسکریپت location به نظر می‌رسد با انجام جایگزینی ساده و سراسری رشته بازنویسی کنند. این کار حتی در JS هم بد است، اما آن‌ها این کار را داخل رشته‌های منبع شیدر هم انجام می‌دادند. حدس می‌زنم پارس کردن AST برایشان بیش از حد گران است.

پس منبع شیدر به چیزی شبیه این تبدیل می‌شد:

layout(__cpLocation = 0) in vec3 position;

شناسه آنجا باید location باشد. هر چیز دیگری GLSL نامعتبر است و کامپایلر آن را رد می‌کند. (کوالیفایرهای Layout در GLSL)

این فقط به این معنا یک مشکل Three.js است که Three.js شیدرها را به صورت پویا تولید می‌کند، و ما آن‌ها را در زمان اجرا به WebGL پاس می‌دهیم. باگ واقعی، استراتژی بازنویسی پراکسی است.

چرا «پراکسی را درست نکردم»

یک رویکرد ساده‌لوحانه این بود که رشته جایگزین location در croxyproxy، یعنی __cpLocation را جستجو کنم و آن را با location جایگزین کنم. اما پراکسی‌های مختلف از نام‌های جایگزین متفاوتی استفاده می‌کنند. برخی از __cpLocation استفاده می‌کنند، دیگران از شناسه‌های عجیب دیگری. پس کدنویسی سخت یک راه‌حل مانند «جایگزینی __cpLocation به location» شکننده است.

من به این نیاز داشتم:

  • یک راه‌حل عمومی (بدون کدنویسی سخت شناسه‌های پراکسی).
  • راه‌حلی که حتی اگر پراکسی کلمه location را در جاوااسکریپت من هم بازنویسی کند، کار کند.

ترفند base64: پنهان کردن کلمه location از پراکسی

اگر پراکسی هر location تحت‌اللفظی را که می‌بیند بازنویسی می‌کند، ساده‌ترین حرکت این است که اصلاً از location استفاده نکنیم. به اندازه کافی آسان است. قبلاً ترفندهایی برای این کار را در Lua دیده‌ام وقتی که سیستم محافظت از راهنمای RestedXP را مهندسی معکوس می‌کردم (اگر درست به خاطر داشته باشم، استفاده‌شان از BNGetInfo را مبهم می‌کنند، مثلاً _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

البته این ترفند در جاوااسکریپت هم کار می‌کند. در client/index.html، موارد زیر را در زمان اجرا رمزگشایی می‌کنم:

// Because these proxies try to replace every `location`, we use a base64 encoded string.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

آن atob() بعد از اینکه پراکسی بازنویسی HTML/JS خود را انجام داده اتفاق می‌افتد، پس نمی‌تواند رشته را «از پیش خراب» کند. رشته را به دو قسمت تقسیم می‌کنم تا تشخیص آن سخت‌تر شود، و از 'atob' استفاده می‌کنم چون می‌توانم، اما String.fromCharCode یا فرار هگزی window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] هم ممکن است کار کند.

الگوی شیدر خراب همیشه از نظر ساختاری یکسان است:

layout(<something> = <number>)

پس آن را به صورت عمومی تطبیق می‌دهم و <something> را با شناسه صحیح جایگزین می‌کنم:

source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');

قلاب WebGL: پچ کردن shaderSource (WebGL1 + WebGL2)

از آنجایی که Three.js gl.shaderSource(shader, source) را فراخوانی می‌کند، خود shaderSource را پچ می‌کنم:

const originalShaderSource = WebGLRenderingContext.prototype.shaderSource;

Object.defineProperty(WebGLRenderingContext.prototype, 'shaderSource', {
  value: function (shader, source) {
    return originalShaderSource.call(this, shader, fixCorruptedShaderSource(source));
  },
  writable: true,
  configurable: true,
});

و در صورت وجود، همین پچ را روی WebGL2RenderingContext نیز اعمال می‌کنم.

به محض اینکه این کار انجام شد، خطاهای کامپایل شیدر ناپدید شدند. در این مرحله، croxyproxy کار می‌کرد اما proxyorb هنوز خراب بود. چرا؟! نباید به همان شکل کار کند؟


مرحله ۲: مشکل دوم (دامنه‌ها) و چرا حذف foony.io همه چیز را آسان‌تر کرد

Foony از نظر تاریخی از دو دامنه استفاده می‌کرد، حداقل در ماه گذشته:

  • foony.com برای سایت اصلی
  • foony.io برای منابع استاتیک

دلیل اصلی عملی بود: ارائه منابع از یک دامنه بدون کوکی، از تورم آپلود هدر کوکی در هر درخواست فایل استاتیک جلوگیری می‌کند. این عالی است، اما آنقدرها هم که فکر می‌کنید ضروری نیست با توجه به اینکه HTTP/2 از HPACK برای کاهش بایت‌های ارسال‌شده روی سیم برای هدرها استفاده می‌کند.

این یک بهینه‌سازی معتبر در مرور عادی است.

پشت پراکسی‌ها، این به منبع اصلی خرابی تبدیل شد. و کاربران Foony عاشق پراکسی‌ها هستند. آه

پراکسی‌ها با «سایت اصلی» متفاوت از «سایت‌های دیگر» رفتار می‌کنند

بسیاری از پراکسی‌ها برای «پراکسی کردن این یک صفحه / دامنه» بهینه شده‌اند. آن‌ها با موفقیت HTML اصلی را بارگیری می‌کنند، اسکریپت‌ها را تزریق می‌کنند، ServiceWorker خود را ثبت می‌کنند و غیره.

اما وقتی برنامه شروع به کشیدن منابع از یک origin متفاوت (مانند foony.io) می‌کند، با انواع و اقسام خرابی‌های جالب مواجه می‌شوید:

  • خرابی‌های رهگیری ServiceWorker مانند:
    • «ServiceWorker intercepted the request and encountered an unexpected error»
    • «Loading failed for the module with source»
  • پارامترهای کوئری که توسط زیرساخت پراکسی مورد نیاز است (و به راحتی می‌توان به‌طور تصادفی آن‌ها را حذف کرد).
  • درخواست‌های منابع که فراداده مسیریابی داخلی پراکسی را از دست می‌دهند.
  • پراکسی‌های عجیبی که کل درخواست را با generic-php-slug.php?someQueryParam=hugeEncodedString جایگزین می‌کنند (آره، زحمت پشتیبانی از آن را به خودم ندادم).

مکانیزم‌های داخلی این پراکسی‌ها به پارامترهای کوئری / URL encoding وابسته هستند، و کاملاً شکننده‌اند.

یکی از نمونه‌های بارز، یک URL منبع به این شکل بود:

https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20

آن ?__pot=... مسیریابی/وضعیت خود پراکسی است که به پراکسی می‌گوید درخواست برای کدام دامنه است. اگر آن را حذف کنید، پراکسی‌ها نمی‌توانند درخواست را به درستی حل کنند، و در نهایت به مسیر خطای ServiceWorker می‌رسید.

«تعویض منابع» به نجات (و چرا به سرعت پیچیده شد)

در یک نقطه، یک راه‌حل را امتحان کردم: تشخیص اینکه «ما پراکسی شده‌ایم»، سپس تعویض هر URL منبع foony.io به origin فعلی تا پراکسی همه چیز را به عنوان same-origin ببیند.

این منطقی به نظر می‌رسد، و برای croxyproxy کار کرد، اما پیچیدگی زیادی اضافه کرد:

  • باید تگ‌های link و script را که قبلاً در HTML وجود دارند جایگزین کنید.
  • به یک MutationObserver نیاز دارید تا با تگ‌های تزریق‌شده پویا (modulepreload، stylesheet و غیره) برخورد کند.
  • باید پارامترهای کوئری پراکسی را حفظ کنید، وگرنه مسیریابی‌شان را خراب می‌کنید. و پراکسی‌های مختلف این کار را به شکل متفاوتی انجام می‌دهند. چون البته که این کار را می‌کنند.
  • و هنوز باید منطق را عمومی نگه دارید (بدون globalهای مخصوص پراکسی) تا کد تبدیل به یک آشغال‌دانی متورم نشود.

این جایی هم بود که «ترفند base64» دوباره مطرح شد: حتی در جاوااسکریپت خودم، باید مراقب رشته تحت‌اللفظی location باشم چون پراکسی ممکن است آن را بازنویسی کند.

مهندسی معکوس اسکریپت تزریق‌شده CroxyProxy

در این مرحله کنجکاو شدم: پراکسی واقعاً چه کاری با صفحه من انجام می‌دهد؟ آیا تبلیغات خود را تزریق می‌کند؟ چیزی بدتر؟

اسکریپت سمت کلاینت CroxyProxy به شدت مبهم است.

(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();

که وقتی اجرا می‌شود، نتیجه‌اش این است:

function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...

بر اساس این، به نظر می‌رسد croxyproxy از Obfuscator.io برای این مبهم‌سازی استفاده می‌کند. که خوشبختانه به اندازه کافی آسان است که با webcrack از حالت مبهم خارج شود.

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

((_0x15ca2c, _0x489eaa) => {
  if (typeof module == "object" && module.exports) {
    module.exports = _0x489eaa(require("./punycode"), require("./IPv6"), require("./SecondLevelDomains"));
  } else if (typeof define == "function" && define.amd) {
    define(["./punycode", "./IPv6", "./SecondLevelDomains"], _0x489eaa);
  } else {
    _0x15ca2c.URI = _0x489eaa(_0x15ca2c.punycode, _0x15ca2c.IPv6, _0x15ca2c.SecondLevelDomains, _0x15ca2c);
  }
})(this, function (_0x724467, _0x275183, _0x219d84, _0x2dcc2c) {
  var _0x114c1e = _0x2dcc2c && _0x2dcc2c.URI;
  function _0x5a9187(_0x2fd4ea, _0x3bd460) {
    var _0x23a83f = arguments.length >= 1;
    if (!(this instanceof _0x5a9187)) {

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

ما کمی تزریق تبلیغات / UI داریم:

    Bi(_0x308e2f) {
      console.log("Ads: " + _0x331b11.showAds);
      console.log("Ad codes: " + !!_0x331b11.adsJson);
      console.log("Adblock: " + _0x308e2f);
      _0x34fa18.document.body.insertAdjacentHTML("afterbegin", _0x331b11.modal);
      if (_0x331b11.header) {
        if (_0x331b11.header) {
          this.Ri(_0x308e2f);
        }
        [...document.querySelectorAll("#__cpsHeader a")].forEach(_0xcce595 => {
          _0xcce595.addEventListener("click", function (_0x3ef5f1) {
            if (this.target === "_blank") {
              _0x34fa18.open(this.href, "_blank").focus();
            } else {
              _0x34fa18.location.href = this.href;
            }
            _0x3ef5f1.stopImmediatePropagation();
            _0x3ef5f1.preventDefault();
          }, true);
        });
      }
      return this;
    }

نقاط دیگری هم هست، اما اساساً فقط تبلیغات نشان می‌دهد، از جمله تبلیغات سبک pop-under. همچنین از FuckAdBlock استفاده می‌کند.

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

در هر صورت، اگر به امنیت حساب خود اهمیت می‌دهید، مطلقاً نباید از پراکسی‌های وب استفاده کنید. اگر مجبورید، از وارد کردن هرگونه اطلاعات PII / حساب / خرید خود اجتناب کنید.

«تعویض منابع» در سطل آشغال

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

پس foony.io را حذف کردم. حداقل برای الان.

به محض اینکه منطق CDN foony.io را حذف کردم و همه چیز را روی foony.com استاندارد کردم، پشتیبانی از پراکسی به طرز چشمگیری ساده‌تر شد:

  • بارگیری منابع same-origin.
  • موارد «خاص» کمتر برای توضیح به ServiceWorker پراکسی.
  • بازنویسی کمتر.
  • کد کمتر شکننده.

به طور خلاصه، حذف foony.io یک ساده‌سازی معماری بود که سطح آسیب‌پذیری برای رفتار عجیب پراکسی را کاهش داد.


مرحله ۳: چه چیزی کار می‌کند، چه چیزی نمی‌کند، و چرا

پراکسی‌های تأیید شده در حال کار

در این مرحله، Foony پشت این موارد کار می‌کند:

  • croxyproxy
  • proxyorb

برخی پراکسی‌های دیگر احتمالاً کار می‌کنند. شرط می‌بندم بیشترشان هنوز کار نمی‌کنند. اما حداقل آن‌های مهمی که مردم برای بازی کردن استفاده می‌کنند، کار می‌کنند.

چرا «همه پراکسی‌ها» نه؟

برخی پراکسی‌ها به سادگی نمی‌توانند از یک وب‌اپلیکیشن چندنفره مدرن پشتیبانی کنند. مثال‌ها:

  • پراکسی‌هایی که به درستی از HTTPS پشتیبانی نمی‌کنند.
  • پراکسی‌هایی که WebSockets را خراب یا مسدود می‌کنند (Foony از شبکه‌سازی بلادرنگ استفاده می‌کند). از نظر فنی می‌توانید این را دور بزنید، اما پیچیدگی اضافه می‌کند.
  • پراکسی‌هایی که محدودیت‌های زیادی در مورد درخواست‌های cross-origin، هدرها یا ServiceWorkers دارند.

نکات کلیدی

پراکسی‌های وب بسیار ناامن هستند

آن‌ها میان‌افزاری هستند که:

  • HTML را بازنویسی می‌کنند
  • جاوااسکریپت را بازنویسی می‌کنند
  • گاهی یک ServiceWorker تزریق می‌کنند
  • و اغلب به پارامترهای کوئری / URL encoding برای مسیریابی درخواست‌ها وابسته هستند
  • می‌توانند به هر شکلی با صفحات شما بازی کنند

از عمقی که برخی پراکسی‌ها می‌روند شگفت‌زده شدم: آن‌ها رشته‌های منبع شیدر، کامنت‌ها، و خدا می‌داند چه چیزهای دیگری را بازنویسی می‌کنند.

گاهی بهترین راه‌حل، معماری است

پچ WebGL باعث شد بازی‌ها دوباره رندر شوند، اما حذف استراتژی CDN چنددامنه‌ای باعث شد پشتیبانی از پراکسی پایدار بماند.

یادآوری خوبی است: بهینه‌سازی‌های هوشمندانه می‌توانند کاملاً منطقی باشند تا زمانی که با میان‌افزار خصمانه برخورد کنند. یا افزونه‌های مرورگر کاربر. یا Safari. یا تنظیمات زبان. یا ویژگی‌های دسترسی‌پذیری. یا شعله‌های خورشیدی. یا واقعاً هر چیزی.


نتیجه‌گیری

Foony اکنون پشت پراکسی‌هایی که مهم هستند (croxyproxy و proxyorb) کار می‌کند، بدون اینکه پایگاه کد را به یک آشفتگی مخصوص پراکسی تبدیل کند:

  • یک رفع عمومی برای شیدر Three.js (بدون شناسه‌های مخصوص پراکسی).
  • یک استراتژی دامنه ساده‌تر (foony.com همه جا).
8 Ball Pool online multiplayer billiards icon