• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!
  • 2026 staff recruitment is open! Check it out and consider applying!

WebSockets proxy

kor

PHP ziom
Premium User
Joined
Jul 12, 2008
Messages
275
Solutions
13
Reaction score
495
Location
Bialystok, Poland
GitHub
rookgaard
YouTube
Rookgaard
Warning: this will be a long post – TL;DR: Below, I describe a method for eliminating dozens of proxies in favor of a single, well-protected WebSocket-based connection.

1765314798626.webp

Some time ago, there was a topic about a browser (ot)client OTClient for Web Browsers (https://otland.net/threads/otclient-for-web-browsers.290165/). It worked by compiling (appropriately modified) OTClient sources as WASM and then running it in a browser. Since it's impossible/insecure (yet) to connect directly from the browser using the TCP protocol, WebSockets were used. After several attempts and with the help of @Gesior.pl , I managed to configure it, which still works today - for example, in the cam viewer for the Tibiantis server – Tibiantis Info - Cams (https://tibiantis.info/cams) – simply click "watch" next to any entry, and a browser window will open and a connection to the server will be established.

To support WebSockets connections with TFS (or any other Tibia engine, even the original binary from the leak), the connection needs to be "translated" back. For this purpose, I used nginx, which receives and maintains a WSS connection, and then forwards (proxies) it to the specified ip/port. Websockify runs there, which (according to the wiki) "is a WebSocket to TCP proxy/bridge" to the login/game server port (how I configured this is described here OTClient for Web Browsers (https://otland.net/threads/otclient-for-web-browsers.290165/post-2761622)).

Okay, we have the server part, we have the client in the browser, but what if we went further and forced the standard Cipsoft client to work in this mode? What's missing is a proxy/bridge that would work the other way around – translating the TCP connection into a WebSocket. Why Cipsoft? Because I really enjoy working with it through the extensions I create (e.g., Tibia - DLL for 7.72 client to mimic 7.4 client (https://otland.net/threads/dll-for-7-72-client-to-mimic-7-4-client.283169/) or Client DLL extension (https://www.youtube.com/playlist?list=PLwGkOisgt0UMyX8E1w3n1Bj5SWqIuScmt)). Many servers still use it as a "return to nostalgia," and even more develop their clients based on the otclient just to make them resemble it as closely as possible.

Inspired by the similarly functioning "bridge" mentioned by Gesior in the context of Giveria - Alpha Proxy Guide (kondrah/otclient multi-path proxy) (https://otland.net/threads/alpha-proxy-guide-kondrah-otclient-multi-path-proxy.291492/post-2772030), and bearing in mind that in today's world of AI agents, this isn't a problem - I described what I needed and obtained 95% of the following code (with some my fixes later) from it - TibiaBridge.cpp (https://gist.github.com/rookgaard/1e7ba703c3a51439fcba022e5d65089d). Of course there's nothing stopping you from adapting this code using OTC from Kondrah or Mehah.

C++:
...
void printUsage() {
    std::printf("Create config.ini next to TibiaBridge.exe with values:\n");
    std::printf("[remote]\n");
    std::printf("host=example.com\n");
    std::printf("port=443\n");
    std::printf("login_path=/example_login\n");
    std::printf("game_path=/example_game\n\n");
    std::printf("[local]\n");
    std::printf("host=127.0.0.1\n");
    std::printf("login_port=7171\n");
    std::printf("game_port=7172\n");
}
...

One might ask - what's the point of all this - there is Alpha-proxy, so the server IP is not visible anyway. I wouldn't have bothered with this myself, either and just stuck with the browser client until @Niebieski pointed out to me that Cloudflare also protects WebSocket connections (!!!), and it showed in my head something like this:

1765314228744.webp

After configuration (steps visualized in the attachments - in left column is TFS console, right column is local bridge, screen with websockify for login server and screen with websockify with game server), the entire process looks like this:
  • instead of connecting to the target server address/domain on default ports 7171 and 7172, the Tibia client connects to localhost (e.g., by hex editing the binary file) on any ports we specify (or default).
  • on localhost (the user's home computer), a program (also in attachment) runs that listens for TCP connections on these two ports and then connects to the WebSocket, forwarding all outgoing and incoming packets.
  • CloudFlare monitors and protects the connection, while the WebSocket itself (or rather, the domain) resolves to the address of the server where the program (e.g., nginx) is listening for the WSS connection. Because traffic goes through CF, the location of this server cannot be seen by the player/attacker (the attachment shows what local connections look like).
  • another program (e.g., websockify) runs on the server, which, after receiving a connection from nginx, accepts the connection and "translates" them to TCP

I tested it on a sample server, and it looks promising. It's hard to say how much latency is imposed by having traffic flow through CF, but it's certainly similar to servers using proxy servers. A major advantage is that it eliminates the need for multiple proxy servers, and it's free.

This is only a Proof of Concept – the next steps maybe will be integrating the code directly into the DLL to reduce the number of steps for the end user, who typically expects a single click to log in and play.
 

Attachments

Great take. I always preferred a single, well-protected endpoint instead of a network of weak ones (btw Alpha's proxy is usually your first point of failure if you don't have shitton of RAM, or know how to fix it), and now I have an idea how I can add an additional layer of protection. Thanks!

Respect for that sick DLL injects, and always finding a way around Cip's limitations.
 
Update on OTCv8 proxy and WebSockets (+CloudFlare.com/gcore.com free WebSocket protection at end of post)

I added WebSockets support in OTCv8 proxy last week - code with multiple OTCv8 proxies, not one - with "Claude 4.5 Opus" AI in less than one hour using free Antigravity IDE ( Google Antigravity (https://antigravity.google/) ) and just 3 crashes/problems with connection (3 prompts to AI to 'fix it').

I did not make PR yet, as I did not test it on any 100+ OTS - 100+ OTS would report much more crashes than my home PC tests.
If anyone is interested in current state of OTCv8 WebSockets code, I made this PR draft:

Example init.lua config (ws = port 80, wss = port 443; as CloudFlare requires, you cannot change ports):
LUA:
g_proxy.addProxy('ws://websocket-cf.evotra.online', 0, 0) -- CF protected HTTP
g_proxy.addProxy('wss://websocket-direct.evotra.online', 0, 0) -- direct HTTPS to VPS with wrong SSL cert (other domain), works
g_proxy.addProxy('ws://websocket-direct.evotra.online', 0, 0) -- direct HTTP to VPS
-- normal haproxy socket with IP/domain and port, not websocket:
g_proxy.addProxy('arm.skalski.pro', 6501, 0) -- TCP that goes thru haproxy 'send-proxy-v2' protocol (shows real client IP to OTS)
More info/changes how to not 'clear' proxies on 'login to account' in PR.

WSS to CF does not work g_proxy.addProxy('wss://websocket-cf.evotra.online', 0, 0). IDK why, still working on it.

another program (e.g., websockify) runs on the server, which, after receiving a connection from nginx, accepts the connection and "translates" them to TCP
I also did not develop server side code, to translate WebSockets - including CF 'cf-connecting-ip' HTTP header - to 'TCP proxy v2 protocol' (like haproxy does for OTCv8 proxy), that would pass real player IP to OTS, not 127.0.0.1... yet. It should be like 30 minutes with AI, but I got 5 days cooldown on free 'Claude 4.5 Opus' in Antigravity :P

I know that Malvera.online owner (Niebieski) already made it with AI. It works like 'websockify', but accepts only HTTP (no SSL) websocket connections (CF/nginx offloads SSL), reads IP/HTTP headers and translates them to TCP connection with 'TCP proxy v2 protocol' (sends player IP in first packet after TCP connects to OTS).

haproxy part of my TFS 1.6+ "OTCv8 proxy" PR - normally used for 'OTS status for otservlist' - would read it from player connection haproxy (v2 proxy protocol):
You can also test it on Tibia 10.98 with my 'updated' TFS 1.4 - to make it compile on Windows + new features - branch:
just make sure to set config.lua config allowHaProxy to true:

@kor
Of course your code is way different, as it - probably - works with RL Tibia client and OTS with no changes in C++.

I attached to this post 'OTCv8 proxy standalone client' (made by OTCv8 author to make OTCv8 proxy work with RL Tibia clients), which starts OTCv8 proxy for RL Tibia client (7.x - 13.40, on 14+ Tibia protocol 'first network packet' is different and does not work with OTCv8 proxy server anymore).
IDK how to compile it into .exe, but if you do, it's 'launcher' that listens on configured ports (ex. 7171/7172), proxies connections/packets thru configured OTCv8 proxy IPs and starts configured .exe (ex. RL Tibia.exe).
Maybe it can be combined with your code to make RL Tibia client work with OTCv8 proxies with WebSockets.
the next steps maybe will be integrating the code directly into the DLL to reduce the number of steps for the end user, who typically expects a single click to log in and play.
I noticed that problem with 7.x-10.x clients with 'OTCv8 proxy launcher'.
'OTCv8 proxy launcher' must run in same folder as Tibia.dat and Tibia.spr (Tibia.exe cannot be in subfolder ex. client), but maybe it can be fixed to make it launch Tibia client from subdirectory like bin (ex. Tibia 11+ runs from 'bin' subdirectory and OTCv8 proxy can launch it too) or rename Tibia.exe to client (not .exe = not clickable in Windows to run [without proxy]) and still launch it as app in 'proxy launcher', so there would be just one .exe in OTS client directory.

As I have no idea how to compile 'OTCv8 proxy launcher' into .exe, I did not work on it.
I tested it on a sample server, and it looks promising. It's hard to say how much latency is imposed by having traffic flow through CF
In my tests it's from -2 ms to +30 ms - just one player online, on not popular domain protected by CF, with direct ping to server ~30 ms -,
but on Malvera.online - hosted in USA/Canada, to which I connect from Poland - it looks like CF WebSocket ping with OTCv8 proxy is always lower than direct connection to OVH server (1-5 ms lower).

// little offtop: After 3 days with 200+ online on gcore.com "free" WebSocket protection, they blocked all connections and asked to change to "paid plan", yet no problems with CloudFlare

// offtop/edit 2: It's well known that CloudFlare is free for X-XXk domain (website) users for X years and then come to domain owner and asks for 10k$/month (or CF will shut down your domain).
If you plan to use WebSockets for OTS proxy, I recommend to get new domain (some random cheap domain ex. xxx.ovh at ovh.com for 1-2 EUR/year) and create new CloudFlare account, in case they decide to charge your WebSockets for traffic, your domain (OTS website) won't go offline
 

Attachments

Last edited:
IDK why, still working on it.
I had similar issue, but the solution (ofc from AI) was to change "encryption mode" to "Full (strict)" in CF settings, maybe it will help you yoo. And yes, lack of player IP is a problem in that solution, some day I will ask some "agent" to fix it for me :D
 
I had similar issue, but the solution (ofc from AI) was to change "encryption mode" to "Full (strict)" in CF settings, maybe it will help you yoo. And yes, lack of player IP is a problem in that solution, some day I will ask some "agent" to fix it for me :D
Hmm... I'm testing it on evotra.online - not my website/CF account - so it may be related.
I must buy 2 more domains and test them with 'Full' (not strict) and 'Flexible' modes of SSL.
Post automatically merged:

I had similar issue, but the solution (ofc from AI) was to change "encryption mode" to "Full (strict)" in CF settings, maybe it will help you yoo. And yes, lack of player IP is a problem in that solution, some day I will ask some "agent" to fix it for me :D
Hmm... I'm testing it on evotra.online - not my website/CF account - so it may be related.
I'm pretty sure it's set to 'Flexible' (HTTPS between user <-> CF, HTTP between CF <-> OTS www, but it may work different for WebSockets than for websites [translate HTTP/HTTPS for websites to HTTP]).
I must buy 2 more domains and test them with 'Full' (not strict) and 'Flexible' modes of SSL. Maybe next week.
 
Last edited:
Has anyone already got a WebSocket system working 100%? I’m willing to pay $300 for it. Will it be able to prevent the attacks from Raw.exe?
 
Perfect! @Niebieski, could you please accept me? How much does the system cost?

@Gesior.pl I thought he had already validated the protection. Do you think it will protect 100%?

Bruce Willis GIF
 
I've been playing around with this, the websockify version worked almost instantly, just had to change to
Code:
wss://domain.com:8443/?token=login
instead of
Code:
wss://domain.com:8443/login
At least the the version of websockify version I was using didnt want to work without the url token parameter.

Keep in mind that if you intend to use cloudfare for websock protection, the websock server needs to use one of the following ports: 443, 2053, 2083, 2087, 2096, 8443 (it's either hardwired in cloudfare or it wont let me adjust it in free plan).

Also if you want to test without cloudfare protected domain, you need to set those additional flags on top of WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET
C++:
DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
    SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE |
    SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
    SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;

ok = WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));

if (!ok) {
    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    return nullptr;
}
However, I dont think websockify will handle a more active server well - encrypted traffic, threading, socket overhead etc. Doesnt matter if its the original python, C or Golang version (but the Goroutines version would be my best bet)

I've tried to use /etc/nginx/nginx.conf stream{} (libnginx-mod-stream) but couldnt strip the "residual" HTTP headers. Again, nginx is not really designed to manipulate "raw" packets.

Then I went for HAProxy, but couldn't make the HTTP headers strip to work properly. I've tried many different approaches, including LUA scripts with core.register_action.

The best I've got so far is HAProxy dealing with encryption/handshakes, and websockify (in non-ssl mode, so much faster) dealing with winsock->raw translation.

If someone cares to try, this is the haproxy.cfg
Code:
global
    maxconn 1000
    log /dev/log local0
    user haproxy
    group haproxy
    daemon

defaults
    log global
    mode http
    timeout connect 5s
    timeout client 1h
    timeout server 1h

frontend wss_tunnel
    bind *:8443 ssl crt /etc/letsencrypt/live/domain.com/combined.pem
    mode http

    acl is_login url_param(token) -i login
    acl is_game  url_param(token) -i game

    use_backend websockify_login if is_login
    use_backend websockify_game  if is_game
    default_backend deny_all

backend websockify_login
    server local_ws_logic 127.0.0.1:9001

backend websockify_game
    server local_ws_game 127.0.0.1:9002

backend deny_all
    http-request deny deny_status 403
If using certbot installed Let's Encrypt CA, certs needs to be combined like so
Code:
sudo cat /etc/letsencrypt/live/domain.com/fullchain.pem /etc/letsencrypt/live/domain.com/privkey.pem | sudo tee /etc/letsencrypt/live/domain.com/combined.pem > /dev/null

When this is done, run the websockify "translators"
Code:
websockify 9001 127.0.0.1:7171 &
websockify 9002 127.0.0.1:7172 &
And ofc your gameserver needs to bind to 127.0.0.1 instead of global address

I will try to make it work without websockify, but if someone already successfully configured it with nginx/haproxy or any other "industrial grade" tool I would love to see it
 
I will try to make it work without websockify
There must be an app to translate WebSockets to Sockets. nginx/haproxy won't do it.
If you find some nginx/haproxy module that can do it, please share it, it would be much easier to use than custom app.

Problem with websockify is that, it does not pass real player IP to OTS, so all players are connected from 127.0.0.1.

TFS 1.6+ changes to read IP from OTCv8 proxy (custom protocol) and "haproxy" (official haproxy proxy-v2 protocol [ https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt ]):

I uploaded "reverse-proxy-websockets in Go" I made with Claude Opus 4.5 in attachment. I didn't have time to test if it works at all, but you can use it as start point to save ~10 minutes of AI thinking. AI 'implementation plan' and my prompts are in AI directory.

EDIT:
I updated 'otcv8_proxy_standalone_client'. Now it compiles in Visual Studio and CLion using vcpkg. It also shows icon on task bar with number of proxies active and ping.
I'm working on documentation (how to use/configure, 'compilation' documentation is in .zip) and .png launcher logo with transparency instead of .bmp.
When I finish, I will put it on my github. Now I put it in this post attachment.
 

Attachments

Last edited:
Back
Top