background blurbackground mobile blur

1/1/1970

Come ho fatto funzionare Foony dietro i proxy

Ehilà! So da tempo che i proxy web causano problemi di compatibilità con i siti. Tuttavia, il supporto di Foony nei proxy è stato notoriamente pessimo, e risolvere la compatibilità dei proxy con Foony è stato piuttosto complicato.

Non si tratta nemmeno di un problema del tipo "Foony usa API esotiche" (anche se lo facciamo). Era una combinazione di:

  • Proxy che fanno riscritture aggressive di stringhe in punti dove assolutamente non dovrebbero.
  • Proxy che trattano il dominio principale del sito in modo diverso dagli "altri" domini (CDN, host di asset, ecc.).
  • E la dura realtà che alcuni proxy semplicemente non possono supportare le moderne web app (correttezza HTTPS, WebSocket, ecc.).

Non funzioniamo con tutti i proxy, ma ora funzioniamo almeno con croxyproxy e proxyorb, che era l'obiettivo.

Qui sotto spiego cosa si è rotto, perché si è rotto e le soluzioni che hanno davvero contato.


Passaggio 1: shader Three.js validi, ma rotti

Il sintomo

Quando ho provato croxyproxy, non riuscivo a giocare a 8 Ball Pool né a nessun altro gioco three.js di Foony. Continuavo a ricevere errori di compilazione degli shader in Three.js, del tipo:

  • "Shader Error 1282 - VALIDATE_STATUS false"

Quel messaggio era quasi del tutto inutile. Di solito significa "il tuo shader non è valido, buona fortuna". Fantastico. Se ti sei mai chiesto perché su Foony uso sempre messaggi di errore unici per ogni singolo errore, ecco il motivo. Aiuta a individuare i problemi invece di dire semplicemente "il codice è rotto, sistemalo".

Ma perché degli shader three.js perfettamente validi si stavano rompendo? Che storia è?

La causa reale: i proxy che corrompono layout(location = N)

Three.js emette codice GLSL con qualificatori di layout come:

layout(location = 0) in vec3 position;

Alcuni proxy provano a riscrivere qualsiasi cosa assomigli all'API JavaScript location facendo un banale replace globale di stringhe. Già di per sé è una pessima idea in JS, ma lo facevano anche all'interno delle stringhe sorgente degli shader. Suppongo che il parsing dell'AST sia troppo costoso per loro.

Così il sorgente dello shader veniva corrotto in qualcosa del tipo:

layout(__cpLocation = 0) in vec3 position;

Lì l'identificatore deve essere location. Qualsiasi altra cosa è GLSL non valido, e il compilatore la rifiuta. (Layout Qualifiers in GLSL)

Questo è un problema di Three.js solo nel senso che Three.js genera shader dinamicamente, e li passiamo a WebGL a runtime. Il vero bug è la strategia di riscrittura del proxy.

Perché non ho "aggiustato il proxy"

Un approccio ingenuo sarebbe cercare la stringa di sostituzione di location di croxyproxy, __cpLocation, e sostituirla con location. Tuttavia, proxy diversi usano nomi di sostituzione diversi. Alcuni usano __cpLocation, altri usano altri identificatori strani. Quindi una soluzione hardcoded come "rimpiazza __cpLocation con location" è fragile.

Mi serviva:

  • Una soluzione generica (senza identificatori di proxy hardcoded).
  • Una soluzione che funzionasse anche se il proxy stava riscrivendo la parola location anche nel mio JavaScript.

Il trucchetto base64: nascondere la parola location al proxy

Se il proxy riscrive ogni location letterale che vede, la mossa più semplice è semplicemente non usare location. Facile. Ho già visto trucchi del genere in Lua quando ho fatto reverse-engineering del sistema di protezione delle guide di RestedXP (se ricordo bene, offuscano il loro uso di BNGetInfo, tipo _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Ovviamente questo trucco funziona anche in JavaScript. In client/index.html, decodifico questo a runtime:

// Dato che questi proxy provano a sostituire ogni `location`, usiamo una stringa codificata in base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

Quell'atob() avviene dopo che il proxy ha già fatto la sua riscrittura HTML/JS, quindi non può "pre-corrompere" la stringa. Divido la stringa in due per renderla ancora più difficile da rilevare, e uso 'atob' perché posso, ma anche String.fromCharCode o l'escape esadecimale window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] potrebbero funzionare.

Il pattern dello shader rotto è sempre strutturalmente lo stesso:

layout(<qualcosa> = <numero>)

Quindi lo abbino genericamente e sostituisco <qualcosa> con l'identificatore corretto:

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

L'hook WebGL: patchare shaderSource (WebGL1 + WebGL2)

Dato che Three.js chiama gl.shaderSource(shader, source), faccio il patch di shaderSource stesso:

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

E applico la stessa patch a WebGL2RenderingContext se esiste.

Una volta sistemato questo, gli errori di compilazione degli shader sono spariti. A questo punto, croxyproxy funzionava ma proxyorb continuava a fallire. Perché?! Non dovrebbe funzionare allo stesso modo?


Passaggio 2: il secondo problema (i domini) e perché rimuovere foony.io ha reso tutto più semplice

Foony storicamente ha usato due domini, almeno nell'ultimo mese:

  • foony.com per il sito principale
  • foony.io per gli asset statici

Il motivo originale era pratico: servire gli asset da un dominio senza cookie evita il sovraccarico degli header dei cookie su ogni richiesta di file statico. È fantastico, ma non così necessario come si potrebbe pensare, dato che HTTP/2 usa HPACK per ridurre i byte inviati nelle header.

È un'ottimizzazione valida nella navigazione normale.

Dietro i proxy, è diventata una grande fonte di rotture. E la userbase di Foony adora i proxy. sigh

I proxy trattano "il sito principale" diversamente dagli "altri siti"

Molti proxy sono ottimizzati per "fare il proxy di una sola pagina / dominio". Caricheranno con successo l'HTML principale, inietteranno script, registreranno il proprio ServiceWorker, ecc.

Ma quando l'app inizia a tirare asset da un'origine diversa (come foony.io), si entra in tutti i tipi di rotture divertenti:

  • Fallimenti di intercettazione del ServiceWorker, tipo:
    • "ServiceWorker intercepted the request and encountered an unexpected error"
    • "Loading failed for the module with source"
  • Query param richiesti dall'infrastruttura del proxy (e facili da rimuovere accidentalmente).
  • Richieste di asset che perdono i metadati di routing interni del proxy.
  • Proxy strani che sostituiscono l'intera richiesta con generic-php-slug.php?someQueryParam=hugeEncodedString (sì, non mi sono preoccupato di supportarlo).

I meccanismi interni di questi proxy dipendono da query param / encoding URL, e sono piuttosto fragili.

Uno degli esempi rivelatori era un URL di asset come:

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

Quel ?__pot=... è il routing/stato interno del proxy che dice al proxy a quale dominio appartiene la richiesta. Se lo togli, i proxy non riescono a risolvere correttamente la richiesta, e si finisce nel percorso di errore del ServiceWorker.

"Resource swapping" in soccorso (e perché si è complicato in fretta)

A un certo punto, ho provato un workaround: rilevare "siamo sotto proxy", poi scambiare gli URL delle risorse foony.io con l'origine corrente, in modo che il proxy vedesse tutto come stessa origine.

Sembra ragionevole, e ha funzionato per croxyproxy, ma ha aggiunto molta complessità:

  • Bisogna sostituire i tag link e script che esistono già nell'HTML.
  • Serve un MutationObserver per gestire i tag iniettati dinamicamente (modulepreload, stylesheet, ecc.).
  • Bisogna preservare i query param del proxy, altrimenti se ne rompe il routing. E proxy diversi lo fanno in modo diverso. Ovviamente.
  • E bisogna comunque mantenere la logica generica (niente globali specifici del proxy) in modo che il codice non diventi un disastro gonfio.

Anche qui è tornato in scena il "trucchetto base64": persino nel mio JavaScript, dovevo stare attento alla stringa letterale location perché il proxy poteva riscriverla.

Reverse-engineering dello script iniettato da CroxyProxy

A questo punto mi è venuta la curiosità: cosa sta facendo davvero il proxy alla mia pagina? Sta iniettando i propri annunci? Qualcosa di peggio?

Lo script lato client di CroxyProxy è pesantemente offuscato.

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

Che, una volta eseguito, produce:

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

In base a questo, sembra che croxyproxy stia usando Obfuscator.io per questo offuscamento. Che per fortuna è abbastanza facile da deoffuscare con webcrack.

Il risultato è un JavaScript molto più leggibile:

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

Bene. Ora possiamo vedere cosa sta facendo. E... sembra perlopiù a posto. Penso che l'offuscamento serva soprattutto a impedire il rilevamento del proxy. Soprattutto.

Abbiamo qualche iniezione di annunci / 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;
    }

Ci sono altri punti, ma sostanzialmente sta solo mostrando annunci, inclusi quelli pop-under. Usa anche FuckAdBlock.

La vera sostituzione delle stringhe, però, avviene lato server. E chi lo sa cosa sta facendo tutto quello.

In ogni caso, non dovresti assolutamente usare i proxy web se tieni alla sicurezza del tuo account. Se proprio devi, evita di inserire qualsiasi PII / informazione di account / di acquisto.

Il "resource swapping" nel cestino

Ho deciso che la complessità del resource swapping, unita alla complessità in altre parti del codice per il supporto di foony.io, non valeva i piccoli risparmi di rete delle bellissime richieste senza cookie. Stavamo anche notando un calo inspiegato nelle nostre conversioni di gameplay da quando abbiamo adottato foony.io, quindi sospetto che ci fossero altri problemi con foony.io di cui non eravamo a conoscenza.

Quindi ho rimosso foony.io. Almeno per ora.

Una volta cancellata la logica del CDN foony.io e standardizzato tutto su foony.com, il supporto dei proxy è diventato drasticamente più semplice:

  • Caricamenti di asset same-origin.
  • Meno "casi speciali" da spiegare a un ServiceWorker di proxy.
  • Meno riscritture.
  • Codice meno fragile.

In breve, rimuovere foony.io è stata una semplificazione architetturale che ha ridotto la superficie d'attacco per comportamenti strani dei proxy.


Passaggio 3: cosa funziona, cosa no, e perché

Proxy confermati funzionanti

A questo punto, Foony funziona dietro:

  • croxyproxy
  • proxyorb

Probabilmente alcuni altri proxy funzionano. Scommetto che la maggior parte ancora no. Ma almeno quelli importanti che le persone usano per giocare sembrano funzionare.

Perché non "tutti i proxy"?

Alcuni proxy semplicemente non possono supportare una moderna web app multiplayer. Esempi:

  • Proxy che non supportano correttamente HTTPS.
  • Proxy che rompono o bloccano i WebSocket (Foony usa networking real-time). Tecnicamente si potrebbe aggirare il problema, ma aggiungerebbe complessità.
  • Proxy che hanno troppe restrizioni sulle richieste cross-origin, sugli header o sui ServiceWorker.

Punti chiave

I proxy web sono molto insicuri

Sono middleware che:

  • riscrivono l'HTML
  • riscrivono il JavaScript
  • a volte iniettano un ServiceWorker
  • e spesso dipendono da query param / encoding URL per instradare le richieste
  • possono manomettere le tue pagine in molti modi

Sono rimasto sorpreso da quanto in profondità arrivino alcuni proxy: riscrivono stringhe sorgente di shader, commenti, e Dio solo sa cos'altro.

A volte la soluzione migliore è architetturale

La patch WebGL ha fatto tornare a renderizzare i giochi, ma rimuovere la strategia CDN multi-dominio ha fatto sì che il supporto dei proxy rimanesse stabile.

È un buon promemoria: le ottimizzazioni intelligenti possono essere perfettamente ragionevoli finché non si scontrano con middleware ostili. O con le estensioni del browser dell'utente. O con Safari. O con le impostazioni di lingua. O con le funzionalità di accessibilità. O con le eruzioni solari. O con qualsiasi cosa, davvero.


Conclusione

Foony ora funziona dietro i proxy che contano (croxyproxy e proxyorb), senza trasformare il codebase in un caos specifico per i proxy:

  • Una soluzione generica per gli shader Three.js (senza identificatori specifici per i proxy).
  • Una strategia di dominio più semplice (foony.com ovunque).
8 Ball Pool online multiplayer billiards icon