background blurbackground mobile blur

1/1/1970

Sådan fik jeg Foony til at virke bag proxyer

Hejsa! Jeg har længe vidst, at webproxyer skaber kompatibilitetsproblemer for hjemmesider. Men Foonys understøttelse i proxyer har været notorisk dårlig, og det var ret tricky at løse Foonys proxy-kompatibilitet.

Det er heller ikke et "Foony bruger eksotiske API'er"-problem (selvom det gør vi). Det var en kombination af:

  • Proxyer, der laver aggressiv strengomskrivning steder, hvor de absolut ikke burde.
  • Proxyer, der behandler hoveddomænet anderledes end "andre" domæner (CDN'er, asset-hosts osv.).
  • Og den hårde virkelighed, at nogle proxyer simpelthen ikke kan understøtte moderne webapps (HTTPS-korrekthed, WebSockets osv.).

Vi virker ikke med alle proxyer, men vi virker nu i hvert fald med croxyproxy og proxyorb, hvilket var målet.

Nedenfor forklarer jeg, hvad der gik i stykker, hvorfor det gik i stykker, og hvilke fixes der faktisk gjorde en forskel.


Runde 1: Gyldige, men ødelagte Three.js-shaders

Symptomet

Da jeg prøvede croxyproxy, kunne jeg ikke spille 8 Ball Pool eller nogen af Foonys andre three.js spil. Jeg fik konstant en shader-kompileringsfejl i Three.js med fejl som:

  • "Shader Error 1282 - VALIDATE_STATUS false"

Den besked var næsten fuldstændig ubrugelig. Den betyder typisk "din shader er ugyldig, held og lykke." Skønt. Hvis du nogensinde har undret dig over, hvorfor jeg altid bruger unikke fejlmeddelelser for hver eneste fejl på Foony, så er det her grunden. Det hjælper med at lokalisere problemer i stedet for bare "koden er i stykker, fix det."

Men hvorfor gik fuldstændig gyldige three.js-shaders i stykker? Hvad foregår der?

Den egentlige årsag: proxyer, der korrumperer layout(location = N)

Three.js udsender GLSL med layout-kvalifikatorer som:

layout(location = 0) in vec3 position;

Nogle proxyer prøver at omskrive alt, der ligner JavaScript's location-API, ved at lave naiv global strengudskiftning. Det er allerede skidt i JS, men de gjorde det også inde i shader-kildestrenge. AST-parsing er vel for dyrt for dem.

Så shader-kilden blev korrumperet til noget i retning af:

layout(__cpLocation = 0) in vec3 position;

Identifikatoren skal være location der. Alt andet er ugyldigt GLSL, og compileren afviser det. (Layout Qualifiers in GLSL)

Dette er kun et Three.js-problem i den forstand, at Three.js genererer shaders dynamisk, og vi sender dem til WebGL i runtime. Den egentlige fejl er proxyens omskrivningsstrategi.

Hvorfor jeg ikke "fixede proxyen"

En naiv tilgang ville være at lede efter croxyproxys location-erstatningsstreng, __cpLocation, og erstatte den med location. Men forskellige proxyer bruger forskellige erstatningsnavne. Nogle bruger __cpLocation, andre bruger andre underlige identifikatorer. Så at hardkode et fix som "erstat __cpLocation tilbage til location" er skrøbeligt.

Jeg havde brug for:

  • Et generisk fix (ingen hardkodning af proxy-identifikatorer).
  • Et fix, der virker, selv hvis proxyen også omskriver ordet location i min JavaScript.

Base64-tricket: at skjule ordet location for proxyen

Hvis proxyen omskriver hver eneste bogstavelige location, den ser, er den simpleste løsning bare ikke at bruge location. Nemt nok. Jeg har set tricks som dette før i Lua, da jeg reverse-engineerede RestedXP's guide-beskyttelsessystem (hvis jeg husker rigtigt, obfuskerer de deres brug af BNGetInfo, f.eks. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Det her trick virker selvfølgelig også i JavaScript. I client/index.html afkoder jeg følgende i runtime:

// Fordi disse proxyer prøver at erstatte hver `location`, bruger vi en base64-kodet streng.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

Det atob() sker efter proxyen allerede har lavet sin HTML/JS-omskrivning, så den kan ikke "for-korrumpere" strengen. Jeg deler strengen op i to for at gøre det endnu sværere at opdage, og jeg bruger 'atob', fordi jeg kan, men String.fromCharCode eller hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] kunne også virke.

Det ødelagte shader-mønster er altid strukturelt det samme:

layout(<noget> = <tal>)

Så jeg matcher det generisk og erstatter <noget> med den korrekte identifikator:

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

WebGL-hooket: patch shaderSource (WebGL1 + WebGL2)

Da Three.js kalder gl.shaderSource(shader, source), patcher jeg shaderSource selv:

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

Og jeg anvender det samme patch på WebGL2RenderingContext, hvis det findes.

Da det var på plads, forsvandt shader-kompileringsfejlene. På dette tidspunkt virkede croxyproxy, men proxyorb fejlede stadig. Hvorfor?! Burde det ikke virke på samme måde?


Runde 2: Det andet problem (domæner) og hvorfor fjernelse af foony.io gjorde alt nemmere

Foony har historisk brugt to domæner, i hvert fald den seneste måned:

  • foony.com til hovedsiden
  • foony.io til statiske assets

Den oprindelige grund var praktisk: at servere assets fra et cookie-løst domæne undgår oppustning af cookie-headers ved hver statisk filforespørgsel. Det er fint, men ikke nødvendigt, som man skulle tro, eftersom HTTP/2 bruger HPACK til at reducere bytes sendt over kablet for headers.

Det er en gyldig optimering ved normal browsing.

Bag proxyer blev det en stor kilde til fejl. Og Foonys brugerbase elsker proxyer. suk

Proxyer behandler "hovedsiden" anderledes end "andre sider"

Mange proxyer er optimeret til at "proxy denne ene side / dette ene domæne." De indlæser hoved-HTML'en korrekt, indsætter scripts, registrerer deres egen ServiceWorker osv.

Men når appen begynder at hente assets fra et andet origin (som foony.io), kommer du ind i alle mulige sjove brud:

  • ServiceWorker-interception fejl som:
    • "ServiceWorker intercepted the request and encountered an unexpected error"
    • "Loading failed for the module with source"
  • Query-parametre, der kræves af proxy-infrastrukturen (og som er nemme at strippe ved et uheld).
  • Asset-forespørgsler, der mister proxyens interne routing-metadata.
  • Mærkelige proxyer, der erstatter hele forespørgslen med generic-php-slug.php?someQueryParam=hugeEncodedString (ja, jeg gad ikke understøtte det).

Disse proxyers interne mekanismer afhænger af query-parametre / URL-kodning, og de er ret skrøbelige.

Et af de afslørende eksempler var en asset-URL som:

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

Det ?__pot=... er proxyens egen routing/state, der fortæller proxyen, hvilket domæne forespørgslen er til. Hvis du stripper det, kan proxyer ikke løse forespørgslen korrekt, og du ender i ServiceWorker-fejlstien.

"Resource swapping" til undsætning (og hvorfor det blev kompliceret hurtigt)

På et tidspunkt prøvede jeg en workaround: detektér "vi er proxieret," og udskift derefter alle foony.io-resource-URL'er til det aktuelle origin, så proxyen ville se alt som same-origin.

Det lyder rimeligt, og det virkede for croxyproxy, men det tilføjede en masse kompleksitet:

  • Du skal udskifte link- og script-tags, der allerede findes i HTML'en.
  • Du har brug for en MutationObserver til at håndtere dynamisk indsatte tags (modulepreload, stylesheet osv.).
  • Du skal bevare proxyens query-parametre, ellers ødelægger du deres routing. Og forskellige proxyer gør det forskelligt. For det gør de selvfølgelig.
  • Og du skal stadig holde logikken generisk (ingen proxy-specifikke globals), så koden ikke bliver til en oppustet skraldespandsbrand.

Det er også her, "base64-tricket" dukkede op igen: selv i min egen JavaScript skulle jeg være forsigtig med den bogstavelige streng location, fordi proxyen kunne omskrive den.

Reverse-engineering af CroxyProxys indsatte script

På dette tidspunkt blev jeg nysgerrig: hvad gør proxyen egentlig ved min side? Indsætter den sine egne reklamer? Noget værre?

CroxyProxys klient-side script er stærkt obfuskeret.

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

Som, når det køres, resulterer i:

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

Baseret på dette ser det ud til, at croxyproxy bruger Obfuscator.io til denne obfuskering. Hvilket heldigvis er nemt nok at deobfuskere med webcrack.

Det resulterer i meget mere læsbar JavaScript:

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

Fint. Nu kan vi se, hvad den gør. Og... det ser stort set fint ud. Jeg tror, obfuskeringen primært er for at hjælpe med at forhindre detektion af proxyen. Primært.

Vi har noget reklame- / UI-indsættelse:

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

Der er nogle andre steder, men dybest set viser den bare reklamer, inklusive pop-under-stil reklamer. Den bruger også FuckAdBlock.

Den egentlige udskiftning af strengene sker dog server-side. Og hvem ved hvad alt det gør.

Uanset hvad, bør du absolut ikke bruge webproxyer, hvis du går op i din kontosikkerhed. Hvis du må, så undgå at indtaste nogen af dine PII / konto / købsoplysninger.

"Resource swapping" i papirkurven

Jeg besluttede, at kompleksiteten fra resource swapping, kombineret med kompleksiteten i andre dele af koden til foony.io-understøttelse, ikke var de små netværksbesparelser fra smukke, cookie-løse forespørgsler værd. Vi så også et uforklarligt fald i vores gameplay-konverteringer, siden vi indførte foony.io, så jeg har mistanke om, at der var andre problemer med foony.io, vi ikke var bekendt med.

Så jeg fjernede foony.io. I hvert fald for nu.

Da jeg slettede foony.io CDN-logikken og standardiserede alt på foony.com, blev proxy-understøttelse dramatisk simplere:

  • Same-origin asset-indlæsning.
  • Færre "specielle tilfælde" at forklare en proxy-ServiceWorker.
  • Mindre omskrivning.
  • Mindre skrøbelig kode.

Kort sagt var fjernelsen af foony.io en arkitektonisk forenkling, der reducerede overfladearealet for mærkelig proxy-adfærd.


Runde 3: Hvad der virker, hvad der ikke gør, og hvorfor

Bekræftet fungerende proxyer

På dette tidspunkt virker Foony bag:

  • croxyproxy
  • proxyorb

Nogle andre proxyer virker sandsynligvis også. Jeg vil vædde på, at de fleste stadig ikke gør. Men i hvert fald de vigtige, som folk bruger til at spille spil på, ser ud til at virke.

Hvorfor ikke "alle proxyer"?

Nogle proxyer kan simpelthen ikke understøtte en moderne multiplayer-webapp. Eksempler:

  • Proxyer, der ikke understøtter HTTPS ordentligt.
  • Proxyer, der bryder eller blokerer WebSockets (Foony bruger realtidsnetværk). Teknisk set kunne man arbejde uden om dette, men det ville tilføje kompleksitet.
  • Proxyer, der har for mange begrænsninger omkring cross-origin-forespørgsler, headers eller ServiceWorkers.

Vigtigste pointer

Webproxyer er meget usikre

De er middleware, der:

  • omskriver HTML
  • omskriver JavaScript
  • nogle gange indsætter en ServiceWorker
  • og ofte afhænger af query-parametre / URL-kodning til at route forespørgsler
  • kan pille ved dine sider på utallige måder

Jeg blev overrasket over, hvor dybt nogle proxyer går: de omskriver shader-kildestrenge, kommentarer, og Gud ved hvad ellers.

Nogle gange er det bedste fix arkitektonisk

WebGL-patchet fik spillene til at rendere igen, men fjernelsen af multi-domæne CDN-strategien fik proxy-understøttelsen til at forblive stabil.

Det er en god påmindelse: smarte optimeringer kan være helt fornuftige, indtil de støder sammen med fjendtlig middleware. Eller brugerens browser-udvidelser. Eller Safari. Eller sprogindstillinger. Eller tilgængelighedsfunktioner. Eller solstorme. Eller hvad som helst, virkelig.


Konklusion

Foony virker nu bag de proxyer, der betyder noget (croxyproxy og proxyorb), uden at forvandle kodebasen til et proxy-specifikt rod:

  • Et generisk Three.js shader-fix (ingen proxy-specifikke identifikatorer).
  • En simplere domænestrategi (foony.com overalt).
8 Ball Pool online multiplayer billiards icon