Teeworlds 0.7 sample traffic
If you are trying to build your own teeworlds compatible client or server. You have to understand what to send when.
So this page is showing annotated raw data of a real conversation between client and server. I very much recommend using
a wireshark dissector to inspect your traffic if you try to reimplement the protocol. This traffic example is not a complete
documentation of the whole protocol just the few essential packets to get you started. If you understand and reimplement those
you can establish a basic healthy connection.
In this example traffic we look at a client joing a server. The client already knows the map.
So no map download packets will be sent (download process is documented here instead).
The server sends all its basic info such as gametype, map name and similar.
Then after the client is connected it sends 1 input packet. In the end we cleanly close the connection with a disconnect packet.
All packets are prefixed with a [SOURCE->DESTINATION]
indicating who is sending the packet to who.
The packets are listed in the order they are supposed to be send when a client connects to a server.
The packet content is displayed as space seperated hexadecimal encoded bytes.
So 01
means that the bits 00000001
are sent via the network.
And FF
means that the bits 11111111
are sent via the network.
This matches the output of the network dumping tool tcpdump
.
If you have tcpdump installed on your system you can print teeworlds traffic using this command:
tcpdump -nX -i lo "port 8303"
Assuming your loopback network interface is called lo and you have a teeworlds server running on its default port 8303.
Instead of printing packets to the terminal you can also save them to a pcap file using:
tcpdump -nX -i lo -w tw.pcap "port 8303"
Which then can be loaded using wireshark. If you are motivated to compile and install a custom dissector I can highly recommend
the dissector from libtw2 by heinrich5991
which adds nice teeworlds related annotations to the packets.
[CLIENT->SERVER] Hello server this is my token
The first packet that is being sent is from the client to the server.
The only data that is being transmitted is the security token from the client.
The packet header contains FF FF FF FF
as placeholder for the server token.
Since the client does not know the server token yet.
At the end this packet contains 508 nullbytes to prohibit reflection attacks.
Those nullbytes are not shown below.
<04 00 00 FF FF FF FF><05><51 3B 59 46>
^ ^ ^
flags=1 | client token
(CONTROL) NET_CTRLMSG_TOKEN
[SERVER->CLIENT] Hello client this my token
As a response to the clients hello the server answers with its token. So after the packet header containing the clients token. The only data that is being sent. Is the servers security token.
<04 00 00 51 3B 59 46><05><C0 7D 1E 5E>
^ ^ ^ ^
flags=1 | | server token
(CONTROL) | NET_CTRLMSG_TOKEN
client token
[CLIENT->SERVER] Connect
After server and client exchanged tokens the client will once again send its token. This time setting the Control Message to NET_CTRLMSG_CONNECT to initiate the connection. At the end this packet contains 501 The full udp payload is 520 bytes. The 0.7 header is 7 bytes leaving 513 bytes. Then one byte is the control message type 0x05 (NET_CTRLMSG_TOKEN) Then we have 512 more bytes anti spoof. The first 4 bytes of these are the client token. And then remaining 508 are nullbytes. nullbytes to prohibit reflection attacks. Those nullbytes are not shown below.
<04 00 00 C0 7D 1E 5E><01><51 3B 59 46>
^ ^ ^ ^
flags=1 | | client token
(CONTROL) server token NET_CTRLMSG_CONNECT
[SERVER->CLIENT] Accept connection
If this packet is sent the server log will show this message.
[2022-10-25 14:22:25][connection]: got connection, sending accept
<04 00 00 51 3B 59 46><02>
^ ^ ^
flags=1 | |
(CONTROL) | NET_CTRLMSG_ACCEPT
client token
[CLIENT->SERVER] Version and password
Now the client will inform the server about its version.
This packet is defined in
CClient::SendInfo()
and contains the password, the network hash and the supported protocol version.
The vanilla client always sends the password that was entered last by the user.
No matter which server it is connecting to also if the server does not expect a password.
This is nice since it minimizes user input even if the ip changes or multiple servers have the same password.
But it is leaking the password you entered last to every server you connect to.
That is why the ddnet client decided to only send the password after user confirmation.
If the server receives this packet it will log the following line.
[2022-10-25 14:22:25][connection]: connecting online
Since the packet payload now contains uncompressed printable ASCII characters those are included below.
You can quickly identify this packet when running tcpdump
with the -X
flag.
[PACKET HEADER]
00 00 01 C0 7D 1E 5E
^
server token
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
400x40 hex
01000000 binary
^^
||
|vital flag set
|
resend flag unset
Click here for details on chunk header flags
28 01 030x03 hex
00000011 binary
^ ^^
\ /|
\ / |
1 |
| system flag set
v
1
NETMSG_INFO (message id 1)
30 2E 37 20 38 30 32 66 @(..0.7 802f
31 62 65 36 30 61 30 35 36 36 35 66 1be60a05665f
00 6D 79 5F 70 61 73 73 77 6F 72 64 my_password
5F 31 32 33 00 85 1C 00 _123...
[SERVER->CLIENT] Map info
Now the server is informing the client about the map. This is NOT the mapfile it self. Just its metadata. To see what exactly is being sent checkout the code of CServer::SendMap it boils down to this:
String GetMapName()
Int m_CurrentMapCrc
Int m_CurrentMapSize
Int m_MapChunksPerRequest
Int MAP_CHUNK_SIZE
Raw m_CurrentMapSha256
In the ASCII representation of the packet you can spot the mapname.
In this case the map is called
skyblock
.
[PACKET HEADER]
00 01 01 51 3B 59 46
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
40 34 01 05 73 6B 79 62 6C 6F 63 6B @4..skyblock
00 D8 D1 95 A0 0F A5 0E 08 A8 15 76 ...........v
BA 3A AE 46 F3 F5 E6 31 7A 8C BD 7E .:.F...1z..~
A4 B2 44 C4 7E 0E FF B5 86 7F 4B E3 ..D.~.....K.
CA E7 50 93 14 0D 88 00 ..P....
[CLIENT->SERVER] Ready
If the client already has the map or finished the download process it will respond with the ready packet.
For more info on how the traffic would look like if the client did not know the map see
the map_download section.
If the server gets this packet it will print the following log line.
[2022-10-25 14:22:25][server]: player is ready. ClientID=0 addr=[0:0:0:0:0:0:0:1]:61985
The client sends the enum NETMSG_READY
which has the value 18
but as a packed Integer
tangled in with the system flag.
This is how the server unpacks this message id:
int Msg = Unpacker.GetInt();
int Sys = Msg&1;
Msg >>= 1;
So in the raw packet data detecting the NETMSG_READY
flag is tricky.
Here some python code that unpacks
NETMSG_READY
to the Integer 18
given the raw byte 0x25
msg = 0x25
msg >>= 1
print(msg) # => 18 (NETMSG_READY)
This python code only works because the message id is below 63
Otherwise we would have to do some more complicated int unpacking
see the int packing section for more details.
<00 01 01 C0 7D 1E 5E><40 01 02> 25
^ ^ ^ ^
flags=0 | ChunkHeader NETMSG_READY
server token
[SERVER->CLIENT] Motd, Server Settings, Ready
Now the server sends the first packet with a compressed payload. It is using huffman compression to do so. The payload are 3 chunks:
- NETMSGTYPE_SV_MOTD - the message of the day
- NETMSGTYPE_SV_SERVERSETTINGS - custom server settings
- NETMSG_CON_READY - telling the client he can send his startinfo
<10 02 03 51 3B 59 46> 4A 42 88 4A 6E 16 BA 31 46 A2 84 9E BF E2 06
^ ^ ^
flags=4 client token compressed data
(COMPRESSION)
The payload (without the packet header) decompresses to this
<40 02 02 02 00> <40 07 03 22 01 00 01 00 01 08> <40 01 04 0b>
^ ^ ^
motd server_settings ready
[CLIENT->SERVER] Start info (skin data)
Now the client calls CGameClient::SendStartInfo()
which sends all the needed infos to render a tee in the world: Name, Clan, Country, Skinparts
You can see in the ASCII output that this client is connecting with the name nameless tee
and with an empty clantag. You can also identify the skinpart names duodonny and standard (falls back to default).
[PACKET HEADER]
00 04 01 C0 7D 1E 5E
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
41 19 03 36 6E 61 6D 65 6C 65 73 73 A..6nameless
20 74 65 65 00 00 40 73 70 69 6B 79 tee..@spiky
00 64 75 6F 64 6F 6E 6E 79 00 00 73 .duodonny..s
74 61 6E 64 61 72 64 00 73 74 61 6E tandard.stan
64 61 72 64 00 73 74 61 6E 64 61 72 dard.standar
64 00 01 01 00 01 01 01 A0 AC DD 04 d...........
BD D2 A9 85 0C 80 FE 07 80 C0 AB 05 ............
9C DE AA 05 9E C9 E5 01 ........
[SERVER->CLIENT] Vote Clear, Tune Params, Ready To Enter
This packet is usually compressed too. The payload are 3 chunks:- NETMSGTYPE_SV_VOTECLEAROPTIONS - TODO
- NETMSGTYPE_SV_TUNEPARAMS - TODO
- NETMSGTYPE_SV_READYTOENTER - telling the client he can join the game (spawn a tee)
[PACKET HEADER]
00 03 03 51 3B 59 46
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
40 01 05 16 41 05 06 0C A8 0F 88 03 @...A.......
32 A8 14 B0 12 B4 07 96 02 9F 01 B0 2...........
D1 04 80 7D AC 04 9C 17 32 98 DB 06 ...}....2...
80 B5 18 8C 02 BD 01 A0 ED 1A 88 03 ............
BD 01 B8 C8 21 90 01 14 BC 0A A0 9A ....!.......
0C 88 03 80 E2 09 98 EA 01 A4 01 00 ............
A4 01 A4 01 40 01 07 10 ....@...
[CLIENT->SERVER] Enter game
Now the client sends NETMSG_ENTERGAME (19)
using CClient::SendEnterGame()
Keep in mind those net messages are packed and mixed with the system flag. So hex 0x27
will be unpacked to the integer 19
.
If the server gets this packet it will print the following log line.
[2022-10-25 14:22:25][server]: player has entered the game. ClientID=0 addr=[0:0:0:0:0:0:0:1]:61985
<00 07 01 C0 7D 1E 5E> <40 01 04> 27
^ ^ ^
server token ChunkHeader NETMSG_ENTERGAME
[SERVER->CLIENT] Server info
The server sends version, map name, gametype, server name and more prepared in the method
CServer::GenerateServerInfo(CPacker *pPacker, int Token)
The same NETMSG_SERVERINFO
message is used for the server browser where it includes
additional information like the player list.
[PACKET HEADER]
00 04 01 51 3B 59 46
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
40 29 08 09 30 2E 37 2E 35 00 75 6E @)..0.7.5.un
6E 61 6D 65 64 20 73 65 72 76 65 72 named server
00 00 73 6B 79 62 6C 6F 63 6B 00 44 ..skyblock.D
4D 00 00 01 01 08 01 08 M.......
[SERVER->CLIENT] Game Info, Client Info, Snap Single
The payload are 3 chunks:
- NETMSGTYPE_SV_GAMEINFO - score limit, time limit and match
- NETMSGTYPE_SV_CLIENTINFO - all players name, skin and other infos
- NETMSG_SNAPSINGLE - snapshot data (tee positions and similar)
<10 04 03 51 3B 59 46> 4A 36 4C ED E1 47 DE...
^ ^
client token compressed payload
Here is what it decompresses to.
Note that the 4th byte of every chunk is the message id.
They are packed integers
and the last bit is the system flag. Thats how for example
the byte hex 0x26
represents the decimal
message id 29
(NETMSGTYPE_SV_GAMEINFO).
[PACKET HEADER]
10 04 03 51 3B 59 46
[GAME INFO] [GAME INFO ASCII]
40 06 09 26 00 14 00 00 01 @..&.....
^ ^ ^ ^
flags| | |
vital| | |
size| |
6 | |
sequence |
9 |
|
NETMSGTYPE_SV_GAMEINFO
19
[CLIENT INFO] [CLIENT INFO ASCII]
41 1e 0a 240x24 hex
36 decimal
100100 binary
^ ^^
\ /|
10010|
| |
| game message
v
18
NETMSGTYPE_SV_CLIENTINFO
00 01 00 6e 61 6d 65 6c A..$...namel
65 73 73 20 74 65 65 00 00 40 67 72 ess tee..@gr
65 65 6e 73 77 61 72 64 00 64 75 6f eensward.duo
64 6f 6e 6e 79 00 00 73 74 61 6e 64 donny..stand
61 72 64 00 73 74 61 6e 64 61 72 64 ard.standard
00 73 74 61 6e 64 61 72 64 00 01 01 .standard...
00 00 00 00 80 fc af 05 eb 83 d0 0a ............
80 fe 07 80 fe 07 80 fe 07 80 fe 07 ............
00 .
[SNAP SINGLE] [SNAP SINGLE ASCII]
01 18 110x11 hex
17 decimal
010001 binary
^ ^^
\ /|
01000|
| |
| system message
v
8 NETMSG_SNAPSINGLE
ac 31 ad 31 ae 89 01 8e 01 ....1.1.....
00 08 00 04 01 b0 03 90 02 03 04 02 ............
90 04 90 02 02 04 03 b0 04 90 02 04 ............
04 04 b0 07 b0 06 01 04 05 b0 08 b0 ............
06 00 06 00 00 01 00 0a 00 a6 31 90 ..........1.
0b b1 06 00 80 02 00 00 00 40 00 00 .........@..
90 0b b0 06 00 00 0a 00 0a 01 00 00 ............
00 0b 00 08 00 00 ......
[SERVER->CLIENT] Snap Single
If nothing further happens. Nobody presses any button, reconnects or sends a chat message.
The server will keep sending this snap single packet. If the client then sends a packet and
acknowledges the snap the server will send
NETMSG_SNAPEMPTY
until there is new snap data.
The snap is the most complicated message of all.
Its payload is snap metadata and a list of integers representing
snap items.
Since it is compressed below only the first few bytes are shown.
<10 04 03 51 3B 59 46> B5 36 45 11 5D 86 1A...
^ ^
client token compressed payload
Here is what it decompresses to. Again NETMSG_SNAPSINGLE (8)
is extracted out of 0x11
together with the system flag.
[PACKET HEADER]
10 04 03 51 3B 59 46
[PAYLOAD HEADER]
<00 33><110x11 hex
17 decimal
010001 binary
^ ^^
\ /|
01000|
| |
| system message
v
8 NETMSG_SNAPSINGLE
> B8 01 B9 01 84 11 2B 00 03 .3.......+..
^ ^
| NETMSG_SNAPSINGLE
Flags & Size
[PACKET PAYLOAD HEX] [PACKET PAYLOAD ASCII]
00 06 00 00 01 00 0A 00 A6 01 90 04 ............
91 02 00 80 02 00 00 00 40 00 00 90 ........@...
04 90 02 00 00 0A 00 0A 01 00 00 00 ............
0B 00 08 00 00 00 00 00 .....
[CLIENT->SERVER] Client Input (hook/move/fire/jump)
Also the client uses huffman compression to send its inputs. It makes a call to CClient::SendInput() which sends the the acknowledged gametick and the prediction tick so the server knows which snapshots to (re)send. After that it packs a bunch of integers representing the following struct
struct CNetObj_PlayerInput
{
int m_Direction;
int m_TargetX;
int m_TargetY;
int m_Jump;
int m_Fire;
int m_Hook;
int m_PlayerFlags;
int m_WantedWeapon;
int m_NextWeapon;
int m_PrevWeapon;
};
The inputs are only sent if there is new data.
If there is none it will only acknowledged the gameticks.
So the compressed data that is sent via the network looks like this.
<10 0A 01 C0 7D 1E 5E> 4D E9 B0 14 7E 13 D6 F8 6F 57 DC 00
^ ^
flags=4 compressed payload
(COMPRESSION)
Here is what it decompresses to. Again NETMSG_INPUT (20)
is extracted out of 0x29
as seen in this python snippet:
msg = 0x29
msg >>= 1
print(msg) # => 20 (NETMSG_INPUT)
This python code only works because the message id is below 63
Otherwise we would have to do some more complicated int unpacking
see the int packing section for more details.
[PACKET HEADER]
10 0A 01 C0 7D 1E 5E
[PACKET PAYLOAD]
<00 11> 29 82 02 83 02 28 00 01 00 00 00 00 00 00 00 00 05
^ ^
ChunkHeader |
size = 17 NETMSG_INPUT
flags = 0
[SERVER->CLIENT] Input Timing, Snap Empty
After the client sends some input the server will respond with input timing. Which is usually bundled with a snap chunk. In this case there is no new snap data so the snap chunk is of type SNAP_EMPTY.
[PACKET HEADER]
00 0A 02 C0 7D 1E 5E
^ ^ ^ ^
| | | client token
| | 2 chunks (Input Timing, Snap Empty)
| sequence number unchanged
no flags (not compressed for example)
[PACKET PAYLOAD]
00 04 02 24 05 cd fd 00 04 15 81 32 ...$ .... ...2
0d 00 04 0f 82 32 02 .... .2.
[CLIENT->SERVER] Client says bye
[SERVER->CLIENT] Server says bye
If a client is properly shutdown or the user presses the disconnect button.
CNetConnection::Disconnect(const char *pReason)
sends a NET_CTRLMSG_CLOSE (4)
. The same can also be sent by the server if it shutdowns for example.
At the end optionally this packet can contain a string containing the reason of closing the connection.
You probably have seen those disconnect messages in the chat already:
*** 'nameless tee' has left the game (Timeout)
*** 'nameless tee' has left the game (Too weak connection (not acked for 10 seconds))
*** 'hacker' has left the game (custom message)
The server can also use this reason to show a custom shutdown message to the client.
<04 0A 00 C0 7D 1E 5E><04>
^ ^ ^
flags=1 client/server NET_CTRLMSG_CLOSE
(CONTROL) token