The MU Online protocol, explained
Every MU Online server emulator begins at the same place: a TCP socket, a stream of bytes, and nobody who ever wrote down what they mean. Everything the community knows about this protocol was taken apart packet by packet, and after ten years of doing that in OpenMU, it seems worth writing the overview down in one piece.
This is the tour: how a packet is framed, what the three things called "encryption" actually are, how a client gets from a connect server into the world - and what we changed once we had a client of our own.
The frame
Every packet starts with a byte which says two things at once: whether the payload is encrypted, and how long the packet is allowed to be.
| First byte | Encrypted | Length field | Maximum size |
|---|---|---|---|
0xC1 |
no | 1 byte | 255 bytes |
0xC2 |
no | 2 bytes | 65535 bytes |
0xC3 |
yes | 1 byte | 255 bytes |
0xC4 |
yes | 2 bytes | 65535 bytes |
After the length comes the packet code, and for some codes a subcode. That's the whole framing, and it gets small: a packet can consist of nothing but its header. When you close an NPC dialog, your client sends three bytes.
C1 03 31
C1: unencrypted, one length byte. 03: three bytes in total, the header included. 31: the code, which means "I closed the dialog". There is nothing else to say, so nothing else is sent. A good part of the protocol looks like this - in OpenMU's definitions, 69 of the packets a client can send are four bytes or shorter.
With a subcode it's one byte more. This is the greeting the connect server sends right after a client connects:
C1 04 00 01
Code 00, subcode 01, no payload: "hello, I'm here". A game client answers it by asking for the server list.
Subcodes are not a sign that the code space ran out - roughly half of the 256 possible codes are still unused today. They group what belongs together. F1 is the session: 00 for having entered the game server, 01 for login, 02 for logout, 03 for the logout the client sends when its own cheat detection triggers. F3 is everything about a character - list, creation, deletion, selection, stat points - and features which came later got a code of their own with the whole feature behind its subcodes: B2 for castle siege, F6 for quests, AA for duels, 3F for player shops. In OpenMU's definitions, the server sends 129 distinct codes, 25 of which carry subcodes; F3 alone holds 48 different packets. And, as I mentioned in an earlier post, the same message can have a different code depending on which localized client the player runs - the hit packet is 0x11 in the English client and 0xD6 in the Japanese one. The protocol has no version field to ask; the server has to know.
One more detail in encrypted packets: the first byte of the decrypted payload is a counter which runs from 0x00 to 0xFF, so that two identical packets never look identical on the wire. It makes replay attacks harder - at least for attackers who can't count.
Three things called encryption
"MU packet encryption" refers to three different mechanisms, applied in different places and in different directions.
SimpleModulus is the block cipher behind C3 and C4. According to a Korean change log it appeared in version 0.74.01, originally with 16 keys encrypting blocks of 32 bytes. Somewhere between 0.75 and 0.97 the block size was reduced to 8 bytes, so each block is now touched by 4 keys instead of 16. I wrote about how it works in detail and later revisited it.
Xor32 is a rolling XOR with a 32 byte key, applied on top of SimpleModulus in the client-to-server direction. Webzen changed those keys during maintenances, which tells you how much they trusted them. They're also really easy to be calculated, if you have the encrypted and unencrypted content of at least one longer packet. Hint: When you send a long chat message, the client sends it encrypted. The server sends the same message back, but unencrypted. Additionally, a chat message structure is so simple, the unencrypted content can be guessed.
Xor3 is three bytes - 0xFC 0xCF 0xAB - and it protects your login credentials. That is not a typo, and those three bytes have been public for two decades.
Which is the honest summary of this whole layer: the algorithms and keys have been known in the server and cheater community for well over ten years, and the keys can be derived from known packet content or brute-forced in a fraction of a second. This is obfuscation, not security. Everything arriving from a client is hostile input, and the server has to validate every single thing a packet claims - which is exactly why item duplication bugs were possible in the first place. If you run OpenMU for real, the encryptors sit behind interfaces so you can replace them, and you should.
From connecting to standing in Lorencia
The path a client takes is short and worth knowing, because most "I can't connect" reports fail somewhere in the middle of it.
First, the connect server. It's a separate, tiny service whose only job is to tell clients where the game servers are. The conversation is four packets: the server says hello, the client asks for the server list, the server answers with the list plus a load percentage per server, the client asks for the connection info of the one it picked, and gets an IP and a port back.
Then, the game server. The client opens a second connection, this time encrypted, and sends its login with the credentials wrapped in the three-byte Xor3, wrapped by SimpleModulus. The server answers with the character list, the client picks one, and after a short exchange the character enters the world - which is the moment the server starts sending the endless stream of "these objects are now in your scope" and "this one moved" packets that make up the actual game.
We describe packets as data, not as code
The interesting part of maintaining this protocol isn't any single packet - it's that there are so many of them. OpenMU currently defines 195 packets sent by the client, 281 sent by the game server, 11 for the connect server and 7 for the chat server.
Nobody wants to write and maintain that as hand-written parsing code, so we don't. Every packet is described in XML - its code, its subcode, its fields with their types and offsets, what it does, and what it causes on the other side - and everything else is generated from that description: the C# structs which wrap the raw bytes without allocating, the extension methods for sending, the tests, the 500 markdown files in the repository's docs/Packets folder, and these days even the C++ bindings the client uses. I wrote about that machinery when it was new, in handling packet structures in .NET and generating message structs by data.
The practical effect is that "the protocol" isn't spread over the codebase. It's four XML files, and everything which speaks it derives from them.
What we changed
For most of OpenMU's life, all of the above was fixed: the client was a binary we couldn't touch, so the protocol was a given. Since we have our own client, it isn't anymore.
The extensions are the ones the old format made impossible. Damage and experience are no longer capped at 16 bits, so a big hit is one packet instead of a loop of them. Items are serialized dynamically in 5 to 15 bytes with a flags byte, instead of squeezing every property into a fixed 12-byte bit puzzle. The appearance of a character gets 27 bytes instead of 18. Monsters send a health bar after being hit. The server tells the client which chat commands it may use.
None of that breaks the original client, because the server picks its view implementations per client version: an original Season 6 client gets the original packets, and our client - which announces itself as season 106 - gets the extended ones. Same server, same game logic, different bytes on the wire.
If you want to look at it yourself
Two things in the repository are worth knowing about. The docs/Packets folder contains a markdown page per packet, generated from the definitions, including when it's sent and what it triggers - that's the closest thing to a protocol specification that exists. And the network analyzer sits between a client and a server, shows the traffic live, resolves it against the packet definitions and shows you the values instead of raw bytes.
Both of them exist because reading someone else's protocol is normally a lonely activity with a hex editor. It doesn't have to be.