This is a technical post about preserving real client IPs when running MUD servers behind reverse proxies in Docker containers.
If you run MUD servers inside Docker containers behind a reverse proxy like Traefik, HAProxy or nginx, you’ve probably noticed that every player connection appears to come from the same internal IP address — the proxy’s container IP. This breaks IP-based features like login logs, anti-multiplay checks, IP banning, and anything else that relies on query_ip_number() or its equivalent.
This post explains the problem, why it happens, and how we solved it by adding PROXY protocol v1 support to the MUD drivers we use at Maldorne.
The problem
In a traditional MUD setup, the driver listens directly on a public port. When a player connects, the OS tells the driver their real IP address via accept() + getpeername().
In a containerized setup with a reverse proxy, the traffic flow is:
Player (198.51.100.42) → Traefik (public port) → MUD container (internal port) |
Traefik terminates the TCP connection from the player and opens a new one to the MUD. The MUD’s accept() sees only Traefik’s internal Docker IP (e.g. 172.18.0.2), not the player’s. The same happens with WebSocket-to-telnet proxies used by browser-based MUD clients.
The result: every player appears to connect from the same IP, making login logs useless and IP-based security impossible.
The solution: PROXY protocol
PROXY protocol is a simple, widely adopted standard created by the HAProxy team. When enabled, the reverse proxy prepends a single ASCII line to the TCP stream before any application data:
PROXY TCP4 198.51.100.42 203.0.113.10 49152 5000\r\n |
The backend reads this line, extracts the real client IP, and processes the rest of the stream normally. The format is human-readable (v1) and trivial to parse. A good overview: Exploring the PROXY Protocol.
Most modern reverse proxies can send PROXY protocol headers:
- Traefik: via TCP serversTransport with
proxyProtocol.version: 1 - HAProxy: via
send-proxyorsend-proxy-v2on theserverline - nginx: via
proxy_protocol onin astreamblock
The missing piece is support on the receiving side — the MUD driver.
Configuring Traefik
Since Traefik v3.5.2, the proxyProtocol option moved from the load balancer to TCP serversTransport. Define a transport in a dynamic configuration file:
# traefik/dynamic.yml |
Mount the file and enable the file provider in your compose:
services: |
Then reference the transport from each MUD service via Docker labels:
labels: |
MudOS
We added PROXY protocol v1 support to our MudOS fork (branch v22.2-maldorne). The patch is in driver/comm.c, inside the new_user_handler() function, right after the accept() call stores the peer address.
The logic:
MSG_PEEKthe first bytes of the new connection.- If they start with
"PROXY ", read the full header line (up to\r\n). - Parse the source IP and port from the header.
- Overwrite the stored client address in the
interactive_tstruct. - If the connection does not start with
"PROXY ", do nothing — backwards compatible with direct connections.
The feature is controlled by #define SUPPORT_PROXY_PROTOCOL in driver/local_options, following MudOS conventions for compile-time options.
The patch
In driver/comm.c, after the line that copies the peer address into the user struct:
memcpy((char *) &all_users[i]->addr, (char *) &addr, length); |
Add:
|
And in driver/local_options:
That’s it. query_ip_number() and query_ip_name() will now return the real client IP for proxied connections, and work exactly as before for direct connections.
LDMud
We added PROXY protocol v1 support to our LDMud fork (branch 3.6.8-maldorne) and submitted a pull request to the official LDMud repository.
The patch is in src/comm.c, in the get_message() function where new connections are accepted. The logic is the same as the MudOS patch: after accept() returns a new socket and before new_player() is called, peek at the first bytes and parse the PROXY header if present.
The feature is controlled by #define SUPPORT_PROXY_PROTOCOL in config.h, enabled at configure time with:
./configure --enable-use-proxy-protocol |
The LDMud patch differs from the MudOS one in two ways:
-
IPv6 support: LDMud can be compiled with
USE_IPV6. The patch handles both native IPv6 addresses and IPv4-to-IPv6-mapped addresses (when Traefik sendsPROXY TCP4but the driver uses IPv6 sockets internally). -
Integration with configure: instead of a manual
#definein a local options file, the feature follows LDMud’s standardconfigure.acpattern with--enable-use-proxy-protocol(disabled by default) and appears in--optionsoutput as “PROXY protocol v1 supported”.
The patch
In src/comm.c, inside get_message(), find the accept() call for new connections:
new_socket = accept(sos[i], (struct sockaddr *)&addr, &length); |
Replace with:
new_socket = accept(sos[i], (struct sockaddr *)&addr, &length); |
In src/config.h.in, add near the IPv6 define:
/* Define this if you want PROXY protocol v1 support. When enabled, the |
In src/main.c, add the --options display line after IPv6:
|
And in src/autoconf/configure.ac, add the option and its processing (see the pull request for the exact configure.ac changes).
All IP-related efuns (query_ip_number(), query_ip_name(), interactive_info(II_IP_ADDRESS), interactive_info(II_IP_NAME)) transparently return the real client IP without any mudlib changes.
DGD
Work in progress. We are currently working on adding PROXY protocol support to DGD.
FluffOS
FluffOS is the main actively maintained fork of MudOS. It does not support PROXY protocol as of 2026. The same problem was raised in 2019 and the maintainer’s approach was to add a native WebSocket server to the driver instead, avoiding the external proxy entirely.
Work in progress. We are currently working on adding PROXY protocol support to FluffOS, following the same approach as our MudOS patch.
What about WebSocket proxies?
For browser-based MUD clients that connect through a WebSocket-to-telnet proxy (like our mud-web-proxy), the proxy knows the real client IP from the HTTP headers. It can inject a PROXY protocol v1 header when opening the telnet connection to the MUD, so the driver sees the real IP regardless of the connection path.
This means both connection paths are covered:
| Path | How the real IP is preserved |
|---|---|
| Telnet client → Traefik → MUD | Traefik sends PROXY protocol header |
| Web client → Traefik → WebSocket proxy → MUD | WebSocket proxy sends PROXY protocol header |
Work in progress. We are currently working on adding PROXY protocol header injection to our mud-web-proxy.
References
- PROXY protocol v1/v2 specification (HAProxy)
- Exploring the PROXY Protocol (blog post)
- Traefik TCP serversTransport documentation
- Traefik v3 migration: deprecation of proxyProtocol option
- Our MudOS fork with PROXY protocol support
- Our LDMud fork with PROXY protocol support
- Pull request to official LDMud repository
- HAProxy blog: Preserve Source IP Address Despite Reverse Proxies