Quake 3 network protocol 43 proxy server

I decided to experiment with java NIO and thought it’d be useful to resurrect my quake 3 proxy server. The completed code may be a useful followup to an original article I wrote many years ago about the Quake 3 network protocol

If I get time I’ll create a version for protocol 68 that can decode the Huffman compressed packet data on the fly, but don’t hold your breath!

Instructions can be found in the code comments, otherwise leave me a message below.

package uk.org.tilion.quake3.proxy;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
 * A Quake3 proxy server that communicates using protocol 43, 1.11 - 1.16 point
 * release of quake3 engine. It's designed to handle a single client and provide
 * a way of experimenting with the quake3 protocol on the fly.
 * 
 * Packet fragmentation is ignored for simplicity, with sequenceId being read
 * without performing fragmentation checks.
 * 
 * Example handshake (OOB prefixed packets)
 *   CLIENT : getChallenge
 *   SERVER : challengeResponse <ID>
 *   CLIENT : connect "cg_predictItems1sexmalehandicap100color3snaps40rate10000modeldoom/rednameUnnamaedPlayerprotocol68qport<PORT>challenge<ID>"
 *   SERVER : connectResponse
 * 
 * Client game data packet
 * +-----------------+-----+----------+-----------+
 * | NAME            | LEN | ENCODING | ENDIANESS |
 * +-----------------+-----+----------+-----------+
 * |  SequenceNumber |  32 |     None |    Little |
 * |           QPort |  16 |     None |    Little |
 * |                 |     |      XOR |           |
 * 
 * Server game data packet
 * +-----------------+-----+----------+-----------+
 * | NAME            | LEN | ENCODING | ENDIANESS |
 * +-----------------+-----+----------+-----------+
 * |  SequenceNumber |  32 |     None |    Little |
 * |                 |     |      XOR |           |
 * 
 * If anyone gets time to implement the XOR decoding, do let me know! I suspect
 * it's the Netchan_UnScamblePacket() function from qcommon/net_chan.c in the 
 * 1.32 full source release.
 * 
 * @author Darren Edmonds
 */
public class Protocol43ProxyServer {

    private static final int BUFFER_SIZE = 1024 * 2;  // size in bytes
    
    private int localPort;
    private SocketAddress serverAddr;
    private SocketAddress clientAddr;
    private DatagramChannel serverChannel; // proxy <==> quake3 server
    private DatagramChannel clientChannel; // proxy <==> quake3 client
    private boolean running;
    
    Protocol43ProxyServer(int localPort, String remoteServer, int remotePort) {
        this.localPort = localPort;
        this.serverAddr = new InetSocketAddress(remoteServer, remotePort);
    }

    /**
     * Start the proxy server
     * @throws IOException 
     */
    public void start() throws IOException {
        Selector selector = Selector.open();
        
        this.clientChannel = DatagramChannel.open();
        this.clientChannel.configureBlocking(false);
        this.clientChannel.socket().bind(new InetSocketAddress(this.localPort));
        this.clientChannel.register(selector, SelectionKey.OP_READ);
        
        this.serverChannel = DatagramChannel.open();
        this.serverChannel.configureBlocking(false);
        this.serverChannel.socket().bind(null);
        this.serverChannel.register(selector, SelectionKey.OP_READ);
        
        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
        buffer.order(ByteOrder.BIG_ENDIAN);

        this.running = true;
        Iterator<SelectionKey> it = null;
        SelectionKey key = null;
        while (this.running) {
            int n = selector.select();
            if (n == 0) continue; // nothing to do
            
            it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                key = it.next();
                
                if (key.isReadable()) {
                    this.handlePacket((DatagramChannel)key.channel(), buffer);
                }
                
                it.remove();
            }
        }
    }

    /**
     * Read a packet of data from the client (or server) datagram channel then
     * proxy it over to the other channel.
     * @param channel
     * @param buffer
     * @throws IOException 
     */
    private void handlePacket(DatagramChannel channel, ByteBuffer buffer)
            throws IOException {
        buffer.clear();
        
        if (channel == this.clientChannel) {
            SocketAddress sender;
            
            while ((sender = channel.receive(buffer)) != null) {
                if (this.clientAddr == null) this.clientAddr = sender;
                //System.out.println("CLIENT sent " + buffer.position() + " bytes");
                inspectClientPacket(buffer);
                writePacket(this.serverChannel, buffer, this.serverAddr);
            }
            
        } else {
            while (channel.receive(buffer) != null) {
                //System.out.println("SERVER sent " + buffer.position() + " bytes");
                inspectServerPacket(buffer);
                writePacket(this.clientChannel, buffer, this.clientAddr);
            }
        }
    }

    /**
     * Write buffer to channel, transmitting content to recipient
     * @param channel
     * @param buffer
     * @param recipient
     * @throws IOException 
     */
    private void writePacket(DatagramChannel channel, ByteBuffer buffer,
            SocketAddress recipient) throws IOException {
        buffer.flip();
        while (buffer.hasRemaining()) {
            channel.send(buffer, recipient);
        }
    }

    /**
     * Chance to inspect/modify packet sent from client before it is relayed
     * to the server
     * @param buffer
     */
    private void inspectClientPacket(ByteBuffer buffer) {
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        int sequenceId = buffer.getInt(0);

        if (sequenceId == -1) { // 4 OOB bytes in the header
            byte[] textArr = new byte[buffer.position() - 4];
            int oldPos = buffer.position();
            buffer.position(4);
            buffer.get(textArr);
            buffer.position(oldPos);

            String command = new String(textArr, Charset.forName("UTF-8"));
            System.out.println("CLIENT " + command);
            
            if (command.startsWith("connect ")) {
                /* rewrite the qport value to reflect the proxy local port
                 * rather than the client local port - not critical for testing
                 * but required for multiple clients on same IP via NAT */
            }
        } else {
            int qport = buffer.getShort(4);
            System.out.println("CLIENT seq=  " + String.format("%15d", sequenceId));
            System.out.println("CLIENT qport=" + String.format("%15d", qport));
        }
        
        buffer.order(ByteOrder.BIG_ENDIAN);
    }

    /*
     * Chance to inspect/modify packet sent from server before it is relayed
     * to the client
     * @param buffer
     */
    private void inspectServerPacket(ByteBuffer buffer) {
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        int sequenceId = buffer.getInt(0);
        
        if (sequenceId == -1) { // 4 OOB bytes in the header
            byte[] textArr = new byte[buffer.position() - 4];
            int oldPos = buffer.position();
            buffer.position(4);
            buffer.get(textArr);
            buffer.position(oldPos);

            String command = new String(textArr, Charset.forName("UTF-8"));
            System.out.println("SERVER " + command);
            
            if (command.startsWith("connectResponse")) {
                /* server puts client into connecting state and starts sending
                 * game updates */
            }
        } else {
            System.out.println("SERVER seq=  " + String.format("%15d", sequenceId));
        }
        
        buffer.order(ByteOrder.BIG_ENDIAN);
    }

    /**
     * Main
     * @param args 
     */
    public static void main(String[] args) {
        Protocol43ProxyServer server = new Protocol43ProxyServer(
                27960, 
                "myserver.com", // CHANGE TO REAL SERVER IP
                27961); // CHNAGE TO REAL SERVER PORT
        try {
            server.start();
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        
        /* now start quake3.exe and connect localhost:27960
         * your connection will end up at myserver.com:27961, proxied via
         * localhost to allow you to inspect packets on the fly */
    }
}

Quake 3 network protocol

This document describes the network protocol that quake 3 uses to converse with clients and the outside world (query servers). Currently more of a work in progress.

I recently (August 2012) added a blog entry with a revised version of my protocol 43 proxy server incase anyone finds it useful to continue experimenting.

Query

To query a server is very simple. Send a connectionless (UDP) packet with 4 OOB header bytes (0xff) and the text string getstatus. There are many sites which contain a thorough description of this so I won’t go into details.

Game Protocol 68 – used by 1.32

All game packets are connectionless (UDP), but there is still a handshaking process which must occur before you are allowed to join the server.

The client sends a challenge request (sometimes you need to send multiple requests before the server will respond).

+------------+----------------+
| Header     | Content        |
+------------+----------------+
| 0xffffffff | getchallenge   |
+------------+----------------+

If the server is able to accept more connections it will reply with.

+------------+------------------------+
| Header     | Content                |
+------------+------------------------+
| 0xffffffff | challengeResponse <ID> |
+------------+------------------------+

Once the client has the <ID> it can send a connect request. However, the CS is huffman compressed in protocol 68 and is NOT clear text as indicated below. Protocol 43 uses the plain text version.

+------------+----------------+
| Header     | Content        |
+------------+----------------+
| 0xffffffff | connect "<CS>" |
+------------+----------------+

<CS> represents a connection string containing the player details, e.g.
\cg_predictItems\1\sex\male\handicap\100\color\3\snaps\40\rate\10000\model\doom/red\name\UnnamaedPlayer\protocol\68\qport\<PORT>\challenge\<ID>
<PORT> represents the local port used to send this packet

If the connect is successful the server will reply with the following

+------------+-----------------+
| Header     | Content         |
+------------+-----------------+
| 0xffffffff | connectResponse |
+------------+-----------------+

The server will now place you in the CNCT (connecting) state and start sending you game updates.

This is where the communication gets substantially more difficult, so I warn you what follows may be incomplete and perhaps incorrect – although I hope not!.

Client to Server

+-----------------------------------------------------------------------------------------------+
| NAME                    | LEN | ENCODING     | COMMENT                                        |
+-------------------------+-----+---------------------------------------------------------------+
| sequenceNumber          | 32  | None         | MSG_ReadLong                                   |
| qport                   | 16  | None         | MSG_ReadShort                                  |
| serverId                | 32  | Huff (1)     | MSG_ReadLong                                   |
| messageAcknowledge      | 32  | Huff (1)     | MSG_ReadLong                                   |
| reliableAcknowledge     | 32  | Huff (1)     | MSG_ReadLong                                   |
+-------------------------+-----+--------------+------------------------------------------------+
| clientCommand           | 8   | (1), XOR (2) | MSG_ReadByte                                   |
| ...                     |     |              |                                                |
+-------------------------+-----+--------------+------------------------------------------------+

Client commands

0 - clc_bad
1 - clc_nop
2 - clc_move
3 - clc_moveNoDelta
4 - clc_clientCommand
5 - clc_EOF

Server to Client

+-----------------------------------------------------------------------------------------------+
| NAME                    | LEN | ENCODING     | COMMENT                                        |
+-------------------------+-----+---------------------------------------------------------------+
| sequenceNumber          | 32  | None         | MSG_ReadLong                                   |
| reliableAcknowledge     | 32  | Huff (1)     | MSG_ReadLong                                   |
+-------------------------+-----+--------------+------------------------------------------------+
| serverCommand           | 8   | (1), XOR (3) | MSG_ReadByte                                   |
| ...                     |     |              |                                                |
+-------------------------+-----+--------------+------------------------------------------------+

Server Commands

0 - svc_bad
1 - svc_nop
2 - svc_gamestate
3 - svc_configstring
4 - svc_baseline
5 - svc_serverCommand
6 - svc_download
7 - svc_snapshot
8 - svc_EOF

Details

(1) – Huffman compression using a predefined set of nodes to further reduce message length (detailed below).

int msg_hData[256] = {
250315,// 0
41193,// 1
6292,// 2
7106,// 3
3730,// 4
3750,// 5
6110,// 6
23283,// 7
33317,// 8
6950,// 9
7838,// 10
9714,// 11
9257,// 12
17259,// 13
3949,// 14
1778,// 15
8288,// 16
1604,// 17
1590,// 18
1663,// 19
1100,// 20
1213,// 21
1238,// 22
1134,// 23
1749,// 24
1059,// 25
1246,// 26
1149,// 27
1273,// 28
4486,// 29
2805,// 30
3472,// 31
21819,// 32
1159,// 33
1670,// 34
1066,// 35
1043,// 36
1012,// 37
1053,// 38
1070,// 39
1726,// 40
888,// 41
1180,// 42
850,// 43
960,// 44
780,// 45
1752,// 46
3296,// 47
10630,// 48
4514,// 49
5881,// 50
2685,// 51
4650,// 52
3837,// 53
2093,// 54
1867,// 55
2584,// 56
1949,// 57
1972,// 58
940,// 59
1134,// 60
1788,// 61
1670,// 62
1206,// 63
5719,// 64
6128,// 65
7222,// 66
6654,// 67
3710,// 68
3795,// 69
1492,// 70
1524,// 71
2215,// 72
1140,// 73
1355,// 74
971,// 75
2180,// 76
1248,// 77
1328,// 78
1195,// 79
1770,// 80
1078,// 81
1264,// 82
1266,// 83
1168,// 84
965,// 85
1155,// 86
1186,// 87
1347,// 88
1228,// 89
1529,// 90
1600,// 91
2617,// 92
2048,// 93
2546,// 94
3275,// 95
2410,// 96
3585,// 97
2504,// 98
2800,// 99
2675,// 100
6146,// 101
3663,// 102
2840,// 103
14253,// 104
3164,// 105
2221,// 106
1687,// 107
3208,// 108
2739,// 109
3512,// 110
4796,// 111
4091,// 112
3515,// 113
5288,// 114
4016,// 115
7937,// 116
6031,// 117
5360,// 118
3924,// 119
4892,// 120
3743,// 121
4566,// 122
4807,// 123
5852,// 124
6400,// 125
6225,// 126
8291,// 127
23243,// 128
7838,// 129
7073,// 130
8935,// 131
5437,// 132
4483,// 133
3641,// 134
5256,// 135
5312,// 136
5328,// 137
5370,// 138
3492,// 139
2458,// 140
1694,// 141
1821,// 142
2121,// 143
1916,// 144
1149,// 145
1516,// 146
1367,// 147
1236,// 148
1029,// 149
1258,// 150
1104,// 151
1245,// 152
1006,// 153
1149,// 154
1025,// 155
1241,// 156
952,// 157
1287,// 158
997,// 159
1713,// 160
1009,// 161
1187,// 162
879,// 163
1099,// 164
929,// 165
1078,// 166
951,// 167
1656,// 168
930,// 169
1153,// 170
1030,// 171
1262,// 172
1062,// 173
1214,// 174
1060,// 175
1621,// 176
930,// 177
1106,// 178
912,// 179
1034,// 180
892,// 181
1158,// 182
990,// 183
1175,// 184
850,// 185
1121,// 186
903,// 187
1087,// 188
920,// 189
1144,// 190
1056,// 191
3462,// 192
2240,// 193
4397,// 194
12136,// 195
7758,// 196
1345,// 197
1307,// 198
3278,// 199
1950,// 200
886,// 201
1023,// 202
1112,// 203
1077,// 204
1042,// 205
1061,// 206
1071,// 207
1484,// 208
1001,// 209
1096,// 210
915,// 211
1052,// 212
995,// 213
1070,// 214
876,// 215
1111,// 216
851,// 217
1059,// 218
805,// 219
1112,// 220
923,// 221
1103,// 222
817,// 223
1899,// 224
1872,// 225
976,// 226
841,// 227
1127,// 228
956,// 229
1159,// 230
950,// 231
7791,// 232
954,// 233
1289,// 234
933,// 235
1127,// 236
3207,// 237
1020,// 238
927,// 239
1355,// 240
768,// 241
1040,// 242
745,// 243
952,// 244
805,// 245
1073,// 246
740,// 247
1013,// 248
805,// 249
1008,// 250
796,// 251
996,// 252
1057,// 253
11457,// 254
13504,// 255
};

(2) – XOR algorithm used by the server to decode the message content.

#define CL_ENCODE_START 12
byte key, *string;
int i, index;

string = (byte *)clc.serverCommands[ reliableAcknowledge &amp; (MAX_RELIABLE_COMMANDS-1) ];
index = 0;
//
key = clc.challenge ^ serverId ^ messageAcknowledge;
for (i = CL_ENCODE_START; i &lt; msg-&gt;cursize; i++) {
    // modify the key with the last received now acknowledged server command
    if (!string[index]) {
        index = 0;
    }

    if (string[index] &gt; 127 || string[index] == '%') {
        key ^= '.' &lt;&lt; (i &amp; 1);
    }  else {
        key ^= string[index] &lt;&lt; (i &amp; 1); } index++; // encode the data with this key *(msg-&gt;data + i) = (*(msg-&gt;data + i)) ^ key;
}

(3) – XOR algorithm used by the client to decode the message content.

Notes

data has mixed endianess – is this right?

Packet fragmentation is worked out using a sequencetNumber & FRAGMENT_BIT (where FRAGMENT_BIT is 1<<31) calculation. If fragmented, flip the bit to 0 to correct the sequence number.

cl_shownet 1 – MSG_SIZE
cl_shownet 2|3 – READ_COUNTCMD
showpackets 1 – WHO recv MSG_SIZE : s=SEQ_NO (optional fragment info)

Acknowledgements