background blurbackground mobile blur

1/1/1970

Foony'yi Proxy'lerin Arkasında Nasıl Çalıştırdım

Selam! Web proxy'lerin web siteleri için uyumluluk sorunlarına yol açtığını uzun zamandır biliyordum. Ancak Foony'nin proxy'lerdeki desteği fena halde kötüydü ve Foony'nin proxy uyumluluğunu çözmek epey zor oldu.

Bu, "Foony egzotik API'ler kullanıyor" meselesi de değil (gerçi öyle). Şunların bir karışımıydı:

  • Proxy'lerin, kesinlikle yapmamaları gereken yerlerde agresif string yeniden yazımı yapması.
  • Proxy'lerin ana site domain'ine "diğer" domain'lerden (CDN'ler, asset host'ları vb.) farklı davranması.
  • Ve bazı proxy'lerin modern web uygulamalarını (HTTPS doğruluğu, WebSocket'ler vb.) destekleyememe gerçeği.

Her proxy ile çalışmıyoruz, ama artık en azından croxyproxy ve proxyorb ile çalışıyoruz, hedef de buydu.

Aşağıda neyin bozulduğunu, neden bozulduğunu ve gerçekten önemli olan düzeltmeleri anlatıyorum.


1. Geçiş: Geçerli ama bozuk Three.js shader'ları

Belirti

croxyproxy'yi denediğimde 8 Ball Pool ya da Foony'nin diğer three.js oyunlarından hiçbirini oynayamıyordum. Three.js'te şuna benzer hatalarla shader derleme hatası alıp duruyordum:

  • "Shader Error 1282 - VALIDATE_STATUS false"

Bu mesaj neredeyse tamamen işe yaramazdı. Genelde "shader'ın geçersiz, bol şans" anlamına geliyor. Harika. Foony'de neden her tek hata için benzersiz hata mesajları kullandığımı merak ediyorsanız, sebebi bu. Sadece "kod bozuk, git düzelt" demek yerine sorunları tam olarak belirlemeye yardımcı oluyor.

Peki neden mükemmel geçerli three.js shader'ları bozuluyordu? Ne oluyordu?

Asıl sebep: proxy'lerin layout(location = N) ifadesini bozması

Three.js, şuna benzer layout qualifier'larıyla GLSL üretiyor:

layout(location = 0) in vec3 position;

Bazı proxy'ler, JavaScript location API'sine benzeyen her şeyi naive global string değiştirme yaparak yeniden yazmaya çalışıyor. Bu zaten JS'te kötü bir şey, ama bunu shader kaynak string'lerinin içinde de yapıyorlardı. Sanırım AST parsing onlar için fazla pahalı.

Yani shader kaynağı şuna benzer bir şekle bozuluyordu:

layout(__cpLocation = 0) in vec3 position;

Identifier orada mutlaka location olmak zorunda. Başka herhangi bir şey geçersiz GLSL'dir ve derleyici reddeder. (GLSL'de Layout Qualifier'lar)

Bu Three.js'in bir sorunu sadece şu anlamda: Three.js shader'ları dinamik olarak üretiyor ve biz bunları çalışma zamanında WebGL'e geçiyoruz. Asıl bug proxy'nin yeniden yazma stratejisinde.

Neden "proxy'yi düzeltmedim"

Naive bir yaklaşım, croxyproxy'nin location yerine koyduğu __cpLocation string'ini arayıp location ile değiştirmek olurdu. Ancak farklı proxy'ler farklı yer değiştirme isimleri kullanıyor. Bazıları __cpLocation kullanıyor, diğerleri başka tuhaf identifier'lar kullanıyor. Yani "__cpLocation'ı tekrar location yap" gibi bir düzeltmeyi sabit kodlamak kırılgan.

Bana gereken:

  • Genel bir düzeltme (proxy identifier'larını sabit kodlamadan).
  • Proxy benim JavaScript'imde de location kelimesini yeniden yazsa bile çalışan bir düzeltme.

base64 numarası: location kelimesini proxy'den gizlemek

Eğer proxy gördüğü her literal location'ı yeniden yazıyorsa, en basit hamle location'ı hiç kullanmamak. Yeterince kolay. Bu numaranın benzerini daha önce Lua'da görmüştüm, RestedXP'nin guide koruma sistemini tersine mühendislik yaparken (yanlış hatırlamıyorsam, BNGetInfo kullanımlarını gizliyorlardı, ör. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Bu numara JavaScript'te de çalışıyor tabii. client/index.html'de aşağıdakini çalışma zamanında decode ediyorum:

// Bu proxy'ler her `location`'ı değiştirmeye çalıştığı için base64 ile encode edilmiş bir string kullanıyoruz.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

O atob(), proxy HTML/JS yeniden yazmasını yaptıktan sonra çalışıyor, yani string'i "önceden bozma" şansı yok. Tespit edilmesini daha da zorlaştırmak için string'i ikiye bölüyorum ve 'atob' kullanıyorum çünkü kullanabilirim, ama String.fromCharCode ya da hex-escape ile window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] da işe yarayabilir.

Bozuk shader deseni her zaman yapısal olarak aynı:

layout(<bir şey> = <sayı>)

Yani bunu genel olarak eşleştirip <bir şey>'i doğru identifier ile değiştiriyorum:

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

WebGL hook'u: shaderSource'u patch'le (WebGL1 + WebGL2)

Three.js gl.shaderSource(shader, source) çağırdığı için, shaderSource'un kendisini patch'liyorum:

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

Ve aynı patch'i varsa WebGL2RenderingContext'e de uyguluyorum.

Bu yerine oturduğunda shader derleme hataları kayboldu. Bu noktada croxyproxy çalışıyordu ama proxyorb hala başarısız oluyordu. Niye?! Aynı şekilde çalışmaması mı lazım?


2. Geçiş: İkinci sorun (domain'ler) ve foony.io'yu kaldırmak her şeyi neden kolaylaştırdı

Foony tarihsel olarak iki domain kullanıyordu, en azından geçen ay boyunca:

  • Ana site için foony.com
  • Statik asset'ler için foony.io

Asıl sebep pratikti: cookie'siz bir domain'den asset sunmak, her statik dosya isteğinde cookie header upload şişkinliğinden kaçınmayı sağlıyor. Bu harika, ama HTTP/2'nin header'lar için gönderilen byte'ları azaltmak amacıyla HPACK kullandığı düşünülürse sandığınız kadar gerekli değil.

Normal gezinmede geçerli bir optimizasyon.

Proxy'lerin arkasında ise büyük bir kırılma kaynağı oldu. Ve Foony'nin kullanıcı tabanı proxy'leri seviyor. iç çekiş

Proxy'ler "ana siteyi" "diğer siteler"den farklı ele alır

Birçok proxy "bu tek sayfayı / domain'i proxy'le" için optimize edilmiştir. Ana HTML'i başarıyla yükler, script'ler enjekte eder, kendi ServiceWorker'ını kaydeder vs.

Ama uygulama farklı bir origin'den (foony.io gibi) asset çekmeye başladığında, her türlü eğlenceli kırılganlığa giriyorsunuz:

  • Şu gibi ServiceWorker araya girme hataları:
    • "ServiceWorker intercepted the request and encountered an unexpected error"
    • "Loading failed for the module with source"
  • Proxy altyapısı tarafından gerekli kılınan (ve yanlışlıkla kolayca silinebilen) query param'lar.
  • Asset isteklerinin proxy'nin dahili routing meta verilerini kaybetmesi.
  • Tüm isteği generic-php-slug.php?someQueryParam=hugeEncodedString ile değiştiren tuhaf proxy'ler (evet, onu desteklemekle uğraşmadım).

Bu proxy'lerin dahili mekanizmaları query param'lara / URL encoding'e bağımlı ve oldukça kırılganlar.

Belirgin örneklerden biri şuna benzer bir asset URL'siydi:

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

O ?__pot=..., proxy'nin kendi routing/state'i ve isteğin hangi domain için olduğunu proxy'ye söylüyor. Onu silerseniz, proxy'ler isteği doğru çözümleyemez ve ServiceWorker hata yoluna düşersiniz.

"Resource swapping" kurtarmaya geliyor (ve hızla nasıl karmaşıklaştı)

Bir noktada bir geçici çözüm denedim: "proxy'leniyoruz" durumunu tespit edip her foony.io kaynak URL'sini mevcut origin ile değiştirmek, böylece proxy her şeyi same-origin olarak görsün.

Mantıklı geliyor ve croxyproxy için işe yaradı, ama çok karmaşıklık ekledi:

  • HTML'de zaten var olan link ve script tag'lerini değiştirmeniz gerekiyor.
  • Dinamik olarak enjekte edilen tag'leri (modulepreload, stylesheet vb.) ele almak için bir MutationObserver'a ihtiyacınız var.
  • Proxy'nin query parametrelerini korumak zorundasınız, yoksa routing'lerini bozarsınız. Ve farklı proxy'ler bunu farklı şekilde yapıyor. Tabii ki öyle.
  • Ve hala mantığı genel tutmak zorundasınız (proxy'ye özel global'ler olmadan), yoksa kod şişmiş bir çöp yangınına dönüşür.

"base64 numarası" da burada tekrar gündeme geldi: kendi JavaScript'imde bile location literal string'ine dikkat etmek zorundaydım çünkü proxy onu yeniden yazabilirdi.

CroxyProxy'nin enjekte ettiği script'i tersine mühendisleme

Bu noktada merak ettim: proxy aslında sayfama ne yapıyor? Kendi reklamlarını mı enjekte ediyor? Daha kötü bir şey mi?

CroxyProxy'nin client-side script'i yoğun şekilde obfuscate edilmiş.

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

Çalıştırıldığında şu sonucu veriyor:

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

Buna bakılırsa croxyproxy bu obfuscation için Obfuscator.io kullanıyor gibi görünüyor. Şükür ki webcrack ile deobfuscate etmek yeterince kolay.

Bu çok daha okunabilir bir JavaScript veriyor:

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

Güzel. Şimdi ne yaptığını görebiliyoruz. Ve... çoğunlukla iyi görünüyor. Sanırım obfuscation çoğunlukla proxy'nin tespit edilmesini önlemek içindir. Çoğunlukla.

Biraz reklam / UI enjeksiyonu var:

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

Başka bazı yerler de var, ama temelde sadece reklam gösteriyor, pop-under tarzı reklamlar dahil. Ayrıca FuckAdBlock kullanıyor.

Ama string'lerin asıl değiştirilmesi sunucu tarafında oluyor. Ve onun ne yaptığını kim bilebilir.

Her halükarda, hesap güvenliğinize önem veriyorsanız web proxy'leri kesinlikle kullanmamalısınız. Eğer mecbursanız, kişisel / hesap / satın alma bilgilerinizden hiçbirini girmekten kaçının.

"Resource swapping" çöpe

Resource swapping'in karmaşıklığının, foony.io desteği için kodun diğer kısımlarındaki karmaşıklıkla birleştiğinde, güzel cookie'siz isteklerin küçük ağ tasarrufuna değmediğine karar verdim. Ayrıca foony.io'yu benimsedikten sonra oyun oynama dönüşümlerimizde açıklanamayan bir düşüş görüyorduk, bu yüzden farkında olmadığımız başka foony.io sorunlarının olabileceğinden şüpheleniyorum.

Bu yüzden foony.io'yu kaldırdım. Şimdilik en azından.

foony.io CDN mantığını sildikten ve her şeyi foony.com'da standartlaştırdıktan sonra proxy desteği dramatik şekilde basitleşti:

  • Same-origin asset yüklemeleri.
  • Bir proxy ServiceWorker'a açıklanması gereken daha az "özel durum".
  • Daha az yeniden yazma.
  • Daha az kırılgan kod.

Kısacası, foony.io'yu kaldırmak, tuhaf proxy davranışları için yüzey alanını azaltan mimari bir basitleştirmeydi.


3. Geçiş: Ne çalışıyor, ne çalışmıyor ve neden

Onaylı çalışan proxy'ler

Bu noktada Foony şunların arkasında çalışıyor:

  • croxyproxy
  • proxyorb

Bazı diğer proxy'ler muhtemelen çalışıyor. Bahse girerim çoğu hala çalışmıyordur. Ama en azından insanların oyun oynamak için kullandığı önemli olanlar çalışıyor gibi görünüyor.

Neden "tüm proxy'ler" değil?

Bazı proxy'ler modern bir multiplayer web uygulamasını destekleyemiyor, basitçe. Örnekler:

  • HTTPS'i düzgün desteklemeyen proxy'ler.
  • WebSocket'leri bozan veya engelleyen proxy'ler (Foony gerçek zamanlı network kullanıyor). Teknik olarak bunun etrafından dolaşılabilir, ama karmaşıklık eklerdi.
  • Cross-origin istekler, header'lar veya ServiceWorker'lar etrafında çok fazla kısıtlamaya sahip proxy'ler.

Önemli çıkarımlar

Web proxy'leri çok güvensizdir

Şunları yapan middleware'lerdir:

  • HTML'i yeniden yazar
  • JavaScript'i yeniden yazar
  • bazen bir ServiceWorker enjekte eder
  • ve istekleri yönlendirmek için sıkça query param'lara / URL encoding'e bağımlıdır
  • sayfalarınızla sayısız şekilde oynayabilir

Bazı proxy'lerin ne kadar derine gittiğine şaşırdım: shader kaynak string'lerini, yorumları ve Tanrı bilir başka neleri yeniden yazıyorlar.

Bazen en iyi düzeltme mimari olur

WebGL patch'i oyunların tekrar render olmasını sağladı, ama çoklu domain CDN stratejisini kaldırmak proxy desteğinin kararlı kalmasını sağladı.

Güzel bir hatırlatma: zekice optimizasyonlar, düşmanca middleware ile çarpışana kadar gayet makul olabilir. Ya da kullanıcının tarayıcı eklentileriyle. Ya da Safari ile. Ya da dil ayarlarıyla. Ya da erişilebilirlik özellikleriyle. Ya da güneş patlamalarıyla. Ya da herhangi bir şeyle, gerçekten.


Sonuç

Foony artık önemli olan proxy'lerin (croxyproxy ve proxyorb) arkasında çalışıyor, kod tabanını proxy'ye özel bir karmaşaya çevirmeden:

  • Genel bir Three.js shader düzeltmesi (proxy'ye özel identifier'lar olmadan).
  • Daha basit bir domain stratejisi (her yerde foony.com).
8 Ball Pool online multiplayer billiards icon