

1/1/1970
Jak sprawiłem, że Foony działa za proxy
Cześć! Od dawna wiedziałem, że proxy webowe powodują problemy z kompatybilnością stron internetowych. Jednak wsparcie Foony w proxy było notorycznie kiepskie, a rozwiązanie kwestii kompatybilności okazało się dość trudne.
To nie jest też przypadek typu „Foony używa egzotycznych API” (chociaż tak jest). Była to kombinacja:
- Proxy agresywnie przepisujące ciągi znaków w miejscach, w których absolutnie nie powinny tego robić.
- Proxy traktujące domenę głównej strony inaczej niż „inne” domeny (CDN, hosty zasobów itp.).
- I brutalnej rzeczywistości, że niektóre proxy po prostu nie potrafią obsłużyć nowoczesnych aplikacji webowych (poprawność HTTPS, WebSockets itp.).
Nie współpracujemy z każdym proxy, ale teraz działamy przynajmniej z croxyproxy i proxyorb, co było celem.
Poniżej wyjaśniam, co się zepsuło, dlaczego się zepsuło i jakie poprawki naprawdę miały znaczenie.
Etap 1: Poprawne, ale zepsute shadery Three.js
Objaw
Kiedy spróbowałem croxyproxy, nie mogłem zagrać w 8 Ball Pool ani w żadne inne gry Foony oparte na three.js. Ciągle dostawałem błąd kompilacji shaderów w Three.js z komunikatami w stylu:
- „Shader Error 1282 - VALIDATE_STATUS false”
Ten komunikat był prawie zupełnie bezużyteczny. Zwykle oznacza „twój shader jest nieprawidłowy, powodzenia”. Świetnie. Jeśli kiedykolwiek zastanawiałeś się, dlaczego zawsze używam unikalnych komunikatów błędów dla każdego pojedynczego błędu w Foony, to właśnie dlatego. Pomaga to zlokalizować problemy zamiast po prostu „kod jest zepsuty, idź to napraw”.
Ale dlaczego idealnie poprawne shadery three.js się psuły? Co jest grane?
Prawdziwa przyczyna: proxy psujące layout(location = N)
Three.js generuje GLSL z kwalifikatorami layout, takimi jak:
layout(location = 0) in vec3 position;
Niektóre proxy próbują przepisać wszystko, co wygląda jak API JavaScript location, robiąc naiwne globalne zastępowanie ciągów znaków. To już jest złe w JS, ale robiły to także wewnątrz źródeł shaderów. Pewnie parsowanie AST jest dla nich zbyt drogie.
Więc źródło shadera zostało uszkodzone do czegoś takiego:
layout(__cpLocation = 0) in vec3 position;
Identyfikator musi być tam location. Wszystko inne jest niepoprawnym GLSL i kompilator to odrzuca. (Kwalifikatory Layout w GLSL)
To problem Three.js tylko w tym sensie, że Three.js generuje shadery dynamicznie i przekazuje je do WebGL w czasie wykonywania. Prawdziwy błąd leży w strategii przepisywania proxy.
Dlaczego nie „naprawiłem proxy”
Naiwnym podejściem byłoby wyszukanie ciągu zastępującego location croxyproxy, czyli __cpLocation, i zamiana go na location. Jednak różne proxy używają różnych nazw zastępczych. Niektóre używają __cpLocation, inne dziwnych identyfikatorów. Więc zakodowanie na sztywno poprawki typu „zamień __cpLocation z powrotem na location” jest kruche.
Potrzebowałem:
- Generycznej poprawki (bez kodowania na sztywno identyfikatorów proxy).
- Poprawki, która działa nawet jeśli proxy przepisuje słowo
locationtakże w moim JavaScripcie.
Sztuczka z base64: ukrywanie słowa location przed proxy
Jeśli proxy przepisuje każdy literał location, jaki widzi, najprostszym ruchem jest po prostu nie używać location. Wystarczająco proste. Widziałem już wcześniej takie sztuczki w Lua, gdy reverse-engineerowałem system ochrony przewodnika RestedXP (jeśli dobrze pamiętam, obfuskują użycie BNGetInfo, np. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Ta sztuczka oczywiście działa też w JavaScripcie. W client/index.html dekoduję następujące w czasie wykonywania:
// Ponieważ te proxy próbują zastąpić każde `location`, używamy ciągu zakodowanego w base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Ten atob() dzieje się po tym, jak proxy już dokonało przepisania HTML/JS, więc nie może „wstępnie uszkodzić” ciągu. Dzielę ciąg na dwa, aby utrudnić jego wykrycie, a używam 'atob', bo mogę, ale String.fromCharCode lub escapowanie hex window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] też mogłoby zadziałać.
Wzorzec uszkodzonego shadera jest zawsze strukturalnie taki sam:
layout(<coś> = <liczba>)
Więc dopasowuję to generycznie i zastępuję <coś> poprawnym identyfikatorem:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
Hak WebGL: patchowanie shaderSource (WebGL1 + WebGL2)
Ponieważ Three.js wywołuje gl.shaderSource(shader, source), patchuję sam shaderSource:
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,
});
I stosuję ten sam patch do WebGL2RenderingContext, jeśli istnieje.
Gdy to było na miejscu, błędy kompilacji shaderów zniknęły. W tym momencie croxyproxy działało, ale proxyorb wciąż się psuło. Dlaczego?! Czy to nie powinno działać tak samo?
Etap 2: Drugi problem (domeny) i dlaczego usunięcie foony.io ułatwiło wszystko
Foony historycznie używało dwóch domen, przynajmniej przez ostatni miesiąc:
foony.comdla głównej stronyfoony.iodla zasobów statycznych
Oryginalny powód był praktyczny: serwowanie zasobów z domeny bez ciasteczek pozwala uniknąć narzutu nagłówków cookie przy każdym żądaniu pliku statycznego. To świetnie, ale nie aż tak konieczne, jak mogłoby się wydawać, biorąc pod uwagę, że HTTP/2 używa HPACK do redukcji bajtów wysyłanych przez sieć dla nagłówków.
To prawidłowa optymalizacja w normalnym przeglądaniu.
Za proxy stało się to głównym źródłem awarii. A baza użytkowników Foony uwielbia proxy. westchnienie
Proxy traktują „główną stronę” inaczej niż „inne strony”
Wiele proxy jest zoptymalizowanych pod kątem „proxuj tę jedną stronę / domenę”. Pomyślnie ładują główny HTML, wstrzykują skrypty, rejestrują własny ServiceWorker itp.
Ale gdy aplikacja zaczyna pobierać zasoby z innego origin (jak foony.io), wpadasz w wszelkiego rodzaju zabawne psucie:
- Awarie przechwytywania ServiceWorker, takie jak:
- „ServiceWorker intercepted the request and encountered an unexpected error”
- „Loading failed for the module with source”
- Parametry zapytań wymagane przez infrastrukturę proxy (i łatwe do przypadkowego usunięcia).
- Żądania zasobów tracące wewnętrzne metadane routingu proxy.
- Dziwne proxy zastępujące całe żądanie czymś w stylu
generic-php-slug.php?someQueryParam=hugeEncodedString(taa, nie zawracałem sobie głowy wspieraniem tego).
Wewnętrzne mechanizmy tych proxy zależą od parametrów zapytań / kodowania URL i są dość kruche.
Jednym z charakterystycznych przykładów był URL zasobu taki jak:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
To ?__pot=... to własny routing/stan proxy, który mówi proxy, dla której domeny jest żądanie. Jeśli to usuniesz, proxy nie może poprawnie rozwiązać żądania i kończysz na ścieżce błędu ServiceWorker.
„Podmiana zasobów” na ratunek (i dlaczego szybko się skomplikowała)
W pewnym momencie spróbowałem obejścia: wykryć „jesteśmy za proxy”, a następnie zamienić wszelkie URL-e zasobów foony.io na bieżący origin, aby proxy widziało wszystko jako same-origin.
Brzmi rozsądnie i zadziałało dla croxyproxy, ale dodało dużo złożoności:
- Trzeba zastąpić tagi
linkiscript, które już istnieją w HTML. - Potrzebny jest
MutationObserverdo obsługi dynamicznie wstrzykiwanych tagów (modulepreload, stylesheet itp.). - Trzeba zachować parametry zapytań proxy, inaczej psujesz ich routing. A różne proxy robią to różnie. Bo oczywiście tak.
- I nadal musisz zachować logikę generyczną (bez globalnych zmiennych specyficznych dla proxy), aby kod nie stał się rozdętym śmietnikiem.
To także miejsce, gdzie ponownie pojawiła się „sztuczka z base64”: nawet w moim własnym JavaScripcie musiałem uważać na literał location, bo proxy mogło go przepisać.
Reverse-engineering wstrzykniętego skryptu CroxyProxy
W tym momencie zrobiłem się ciekawy: co właściwie proxy robi z moją stroną? Czy wstrzykuje własne reklamy? Coś gorszego?
Skrypt klienta CroxyProxy jest mocno zobfuskowany.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Co po uruchomieniu daje:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Na podstawie tego wygląda na to, że croxyproxy używa Obfuscator.io do tej obfuskacji. Co na szczęście jest wystarczająco łatwe do deobfuskacji za pomocą webcrack.
To daje znacznie czytelniejszy 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)) {
Nieźle. Teraz widzimy, co robi. I... wygląda w większości w porządku. Myślę, że obfuskacja służy głównie zapobieganiu wykrywania proxy. Głównie.
Mamy trochę wstrzykiwania reklam / 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;
}
Jest kilka innych miejsc, ale w zasadzie po prostu pokazuje reklamy, w tym reklamy typu pop-under. Używa też FuckAdBlock.
Prawdziwe zastępowanie ciągów dzieje się jednak po stronie serwera. A kto wie, co to wszystko robi.
Tak czy inaczej, absolutnie nie powinieneś używać proxy webowych, jeśli zależy ci na bezpieczeństwie konta. Jeśli musisz, unikaj wprowadzania jakichkolwiek danych osobowych / informacji o koncie / informacji o zakupach.
„Podmiana zasobów” do kosza
Zdecydowałem, że złożoność wynikająca z podmiany zasobów, w połączeniu ze złożonością w innych częściach kodu dla wsparcia foony.io, nie była warta niewielkich oszczędności sieciowych pięknych, bezciasteczkowych żądań. Zauważyliśmy też niewyjaśniony spadek konwersji rozgrywki od momentu adopcji foony.io, więc podejrzewam, że były inne problemy z foony.io, których nie byliśmy świadomi.
Więc usunąłem foony.io. Przynajmniej na razie.
Po usunięciu logiki CDN foony.io i ustandaryzowaniu wszystkiego na foony.com, wsparcie proxy stało się dramatycznie prostsze:
- Ładowanie zasobów same-origin.
- Mniej „przypadków specjalnych” do wytłumaczenia ServiceWorkerowi proxy.
- Mniej przepisywania.
- Mniej kruchego kodu.
Krótko mówiąc, usunięcie foony.io było uproszczeniem architektonicznym, które zmniejszyło powierzchnię ataku dla dziwnych zachowań proxy.
Etap 3: Co działa, co nie działa i dlaczego
Potwierdzone działające proxy
W tym momencie Foony działa za:
- croxyproxy
- proxyorb
Niektóre inne proxy prawdopodobnie też działają. Założę się, że większość nadal nie działa. Ale przynajmniej te ważne, których ludzie używają do grania, wydają się działać.
Dlaczego nie „wszystkie proxy”?
Niektóre proxy po prostu nie potrafią obsłużyć nowoczesnej aplikacji webowej z trybem multiplayer. Przykłady:
- Proxy, które nie obsługują poprawnie HTTPS.
- Proxy, które psują lub blokują WebSockets (Foony używa sieci w czasie rzeczywistym). Technicznie można to obejść, ale dodałoby to złożoności.
- Proxy, które mają zbyt wiele ograniczeń wokół żądań cross-origin, nagłówków lub ServiceWorkerów.
Kluczowe wnioski
Proxy webowe są bardzo niebezpieczne
To middleware, który:
- przepisuje HTML
- przepisuje JavaScript
- czasem wstrzykuje ServiceWorker
- i często zależy od parametrów zapytań / kodowania URL do routingu żądań
- może majstrować przy twoich stronach na wiele sposobów
Byłem zaskoczony, jak głęboko idą niektóre proxy: przepisują źródła shaderów, komentarze i Bóg wie co jeszcze.
Czasem najlepsza poprawka jest architektoniczna
Patch WebGL sprawił, że gry znów się renderują, ale usunięcie wielodomenowej strategii CDN sprawiło, że wsparcie proxy pozostało stabilne.
To dobre przypomnienie: sprytne optymalizacje mogą być całkowicie rozsądne, dopóki nie zderzą się z wrogim middleware. Albo rozszerzeniami przeglądarki użytkownika. Albo Safari. Albo ustawieniami języka. Albo funkcjami dostępności. Albo rozbłyskami słonecznymi. Albo czymkolwiek, naprawdę.
Podsumowanie
Foony teraz działa za proxy, które się liczą (croxyproxy i proxyorb), bez przekształcania bazy kodu w bałagan specyficzny dla proxy:
- Generyczna poprawka shaderów Three.js (bez identyfikatorów specyficznych dla proxy).
- Prostsza strategia domen (foony.com wszędzie).