background blurbackground mobile blur

1/1/1970

Hoe ik Foony werkend kreeg achter proxies

Hoi! Ik weet al heel lang dat webproxy's voor compatibiliteitsproblemen met websites zorgen. Maar de ondersteuning van Foony via proxies was berucht slecht, en het oplossen van Foony's proxy-compatibiliteit was best lastig.

Dit is ook geen soort “Foony gebruikt rare API's”-probleem (ook al doen we dat). Het was een combinatie van:

  • Proxies die agressief tekst herschrijven op plekken waar dat absoluut niet zou moeten.
  • Proxies die het hoofddomein anders behandelen dan “andere” domeinen (CDN's, asset-hosts, enzovoort).
  • En de harde realiteit dat sommige proxies moderne webapps gewoon niet aankunnen (HTTPS-correctheid, WebSockets, enz.).

We werken niet met elke proxy, maar we werken nu in ieder geval met croxyproxy en proxyorb, en dat was het doel.

Hieronder leg ik uit wat er stuk ging, waarom het stuk ging, en welke fixes uiteindelijk echt uitmaakten.


Ronde 1: Geldige, maar kapotte Three.js-shaders

Het symptoom

Toen ik croxyproxy probeerde, kon ik 8 Ball Pool of Foony's andere three.js games niet spelen. Ik kreeg steeds een shader-compilatiefout in Three.js met fouten zoals:

  • “Shader Error 1282 - VALIDATE_STATUS false”

Die melding was bijna volledig nutteloos. Het betekent meestal zoiets als: “je shader is ongeldig, succes.” Top. Als je je ooit afvraagt waarom ik voor elke fout in Foony een unieke foutmelding gebruik, dit is waarom. Het helpt problemen aanwijzen in plaats van alleen “code is stuk, fix het maar.”

Maar waarom gingen perfect geldige three.js-shaders stuk? Wat was hier aan de hand?

De echte oorzaak: proxies die layout(location = N) corrumperen

Three.js genereert GLSL met layout-qualifiers zoals:

layout(location = 0) in vec3 position;

Sommige proxies proberen alles te herschrijven wat lijkt op de JavaScript-API location, door domweg overal in de string te zoeken en te vervangen. Dat is al slecht in JS, maar ze deden het óók in shader-broncode. Blijkbaar is een AST parsen te duur voor ze.

Daardoor werd de shader-broncode gecorrumpeerd tot iets als:

layout(__cpLocation = 0) in vec3 position;

De identifier moet daar location zijn. Alles daarbuiten is ongeldige GLSL, en de compiler weigert het. (Layout Qualifiers in GLSL)

Dit is eigenlijk alleen een Three.js-probleem in de zin dat Three.js shaders dynamisch genereert en wij ze op runtime aan WebGL doorgeven. De echte bug zit in de herschrijf-strategie van de proxy.

Waarom ik de proxy niet “gerepareerd” heb

Een naïeve aanpak zou zijn om te zoeken naar de vervangstring van croxyproxy voor location, __cpLocation, en die weer terug te zetten naar location. Maar verschillende proxies gebruiken verschillende vervangende namen. De een gebruikt __cpLocation, de ander weer iets heel anders. Dus een hardcoded fix als “vervang __cpLocation terug naar location” is erg fragiel.

Ik had nodig:

  • Een generieke fix (geen hardcoded proxy-identifiers).
  • Een fix die ook werkt als de proxy het woord location in mijn JavaScript herschrijft.

De base64-truc: het woord location verstoppen voor de proxy

Als de proxy elke letterlijke location die hij ziet herschrijft, is de simpelste zet om gewoon geen location meer te gebruiken. Klinkt makkelijk. Ik had dit trucje eerder gezien in Lua toen ik RestedXP's bescherming van hun guides reverse-engineerde (als ik het goed herinner obfusceren ze hun gebruik van BNGetInfo, bijvoorbeeld _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Deze truc werkt natuurlijk ook in JavaScript. In client/index.html decodeer ik het volgende op runtime:

// 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

Die atob() wordt uitgevoerd nadat de proxy al zijn HTML/JS-herschrijvingen heeft gedaan, dus hij kan de string niet “vooraf corrumperen”. Ik splits de string in tweeën om het nog moeilijker te maken om te detecteren, en ik gebruik 'atob' omdat het kan, maar String.fromCharCode of een hex-escape van window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] zou ook kunnen werken.

Het kapotte shader-patroon is structureel altijd hetzelfde:

layout(<something> = <number>)

Dus ik match dat generiek en vervang <something> door de juiste identifier:

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

De WebGL-hook: shaderSource patchen (WebGL1 + WebGL2)

Omdat Three.js gl.shaderSource(shader, source) aanroept, patch ik shaderSource zelf:

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

En ik pas dezelfde patch toe op WebGL2RenderingContext als die bestaat.

Toen dat eenmaal stond, verdwenen de shader-compilatiefouten. Op dat punt werkte croxyproxy, maar proxyorb faalde nog steeds. Waarom?! Zou dat niet op dezelfde manier moeten werken?


Ronde 2: Het tweede probleem (domeinen) en waarom foony.io verwijderen alles makkelijker maakte

Foony gebruikte historisch gezien twee domeinen, in elk geval de afgelopen maand:

  • foony.com voor de hoofdpagina
  • foony.io voor statische assets

De oorspronkelijke reden was praktisch: assets vanaf een cookieloos domein serveren voorkomt dat bij elk statisch bestand onnodig cookie-headers meegestuurd worden. Dit is fijn, maar niet zo noodzakelijk als je zou denken, aangezien HTTP/2 HPACK gebruikt om het aantal verzonden header-bytes te beperken.

Het is een geldige optimalisatie bij normaal browsen.

Achter proxies werd het een grote bron van problemen. En Foony's gebruikers zijn dol op proxies. zucht

Proxies behandelen “de hoofdpagina” anders dan “andere sites”

Veel proxies zijn geoptimaliseerd voor “proxy deze ene pagina / dit ene domein”. Ze laden de hoofd-HTML prima, injecteren scripts, registreren hun eigen ServiceWorker, enzovoort.

Maar zodra de app assets gaat ophalen van een ander origin (zoals foony.io), krijg je allerlei leuke breek-dingen:

  • ServiceWorker-interceptiefouten zoals:
    • “ServiceWorker intercepted the request and encountered an unexpected error”
    • “Loading failed for the module with source”
  • Queryparameters die verplicht zijn voor de proxy-infrastructuur (en die je makkelijk per ongeluk weghaalt).
  • Asset-requests die de interne routeringsmetadata van de proxy kwijtraken.
  • Vreemde proxies die het hele request vervangen door iets als generic-php-slug.php?someQueryParam=hugeEncodedString (ja, daar ben ik niet eens aan begonnen).

De interne mechanismen van deze proxies hangen af van queryparameters / URL-encoding, en die zijn behoorlijk fragiel.

Een van de duidelijke voorbeelden was een asset-URL als:

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

Die ?__pot=... is de eigen routing/state van de proxy, die aangeeft voor welk domein het request bedoeld is. Als je die weghaalt, kunnen proxies het verzoek niet meer goed oplossen en beland je in het ServiceWorker-foutpad.

“Resource swapping” to the rescue (en waarom het snel ingewikkeld werd)

Op een gegeven moment probeerde ik een workaround: detecteren “we zitten achter een proxy”, en dan alle foony.io-resource-URL's omwisselen naar de huidige origin, zodat de proxy alles als same-origin ziet.

Dat klinkt redelijk, en het werkte voor croxyproxy, maar het voegde een hoop complexiteit toe:

  • Je moet bestaande link- en script-tags in de HTML vervangen.
  • Je hebt een MutationObserver nodig om dynamisch geïnjecteerde tags af te handelen (modulepreload, stylesheets, enz.).
  • Je moet de queryparameters van de proxy behouden, anders breek je hun routing. En elke proxy doet dat weer anders. Natuurlijk.
  • En je wilt de logica generiek houden (geen proxy-specifieke globals), zodat de code geen opgeblazen rommel wordt.

Hier kwam de “base64-truc” ook weer terug: zelfs in mijn eigen JavaScript moest ik oppassen met de letterlijke string location, omdat de proxy die misschien herschrijft.

De geïnjecteerde script van CroxyProxy reverse-engineeren

Op dit punt werd ik nieuwsgierig: wat doet die proxy eigenlijk met mijn pagina? Injecteren ze hun eigen advertenties? Iets ergers?

Het client-side script van CroxyProxy is zwaar geobfusceerd.

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

Als je dat runt, krijg je zoiets als:

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

Op basis hiervan lijkt het erop dat croxyproxy Obfuscator.io gebruikt voor deze obfuscatie. Gelukkig is dat vrij makkelijk te deobfusceren met webcrack.

Dat levert veel leesbaardere JavaScript op:

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

Mooi. Nu kunnen we zien wat het doet. En... het lijkt grotendeels prima. Ik denk dat de obfuscatie vooral dient om detectie van de proxy te bemoeilijken. Vooral.

We zien wat advertentie- / UI-injectie:

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

Er zijn nog wat andere plekken, maar in de kern tonen ze gewoon advertenties, waaronder pop-under-achtige advertenties. Ze gebruiken ook FuckAdBlock.

De echte vervanging van strings gebeurt echter server-side. En wie weet wat daar allemaal gebeurt.

Hoe dan ook, je zou absoluut geen webproxy's moeten gebruiken als je om de veiligheid van je account geeft. Als het echt moet, voer dan geen van je PII- / account- / aankoopgegevens in.

“Resource swapping” de prullenbak in

Ik besloot dat de complexiteit van resource swapping, gecombineerd met de complexiteit in andere delen van de code voor foony.io-ondersteuning, het kleine netwerkvoordeel van mooie, cookieloze requests niet waard was. We zagen ook een onverklaarbare daling in onze gameplay-conversies sinds we foony.io gebruikten, dus ik vermoed dat er nog andere problemen met foony.io waren waar we niets van wisten.

Dus ik heb foony.io verwijderd. In elk geval voorlopig.

Toen ik de foony.io-CDN-logica verwijderde en alles standaardiseerde op foony.com, werd proxy-ondersteuning ineens een stuk eenvoudiger:

  • Same-origin asset-loads.
  • Minder “speciale gevallen” om uit te leggen aan een proxy-ServiceWorker.
  • Minder herschrijven.
  • Minder fragiele code.

Kortom, foony.io verwijderen was een architecturale vereenvoudiging die de kans op raar proxy-gedrag flink verlaagde.


Ronde 3: Wat wel werkt, wat niet, en waarom

Bevestigd werkende proxies

Op dit punt werkt Foony achter:

  • croxyproxy
  • proxyorb

Andere proxies werken waarschijnlijk ook. Ik gok dat de meeste nog steeds niet werken. Maar in elk geval lijken de belangrijke proxies die mensen gebruiken om games te spelen wel te werken.

Waarom niet “alle proxies”?

Sommige proxies kunnen simpelweg geen moderne multiplayer-webapp aan. Voorbeelden:

  • Proxies die HTTPS niet goed ondersteunen.
  • Proxies die WebSockets breken of blokkeren (Foony gebruikt real-time networking). Technisch gezien kun je daar omheen werken, maar dat voegt weer meer complexiteit toe.
  • Proxies met te veel beperkingen rondom cross-origin-requests, headers of ServiceWorkers.

Belangrijkste punten

Webproxy's zijn erg onveilig

Het zijn stukjes middleware die:

  • HTML herschrijven
  • JavaScript herschrijven
  • soms een ServiceWorker injecteren
  • vaak afhankelijk zijn van queryparameters / URL-encoding om requests te routeren
  • op allerlei manieren met je pagina's kunnen rommelen

Ik was verbaasd hoe diep sommige proxies gaan: ze herschrijven shader-bronstrings, comments, en God weet wat nog meer.

Soms is de beste fix een architecturale

De WebGL-patch zorgde ervoor dat de games weer renderden, maar het verwijderen van de multi-domein-CDN-strategie zorgde ervoor dat proxy-ondersteuning stabiel bleef.

Het is een goede reminder: slimme optimalisaties kunnen prima zijn, tot ze botsen met vijandige middleware. Of browser-extensies van gebruikers. Of Safari. Of taalinstellingen. Of toegankelijkheidsfeatures. Of zonneflammen. Of eigenlijk wat dan ook.


Conclusie

Foony werkt nu achter de proxies die ertoe doen (croxyproxy en proxyorb), zonder dat de codebase verandert in een proxy-specifieke puinhoop:

  • Een generieke Three.js-shaderfix (geen proxy-specifieke identifiers).
  • Een eenvoudiger domeinstrategie (overal foony.com).
8 Ball Pool online multiplayer billiards icon