background blurbackground mobile blur

1/1/1970

Macam Mana Saya Buat Foony Boleh Jalan Di Belakang Proksi

Hai! Saya dah lama tahu proksi web boleh sebabkan masalah keserasian untuk laman web. Tapi sokongan Foony dekat proksi memang teruk gila, dan nak selesaikan isu keserasian Foony dengan proksi ni agak rumit.

Ini pun bukan isu jenis “Foony guna API pelik-pelik” (walaupun memang kami guna). Sebenarnya ia gabungan beberapa benda:

  • Proksi buat penulisan semula string secara agresif dekat tempat yang langsung tak patut.
  • Proksi layan domain laman utama lain macam berbanding domain “lain” (CDN, hos aset dan sebagainya).
  • Dan realiti pahit bahawa sesetengah proksi memang langsung tak boleh sokong aplikasi web moden (ketepatan HTTPS, WebSocket dan lain-lain).

Foony tak boleh jalan dengan semua proksi, tapi sekarang sekurang-kurangnya ia berfungsi dengan croxyproxy dan proxyorb, yang memang itu sasaran asal.

Dekat bawah ni saya cerita apa yang rosak, kenapa benda tu rosak, dan pembaikan mana yang betul-betul penting.


Fasa 1: Shader Three.js Yang Sah Tapi Tetap Rosak

Simptom

Masa saya cuba croxyproxy, saya tak boleh main 8 Ball Pool atau mana-mana game three.js Foony yang lain. Saya asyik dapat ralat kompilasi shader dalam Three.js dengan mesej macam:

  • “Shader Error 1282 - VALIDATE_STATUS false”

Mesej tu hampir langsung tak berguna. Selalunya ia cuma bermaksud “shader kau tak sah, pandai-pandailah”. Best sangat. Kalau korang tertanya-tanya kenapa saya suka guna mesej ralat yang unik untuk setiap satu ralat dekat Foony, inilah sebabnya. Ia bantu kita cam punca masalah, bukannya sekadar "ada benda rosak, pergi la betulkan sendiri".

Tapi kenapa shader Three.js yang memang sah dan betul pun boleh rosak? Apa hal ni?

Punca sebenar: proksi rosakkan layout(location = N)

Three.js hasilkan kod GLSL dengan layout qualifier macam ni:

layout(location = 0) in vec3 position;

Sesetengah proksi cuba tulis semula apa sahaja yang nampak macam API JavaScript location dengan buat penggantian string secara global yang sangat naif. Itu pun dah cukup teruk dalam JS, tapi mereka siap buat dalam string sumber shader sekali. Agaknya parsing AST terlalu mahal untuk mereka.

Jadi kod sumber shader tu jadi rosak dan bertukar jadi lebih kurang macam ni:

layout(__cpLocation = 0) in vec3 position;

Pengenal tu mesti location dekat situ. Apa saja selain tu dikira GLSL tak sah, dan pengkompil akan tolak. (Layout Qualifiers in GLSL)

Ini cuma “masalah Three.js” dari sudut Three.js jana shader secara dinamik dan kita hantar shader tu ke WebGL masa runtime. Bug sebenar datang daripada cara proksi tu tulis semula kod.

Kenapa saya tak “betulkan proksi”

Cara paling mudah kononnya ialah cari string pengganti location yang croxyproxy guna, iaitu __cpLocation, lepas tu tukar balik jadi location. Tapi proksi lain guna nama pengganti yang lain. Ada yang guna __cpLocation, ada yang guna pengenal pelik lain. Jadi kalau kita keras-kodkan pembaikan macam “ganti __cpLocation balik jadi location”, ia sangat rapuh.

Saya perlukan:

  • Pembaikan yang umum (tanpa keras-kod nama pengenal proksi).
  • Pembaikan yang tetap berfungsi walaupun proksi cuba tulis semula perkataan location dalam JavaScript saya sekali.

Helah base64: sorok perkataan location daripada proksi

Kalau proksi tukar setiap location yang dia nampak, langkah paling senang ialah jangan guna location terus. Senang kan. Saya pernah nampak helah macam ni sebelum ni dalam Lua masa saya reverse-engineer sistem perlindungan panduan RestedXP (kalau saya ingat betul, mereka kaburkan penggunaan BNGetInfo, contohnya _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Helah ni berkesan dalam JavaScript juga. Dalam client/index.html, saya nyahkod benda ni masa runtime:

// Sebab proksi ni cuba ganti setiap `location`, kita guna string yang dienkod base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

atob() tu berlaku lepas proksi siap buat penulisan semula HTML/JS dia, jadi proksi tak boleh “rosakkan awal-awal” string tu. Saya pecahkan string tu kepada dua bahagian supaya lagi susah nak kesan, dan saya guna 'atob' saja sebab boleh, tapi String.fromCharCode atau hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] pun mungkin menjadi.

Corak shader yang rosak tu dari segi struktur sentiasa sama:

layout(<something> = <number>)

Jadi saya padankan pola tu secara umum dan ganti <something> dengan pengenal yang betul:

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

Cangkuk WebGL: tampal shaderSource (WebGL1 + WebGL2)

Sebab Three.js akan panggil gl.shaderSource(shader, source), saya tampal shaderSource tu sendiri:

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,
});

Dan saya guna tampalan yang sama pada WebGL2RenderingContext kalau ia wujud.

Bila benda ni dah siap, ralat kompilasi shader terus hilang. Pada tahap ni, croxyproxy dah berfungsi tapi proxyorb masih gagal. Kenapa pula?! Bukan patutnya dua-dua kerja cara yang sama ke?


Fasa 2: Masalah kedua (domain) dan kenapa buang foony.io buat semua benda jadi lagi senang

Sejak kebelakangan ni, Foony guna dua domain, sekurang-kurangnya untuk sebulan yang lepas:

  • foony.com untuk laman utama
  • foony.io untuk aset statik

Sebab asalnya agak praktikal: bila kita hidang aset daripada domain yang tak ada cookie, kita elak lebihan muatan header cookie setiap kali ada permintaan fail statik. Ini memang bagus, cuma tak sewajib yang korang sangka memandangkan HTTP/2 dah guna HPACK untuk kurangkan bilangan bait header yang dihantar.

Ini satu pengoptimuman yang sah dalam keadaan pelayaran biasa.

Bila di belakang proksi, ia tiba-tiba jadi punca utama benda-benda rosak. Dan pengguna Foony memang suka sangat guna proksi. sigh

Proksi layan “laman utama” lain macam berbanding “laman lain”

Banyak proksi dioptimumkan untuk “proksi satu halaman / satu domain ni saja”. Mereka akan berjaya muatkan HTML utama, suntik skrip mereka sendiri, daftar ServiceWorker mereka, dan sebagainya.

Tapi bila app mula tarik aset daripada origin yang lain (contohnya foony.io), masa tu lah segala macam kerosakan “best” mula muncul:

  • Kegagalan pintasan ServiceWorker, contohnya:
    • “ServiceWorker intercepted the request and encountered an unexpected error”
    • “Loading failed for the module with source”
  • Parameter kueri yang sebenarnya diperlukan oleh infrastruktur proksi (dan sangat senang terbuang tanpa sengaja).
  • Permintaan aset hilang metadata routing dalaman proksi.
  • Proksi pelik yang ganti satu permintaan penuh dengan generic-php-slug.php?someQueryParam=hugeEncodedString (ya, yang tu saya memang tak berusaha nak sokong).

Mekanisme dalaman proksi ni banyak bergantung pada parameter kueri / pengekodan URL, dan benda tu sangat rapuh.

Salah satu contoh yang jelas ialah URL aset macam ni:

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

?__pot=... tu ialah routing/state milik proksi sendiri yang beritahu proksi permintaan tu sebenarnya untuk domain mana. Kalau korang buang bahagian tu, proksi tak boleh selesaikan permintaan dengan betul, dan akhirnya korang akan dapat laluan ralat ServiceWorker.

“Resource swapping” sebagai penyelamat (dan kenapa ia cepat jadi komplikated)

Pernah satu ketika, saya cuba jalan pintas: kesan bila “kita sedang melalui proksi”, lepas tu tukar mana-mana URL sumber foony.io kepada origin semasa supaya proksi nampak semua benda seolah-olah datang dari origin yang sama.

Kedengaran macam ok, dan ia memang berjaya untuk croxyproxy, tapi ia tambah terlalu banyak kerumitan:

  • Kita kena ganti tag link dan script yang memang dah wujud dalam HTML.
  • Kita perlukan MutationObserver untuk urus tag yang disuntik secara dinamik (modulepreload, stylesheet dan sebagainya).
  • Kita kena kekalkan parameter kueri milik proksi, kalau tidak routing mereka akan rosak. Dan setiap proksi buat benda ni dengan cara berbeza. Mestilah kan.
  • Dan dalam masa yang sama kita masih kena kekalkan logik yang umum (tanpa global khusus untuk proksi tertentu) supaya kod tak bertukar jadi timbunan sampah yang membengkak.

Dekat sinilah “helah base64” tadi muncul semula: dalam JavaScript saya sendiri pun saya kena berhati-hati dengan string literal location sebab proksi mungkin tulis semula benda tu.

Reverse-engineer skrip yang CroxyProxy suntik

Pada tahap ni saya jadi curious: apa sebenarnya proksi tu buat dekat halaman saya? Dia suntik iklan dia sendiri ke? Atau benda yang lagi teruk?

Skrip bahagian klien milik CroxyProxy sangat-sangat dikaburkan (obfuscated).

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

Bila kod tu dijalankan, ia jadi macam ni:

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

Daripada sini nampak macam croxyproxy guna Obfuscator.io untuk kaburkan kod mereka. Nasib baik agak senang untuk dinyah-obfuskat guna webcrack.

Hasilnya, kita dapat JavaScript yang jauh lebih senang dibaca:

((_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)) {

Nice. Sekarang kita boleh nampak apa yang dia buat. Dan... nampaknya kebanyakannya ok saja. Saya rasa pengaburan tu lebih kepada nak susahkan orang kesan yang ini sebenarnya proksi. Kebanyakannya lah.

Ada sedikit suntikan iklan / 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;
    }

Ada beberapa bahagian lain juga, tapi asasnya cuma untuk tunjuk iklan, termasuk iklan gaya pop-under. Ia juga guna FuckAdBlock.

Tapi penggantian string yang sebenar-benarnya berlaku di bahagian server. Dan siapa lah yang betul-betul tahu apa lagi yang dia buat dekat situ.

Apa pun, korang memang tak patut guna proksi web kalau korang peduli tentang keselamatan akaun. Kalau terpaksa juga, jangan sekali-kali masukkan maklumat peribadi / akaun / bayaran korang.

"Resource swapping" dibuang masuk tong

Saya akhirnya buat keputusan yang kerumitan daripada resource swapping, ditambah pula dengan kerumitan di bahagian lain dalam kod untuk sokong foony.io, langsung tak berbaloi dengan penjimatan rangkaian kecil hasil permintaan tanpa cookie yang nampak cantik tu. Kami juga nampak penurunan pelik dalam kadar pemain mula bermain sejak kami guna foony.io, jadi saya syak ada isu lain dengan foony.io yang kami sendiri tak sedar.

Jadi saya buang terus foony.io. Sekurang-kurangnya buat masa sekarang.

Bila saya padam logik CDN foony.io dan seragamkan semuanya kepada foony.com sahaja, sokongan untuk proksi jadi jauh lebih ringkas:

  • Muat aset daripada origin yang sama.
  • Kurang “kes khas” yang perlu diterangkan kepada ServiceWorker milik proksi.
  • Kurang penulisan semula.
  • Kod jadi kurang rapuh.

Pendek kata, buang foony.io ialah satu pemudahan seni bina yang mengurangkan ruang untuk tingkah laku pelik daripada proksi.


Fasa 3: Apa yang berfungsi, apa yang tak, dan kenapa

Proksi yang disahkan berfungsi

Pada tahap ni, Foony berfungsi di belakang:

  • croxyproxy
  • proxyorb

Mungkin ada beberapa proksi lain yang turut berfungsi. Tapi saya yakin kebanyakannya masih tak jalan. Sekurang-kurangnya proksi penting yang orang guna untuk main game nampaknya ok.

Kenapa bukan “semua proksi”?

Sesetengah proksi memang langsung tak mampu sokong aplikasi web berbilang pemain yang moden. Contohnya:

  • Proksi yang tak menyokong HTTPS dengan betul.
  • Proksi yang rosakkan atau sekat WebSocket (Foony guna rangkaian masa nyata). Secara teknikal boleh diakali, tapi ia akan tambah kerumitan.
  • Proksi yang meletakkan terlalu banyak sekatan pada permintaan rentas origin, header, atau ServiceWorker.

Perkara utama yang patut korang ingat

Web proxy sangat tak selamat

Proksi ni sebenarnya lapisan tengah yang:

  • tulis semula HTML
  • tulis semula JavaScript
  • kadang-kadang suntik ServiceWorker mereka sendiri
  • dan selalunya bergantung pada parameter kueri / pengekodan URL untuk route permintaan
  • boleh ubah suai halaman korang dengan pelbagai cara yang kita tak tahu

Saya sendiri terkejut sedalam mana sesetengah proksi ni sanggup pergi: mereka sanggup tulis semula string sumber shader, komen, dan entah apa lagi.

Kadang-kadang pembaikan terbaik ialah di peringkat seni bina

Tampalan WebGL buat game boleh render semula, tapi buang strategi CDN berbilang domain lah yang buat sokongan proksi kekal stabil.

Ini satu peringatan yang bagus: pengoptimuman yang nampak bijak boleh jadi elok saja sehinggalah ia berlaga dengan middleware yang “bermusuhan”. Atau extension pelayar pengguna. Atau Safari. Atau tetapan bahasa. Atau ciri kebolehcapaian. Atau ribut suria. Atau apa-apa pun sebenarnya.


Kesimpulan

Sekarang Foony dah berfungsi di belakang proksi yang betul-betul penting (croxyproxy dan proxyorb), tanpa menjadikan kod asas kami bersepah penuh benda khusus untuk proksi:

  • Pembaikan shader Three.js yang umum (tanpa pengenal khusus proksi).
  • Strategi domain yang lebih ringkas (foony.com di mana-mana).
8 Ball Pool online multiplayer billiards icon