(Client -> Server)
denotes packets from client to server.(Client <- Server)
denotes packets from server to client.- All datatypes are sent in network order (Big Endian) unless otherwise specified.
- Datatypes are declared with a prefixing
u
ori
for unsigned and signed and a number for the bitlength.
For exampleu8
would be be the C equivalent ofuint8
orunsigned char
- Arrays are represented by the underlying datatype in square brackets,
additionally if the length is known it is added in the brackets, separated by
a semicolon. Eg:
[u8]
,[i32; 16]
- Array ranges (parts of an array) are specified in square brackets with the
included lower bound, a minus and the excluded upper bound. Eg:
[0-10]
- The packets are build in a fixed scheme, though have differences depending in which direction.
- Every column here represents 1 byte.
- The entire packet size must be at max 500 bytes.
+--+--+--+--+--+--+--+--+--+--+--+--+--+---------//----------+
| MAC | PId | CId |PT| Data |
+--+--+--+--+--+--+--+--+--+--+--+--+--+---------//----------+
| \ Meta |
\ Header /
Name | Size | Datatype | Explanation |
---|---|---|---|
MAC | 8 bytes | [u8] | EAX Message Authentication Code |
PId | 2 bytes | u16 | Packet Id |
CId | 2 bytes | u16 | Client Id |
PT | 1 byte | u8 | Packet Type + Flags |
Data | <=487 bytes | [u8] | The packet payload |
+--+--+--+--+--+--+--+--+--+--+--+------------//-------------+
| MAC | PId |PT| Data |
+--+--+--+--+--+--+--+--+--+--+--+------------//-------------+
| \ Meta |
\ Header /
Name | Size | Datatype | Explanation |
---|---|---|---|
MAC | 8 bytes | [u8] | EAX Message Authentication Code |
PId | 2 bytes | u16 | Packet Id |
PT | 1 byte | u8 | Packet Type + Flags |
Data | <=489 bytes | [u8] | The packet payload |
0x00
Voice0x01
VoiceWhisper0x02
Command0x03
CommandLow0x04
Ping0x05
Pong0x06
Ack0x07
AckLow0x08
Init1
The final byte then looks like this
MSB LSB
+--+--+--+--+--+--+--+--+
|UE|CP|NP|FR| Type |
+--+--+--+--+--+--+--+--+
Name | Size | Hex | Explanation |
---|---|---|---|
UE | 1 bit | 0x80 | Unencrypted |
CP | 1 bit | 0x40 | Compressed |
NP | 1 bit | 0x20 | Newprotocol |
FR | 1 bit | 0x10 | Fragmented |
Type | 4 bit | 0-8 | The packet type |
To reduce a packet size the data can be compressed.
When the data is compressed the Compressed
flag must be set.
The algorithm "QuickLZ" is used for compression.
QuickLZ offers different compression levels.
The chosen level differs depending on the packet direction as following
- (Client -> Server) Level 1
- (Client <- Server) Level 3
When the packet payload exceeds the maximum datablock size the data can be
split up across multiple packets.
When splitting occurs, the Fragmented
flag must be set on the first and
the last packet. Other flags, if set, are only set on the first packet.
The data can additionally be compressed before splitting.
When a packet is not encrypted the Unencrypted
flag is set. For encrypted
packets the flag gets cleared.
Packets get encrypted with EAX mode (AES-CTR with OMAC).
The en/decryption parameters get generated for each packet as follows
Name | Type | Explanation |
---|---|---|
PT | u8 | Packet Type |
PId | u16 | Packet Id |
PGId | u32 | Packet GenerationId (see 1.9.2) |
PD | bool | Packet Direction |
SIV | [u8; 20] | Shared IV (see 3.2) |
let temporary: [u8; 26]
temporary[0] = 0x30 if (Client <- Server)
0x31 if (Client -> Server)
temporary[1] = PT
temporary[2-6] = (PGId in network order)[0-4]
temporary[6-26] = SIV[0-20]
let keynonce: [u8; 32]
keynonce = SHA256(temporary)
key: [u8; 16] = keynonce[0-16]
nonce: [u8; 16] = keynonce[16-32]
key[0] = key[0] xor ((PId & 0xFF00) >> 8)
key[1] = key[1] xor ((PId & 0x00FF) >> 0)
The data can now be encrypted with the key
and nonce
from (see 1.6.2) as the
EAX key and nonce and the packet Meta
as defined in (see 1.1) as the EAX
header (sometimes called "Associated Text"). The resulting EAX mac (sometimes
called "Tag") will be stored in the MAC
field as defined in (see 2.1).
When a packet is not encrypted no MAC can be generated by EAX. Therefore the SharedMac (see 3.2) will be used.
This stack is a reference for the execution order of the set data operations. For incoming packets the stack is executed bot to top, for outgoing packets top to bot.
+-----------+
| Data |
+-----------+
| Compress |
+-----------+
| Split |
+-----------+
| Encrypt |
+-----------+
The following chapter describes the data structure for different packet types.
+--+--+--+---------//---------+
| VId |C | Data |
+--+--+--+---------//---------+
Name | Type | Explanation |
---|---|---|
VId | u16 | Voice Packet Id |
C | u8 | Codec Type |
Data | var | Voice Data |
+--+--+--+--+--+---------//---------+
| VId | CId |C | Data |
+--+--+--+--+--+---------//---------+
Name | Type | Explanation |
---|---|---|
VId | u16 | Voice Packet Id |
CId | u16 | Talking Client |
C | u8 | Codec Type |
Data | var | Voice Data |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+---------//---------+
| VId |C |N |M | U* | T* | Data |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+---------//---------+
Name | Type | Explanation |
---|---|---|
VId | u16 | Voice Packet Id |
C | u8 | Codec Type |
N | u8 | Count of ChannelIds to send to |
M | u8 | Count of ClientIds to send to |
U | [u64] | Targeted ChannelIds, repeated N times |
T | [u16] | Targeted ClientIds, repeated M times |
Data | var | Voice Data |
+--+--+--+--+--+---------//---------+
| VId | CId |C | Data |
+--+--+--+--+--+---------//---------+
Name | Type | Explanation |
---|---|---|
VId | u16 | Voice Packet Id |
CId | u16 | Talking Client |
C | u8 | Codec Type |
Data | var | Voice Data |
The TeamSpeak3 Query like command string encoded in UTF-8
Empty.
+--+--+
| PId |
+--+--+
Name | Type | Explanation |
---|---|---|
PId | u16 | The packet id that is acknowledged |
- In case of
Pong
a matching ping packet id is acknowledged. - In case of
Ack
orAckLow
a matching Command or CommandLow packet id respectively is acknowledged.
(see 2.1)-(see 2.5)
Each packet type and packet direction must be maintained by an own packet id counter. This means the client has 9 different packet id counter for outgoing packets.
For each new packet the counter gets increased by 1. This also applies to splitted packets.
The client must also maintain packet ids for incoming packets in case of packets arriving out of order.
All Packet Ids start at 1 unless otherwise specified.
Packet Ids are stored as u16, this mean that they range from 0-65536.
When the packet id overflows from 65535 to 0 at a packet, the generation counter for this packet type gets increased by 1.
Note that the new generation id immediately applies to the 'overflowing' packet.
The generation id counter is solely used for encryption (see 1.6).
In order to reliably send packets over UPD some packet types must get acknowledged when received (see 1.11).
The protocol uses selective repeat for lost packets. This means each packet has its own timeout. Already acknowledged later packets must not be resent. When a packet times out, the exact same packet should be resent until properly acknowledged by the server. If after 30 seconds no resent packet gets acknowledged the connection should be closed. Packet resend timeouts should be calculated with an exponential backoff to prevent network congestion.
Type | Acknowledged (by) | Resend | Encrypted | Splittable | Compressable |
---|---|---|---|---|---|
Voice | ✗ | ✗ | Optional | ✗ | ✗ |
VoiceWhisper | ✗ | ✗ | Optional | ✗ | ✗ |
Command | ✓ (Ack) | ✓ | ✓ | ✓ | ✓ |
CommandLow | ✓ (AckLow) | ✓ | ✓ | ✓ | ✓ |
Ping | ✓ (Pong) | ✗ | ✗ | ✗ | ✗ |
Pong | ✗ | ✗ | ✗ | ✗ | ✗ |
Ack | ✗ | ✓ | ✓ | ✗ | ✗ |
AckLow | ✗ | ✓ | ✓ | ✗ | ✗ |
Init1 | ✓ (next Init1) | ✓ | ✗ | ✗ | ✗ |
A connection is started from the client by sending the first handshake packet. The handshake process consists of 5 different init packets. This includes the so called RSA puzzle to prevent DOS attacks.
The packet header values are set as following for all packets here:
Parameter | Value |
---|---|
MAC | [u8]{ 0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31 } |
key | N/A |
nonce | N/A |
Type | Init1 |
Encrypted | ✗ |
Packet Id | u16: 101 |
Client Id | u16: 0 |
04 bytes : Version of the TeamSpeak client as timestamp
Example: { 0x06, 0x3b, 0xec, 0xe9 }
01 bytes : Init-packet step number
Const: 0x00
04 bytes : Current timestamp in unix format
04 bytes : Random bytes := [A0]
08 bytes : Zeros, reserved.
01 bytes : Init-packet step number
Const: 0x01
16 bytes : Server stuff := [A1]
04 bytes : The bytes from [A0] in reversed order
04 bytes : Version of the TeamSpeak client as timestamp
01 bytes : Init-packet step number
Const: 0x02
16 bytes : The bytes from [A1]
04 bytes : The bytes from [A0] in reversed order
01 bytes : Init-packet step number
Const: 0x03
64 bytes : 'x', an unsigned BigInteger
64 bytes : 'n', an unsigned BigInteger
04 bytes : 'level' a u32
100 bytes : Server stuff := [A2]
04 bytes : Version of the TeamSpeak client as timestamp
01 bytes : Init-packet step number
Const: 0x04
64 bytes : the received 'x'
64 bytes : the received 'n'
04 bytes : the received 'level'
100 bytes : The bytes from [A2]
64 bytes : 'y' which is the result of x ^ (2 ^ level) % n as an unsigned
BigInteger. Padded from the lower side with '0x00' when shorter
than 64 bytes.
Example: { 0x00, 0x00, data ... data}
var bytes : The clientinitiv command data as explained in (see 3.1)
In this phase the client and server exchange basic information and agree on/calculate the symmetric AES encryption key with the ECDH public/private key exchange technique.
Both the client and the server will need a EC public/private key. This key is also the identity which the server uses to recognize a user again. The curve used is 'prime256v1'.
All high level packets specified in this chapter are sent as Command
Type
packets as explained in (see 2.8.3). Additionally the Newprotocol
flag
(see 2.3) must be set on all Command
, CommandLow
and Init1
packets.
The packet header/encryption values for (see 3.1) and (see 3.2) are as following:
Parameter | Value |
---|---|
MAC | (Generated by EAX) |
key | [u8]{0x63, 0x3A, 0x5C, 0x77, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x5C, 0x73, 0x79, 0x73, 0x74, 0x65} |
nonce | [u8]{0x6D, 0x5C, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6C, 0x6C, 0x33, 0x32, 0x2E, 0x63, 0x70, 0x6C} |
Type | Command |
Encrypted | ✓ |
Packet Id | u16: 0 |
Client Id | u16: 0 |
The acknowledgement packets use the same parameters as the commands, except with
the Type Ack
.
(Maybe add a #3.0 Prelude for required cryptographic values, if yes move the omega ASN.1 encoding here)
The first packet is sent (Client -> Server) although this is only sent for
legacy reasons since newer servers (at least 3.0.13.0?) use the data part
embedded in the last Init1
packet from the low-level handshake (see 2.5).
The ip parameter is added but left without value for legacy reasons.
clientinitiv alpha={alpha} omega={omega} ip
-
alpha
is set tobase64(random[u8; 10])
which are 10 random bytes for entropy. -
omega
is set tobase64(publicKey[u8])
omega is an ASN.1-DER encoded public key from the ECDH parameters as following:Type Value Explanation BIT STRING 1bit, Value: 0 LibTomCrypt uses 0 for a public key INTEGER 32 The LibTomCrypt used keysize INTEGER publicKey.x The affine X-Coordinate of the public key INTEGER publicKey.y The affine Y-Coordinate of the public key
The server responds with this command.
initivexpand alpha={alpha} beta={beta} omega={omega}
alpha
must have the same value as sent to the server in the previous step.beta
is set tobase64(random[u8; 10])
by the server.omega
is set tobase64(publicKey[u8])
with the public Key from the server, encoded same as in (see 3.1)
With this information the client now must calculate the shared secret.
let sharedSecret: ECPoint
let x: [u8]
let sharedData: [u8; 32]
let SharedIV: [u8; 20]
let SharedMac: [u8; 8]
let ECDH(A, B) := (A * B).Normalize
sharedSecret = ECDH(serverPublicKey, ownPrivateKey)
x = sharedSecret.x.AsByteArray()
if x.length < 32
sharedData[0-(32-x.length)] = [0..0]
sharedData[(32-x.length)-32] = x[0-x.length]
if x.length == 32
sharedData[0-32] = x[0-32]
if x.length > 32
sharedData[0-32] = x[(x.length-32)-x.length]
SharedIV = SHA1(sharedData)
SharedIV[0-10] = SharedIV[0-10] xor alpha.decode64()
SharedIV[10-20] = SharedIV[10-20] xor beta.decode64()
SharedMac[0-8] = SHA1(SharedIV)[0-8]
Notes:
- Only
SharedIV
andSharedMac
are needed. The other values can be discarded. - The crypto handshake is now completed. The normal encryption scheme (see 1.6) is from now on used.
- All
Command
,CommandLow
,Ack
andAckLow
packets must get encrypted. Voice
packets (andVoiceWhisper
when wanted) should be encrypted when the channel encryption or server wide encryption flag is set.Ping
andPong
must not be encrypted.
clientinit client_nickname client_version client_platform client_input_hardware client_output_hardware client_default_channel client_default_channel_password client_server_password client_meta_data client_version_sign client_key_offset client_nickname_phonetic client_default_token hwid
client_nickname
the desired nicknameclient_version
the client versionclient_platform
the client platformclient_input_hardware
whether a input device is availableclient_output_hardware
whether a output device is availableclient_default_channel
the default channel to join. This can be a channel path or/<id>
(eg/1
) for a channel id.client_default_channel_password
the password for the join channel, prepared the following waybase64(sha1(password))
client_server_password
the password to enter the server, prepared the following waybase64(sha1(password))
client_meta_data
(can be left empty)client_version_sign
a cryptographic sign to verify the genuinity of the clientclient_key_offset
the number offset used to calculate the hashcash (see 4.1) value of the used identityclient_nickname_phonetic
the phonetic nickname for text-to-speechclient_default_token
permission token to be used when connecting to a serverhwid
hardware identification string
Notes:
- Since client signs are only generated and distributed by TeamSpeak systems,
this the recommended client triple, as it is the reference for this paper
- Version:
3.0.19.3 [Build: 1466672534]
- Platform:
Windows
- Sign:
a1OYzvM18mrmfUQBUgxYBxYz2DUU6y5k3/mEL6FurzU0y97Bd1FL7+PRpcHyPkg4R+kKAFZ1nhyzbgkGphDWDg==
- Version:
- The
hwid
is usually around 30 characters long, but strings as short as only few characters like123,456
are accepted - Parameters which are empty or not used must be declared but left without
value and the
=
character
initserver virtualserver_welcomemessage virtualserver_platform virtualserver_version virtualserver_maxclients virtualserver_created virtualserver_hostmessage virtualserver_hostmessage_mode virtualserver_id virtualserver_ip virtualserver_ask_for_privilegekey acn aclid pv lt client_talk_power client_needed_serverquery_view_power virtualserver_name virtualserver_codec_encryption_mode virtualserver_default_server_group virtualserver_default_channel_group virtualserver_hostbanner_url virtualserver_hostbanner_gfx_url virtualserver_hostbanner_gfx_interval virtualserver_priority_speaker_dimm_modificator virtualserver_hostbutton_tooltip virtualserver_hostbutton_url virtualserver_hostbutton_gfx_url virtualserver_name_phonetic virtualserver_icon_id virtualserver_hostbanner_mode virtualserver_channel_temp_delete_delay_default
virtualserver_welcomemessage
the welcome message of the severvirtualserver_platform
the plattform the server is running onvirtualserver_version
the verison of the servervirtualserver_maxclients
the maximum allowed clients on this servervirtualserver_created
the start date of the servervirtualserver_hostmessage
virtualserver_hostmessage_mode
virtualserver_id
virtualserver_ip
virtualserver_ask_for_privilegekey
acn
the accepted client nickname, this might differ from the desired nickname if it's already in useaclid
the assigned client Idpv
???lt
License Type of the serverclient_talk_power
the initial talk powerclient_needed_serverquery_view_power
virtualserver_name
virtualserver_codec_encryption_mode
see CodecEncryptionMode from the official query documentationvirtualserver_default_server_group
virtualserver_default_channel_group
virtualserver_hostbanner_url
virtualserver_hostbanner_gfx_url
virtualserver_hostbanner_gfx_interval
virtualserver_priority_speaker_dimm_modificator
virtualserver_hostbutton_tooltip
virtualserver_hostbutton_url
virtualserver_hostbutton_gfx_url
virtualserver_name_phonetic
virtualserver_icon_id
virtualserver_hostbanner_mode
virtualserver_channel_temp_delete_delay_default
Note:
- From this point on the client knows his client id, therefore it must be set in the header of each packet
The server will now send all needed information to display the entire server properly. Those notifications are in no fixed order, although they are most of the time sent in the here declared order.
See the official query documentation to get further details to this notifications parameter.
channellist cid cpid channel_name channel_topic channel_codec channel_codec_quality channel_maxclients channel_maxfamilyclients channel_order channel_flag_permanent channel_flag_semi_permanent channel_flag_default channel_flag_password channel_codec_latency_factor channel_codec_is_unencrypted channel_delete_delay channel_flag_maxclients_unlimited channel_flag_maxfamilyclients_unlimited channel_flag_maxfamilyclients_inherited channel_needed_talk_power channel_forced_silence channel_name_phonetic channel_icon_id channel_flag_private
cid
Channel idcpid
Channel parent idchannel_name
channel_topic
channel_codec
see the Codec enum from the official query documentationchannel_codec_quality
value between 0-10 representing a bitrate (see XXX)channel_maxclients
channel_maxfamilyclients
channel_order
channel_flag_permanent
channel_flag_semi_permanent
channel_flag_default
channel_flag_password
channel_codec_latency_factor
channel_codec_is_unencrypted
channel_delete_delay
channel_flag_maxclients_unlimited
channel_flag_maxfamilyclients_unlimited
channel_flag_maxfamilyclients_inherited
channel_needed_talk_power
channel_forced_silence
channel_name_phonetic
channel_icon_id
channel_flag_private
After the last channellist
notification the server will send
channellistfinished
Same as the query notification.
To prevent client spamming (connecting to a server with many different clients) the server requires a certain hashcash level on each identity. This level has a exponentially growing calculation time with increasing level. This ensures that a user wanting to spam a certain server needs to invest some time into calculating the required level.
- The publicKey is a string encoded as in (see 3.1) the omega value.
- The key offset is a u64 number, which gets converted to a string when concatenated.
The first step is to calculate a hash as following
let data: [u8; 20] = SHA1(publicKey + keyOffset)
The level can now be calculated by counting the continuous leading zero bits in the data array. The bytes in the array get counted from 0 to 20 and the bits in each byte from least significant to most significant.
To calculate the uid of an identity the public key is required. Therefore you can only calculate the uid of your own identity and the servers identity you are connecting to.
The publicKey is a string encoded as in (see 3.1) the omega value.
The uid can be calculated as following
let uid: string = base64(sha1(publicKey))
The server will regularly send ping packets to check if a client is still alive. The client must answer them with the according pong packet.
The client should also send ping packets to the server to check for connection. They will be answered with according pong packets.
Sending ping packets from the client side should not be started before the crypto handshake has been completed (see 3.3)
- notifyconnectioninforequest
- => setconnectioninfo