Unlocking the Past
If you walked into my high school's computer lab after hours, you wouldn't hear the clicking of people typing essays. You'd hear the frantic, rhythmic slamming of mice and keyboards, punctuated by the faint, muffled sound of the announcer screaming, "M-M-M-MONSTER KILL!" through someone's cheap headphones.
Unreal Tournament 2004 wasn't just a game for me—it was the game. It was the defining multiplayer experience of my high school years. Between dodging Shock Rifle combos, mastering the chaos of Onslaught mode, and the sheer adrenaline of Instagib matches, UT2004 cemented itself as one of my all-time favorite games. It was fast, it was unforgiving, and the netcode was legendary for its time.
Fast forward to today. The gaming landscape has changed, GameSpy is a relic of the past, and Epic Games eventually pulled the plug on the official master servers.
The community, as always, found a way. Projects like OpenSpy stepped in to emulate the old GameSpy infrastructure, keeping UT2004 and dozens of other classic games alive. But recently, I wanted to dig deeper. I wanted to host my own infrastructure and see exactly how this classic game communicated under the hood.
There was just one problem: OpenSpy’s master server implementation isn't open source.
If you know anything about engineers driven by nostalgia, you know that closed source isn't a roadblock—it's an invitation. So, I decided to build my own. Here is how I reverse-engineered the UT2004 master server protocol and built OpenUT2k4MasterServer from scratch.
Diving Into the Matrix: Wireshark and Ghidra
Without access to the original server source code or OpenSpy's repository, I had to figure out how the game client and game server talked to the master server. This meant tackling the problem from two angles: observing the network traffic and decompiling the game's binary.
I started by routing my UT2004 client and a dedicated server through Wireshark to capture the packets talking to utmaster.openspy.net. Seeing the raw hex dump of GameSpy’s "enctype 2" protocol was like reading an ancient dialect.
To make sense of the bytes, I dropped UT2004's IpDrv.dll into Ghidra. By piecing together the decompiled C++ code with the packet captures, the puzzle finally started coming together.
The Game Client Protocol
When you click the "Internet" tab in UT2004, the client initiates a TCP connection to port 28902. It's a strict handshake process:
- Challenge: The server sends a challenge string.
- Auth: The client responds with hashes.
- Approval: The server replies with an
APPROVEDpacket. - Verification: The client sends a verification string (starting with
!). - Request: Once
VERIFIED, the client asks for either the MOTD (news) or the server list.
The Game Server Uplink
If you're hosting a server, it needs to tell the master server it exists. This also happens over TCP 28902 but behaves completely differently after the auth phase:
- After approval, the master server sends "state 2" session keys.
- The game server enters "state 3," keeping the connection alive.
- It sends heartbeats and, after about 60 seconds, fires off a massive refresh packet containing the server name, port, and gametype.
Building OpenUT2k4MasterServer
Armed with the protocol specifications, I fired up Go. Go is fantastic for network applications, making it incredibly easy to handle concurrent TCP connections, binary packing, and UDP listeners.
I built OpenUT2k4MasterServer, a fully open-source drop-in replacement for the UT2004 master server.
Core Features I Implemented:
- TCP Master Server (Port 28902): Handles both the intricate game client server list queries and the persistent game server uplinks.
- UDP Heartbeat Listeners (Ports 27900 & 28902): Catches the lightweight UDP pings servers send to prove they are still alive.
- MOTD / News Support: I even reverse-engineered the GameSpy color codes (e.g.,
\x1b\xff\xff\xff) so the in-game News tab populates correctly with custom messages. - HTTP Management API (Port 8090): Because it’s 2026, I added a modern JSON API to manually register servers and check the network status via the browser.
The Quirks of 2004 Tech
Working on this project reminded me of the fascinating quirks of older game engines. For example, UT2004 categorizes 127.0.0.1 servers strictly under the LAN tab. If you're testing an internet master server locally, you have to use your actual network IP, or you'll be scratching your head wondering why your server isn't showing up in the Internet list. Furthermore, strings are often passed around with hardcoded length bytes and null terminators—classic early 2000s C++ networking.
Keeping the Legacy Alive
There is something profoundly satisfying about writing modern Go code to interface with a C++ game engine compiled over two decades ago.
By open-sourcing the master server, my hope is that the UT2004 community never has to rely entirely on a single point of failure again. Whether you want to host a private master server for a massive LAN party, or just poke around the GameSpy protocol out of curiosity, the code is out there.
High school me would be pretty thrilled to know we’re still finding ways to keep the tournaments running.
If you want to check out the code, host your own server, or contribute, you can find the OpenUT2k4MasterServer repository on my GitHub. Happy fragging!