-
Notifications
You must be signed in to change notification settings - Fork 19
008: Reading a Packet
We will start this section by creating a new class Packet. For better organization, I recommend creating a folder called Packets to hold this, and in C#, all of the packets will be in the Packets sub-namespace. The Packet class should be abstract, as we'll never create a simple packet and only inherited classes will have instances.
Also, we will want to add a helper class called ByteReader at the root of the project. This will help us read the bytes from the byte array. It is mostly a nice wrapper around a MemoryStream to deal with reading in network byte order:
public class ByteReader : IDisposable { private readonly char[] ListSeparator = new char[] { ',' }; private MemoryStream m_Stream; public bool IsEOF { get { if (disposedValue) throw new ObjectDisposedException("ByteReader"); return m_Stream.Position == m_Stream.Length; } } public ByteReader(byte[] data) { m_Stream = new MemoryStream(data); } public byte[] GetBytes(int length) { if (disposedValue) throw new ObjectDisposedException("ByteReader"); byte[] data = new byte[length]; m_Stream.Read(data, 0, length); return data; } public byte[] GetMPInt() { uint size = GetUInt32(); if (size == 0) return new byte[1]; byte[] data = GetBytes((int)size); if (data[0] == 0) return data.Skip(1).ToArray(); return data; } public uint GetUInt32() { byte[] data = GetBytes(4); if (BitConverter.IsLittleEndian) data = data.Reverse().ToArray(); return BitConverter.ToUInt32(data, 0); } public string GetString() { return GetString(Encoding.ASCII); } public string GetString(Encoding encoding) { int length = (int)GetUInt32(); if (length == 0) return string.Empty; return encoding.GetString(GetBytes(length)); } public List<string> GetNameList() { List<string> data = new List<string>(); return new List<string>(GetString().Split(ListSeparator, StringSplitOptions.RemoveEmptyEntries)); } public bool GetBoolean() { return (GetByte() != 0); } public byte GetByte() { if (disposedValue) throw new ObjectDisposedException("ByteReader"); return (byte)m_Stream.ReadByte(); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { m_Stream.Dispose(); m_Stream = null; } disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } #endregion }
Then we can add a static method to the Packet class for reading a packet:
public abstract class Packet { // https://tools.ietf.org/html/rfc4253#section-6.1 public const int MaxPacketSize = 35000; private static int s_PacketHeaderSize = 5; public static Packet ReadPacket(Socket socket) { if (socket == null) return null; // TODO: Get the block size based on the ClientToServer cipher uint blockSize = 8; // We must have at least 1 block to read if (socket.Available < blockSize) return null; // Packet not here byte[] firstBlock = new byte[blockSize]; int bytesRead = socket.Receive(firstBlock); if (bytesRead != blockSize) throw new Exception("Failed to read from socket."); // TODO: Decrypt the block using the ClientToServer cipher uint packetLength = 0; byte paddingLength = 0; using (ByteReader reader = new ByteReader(firstBlock)) { // uint32 packet_length // packet_length // The length of the packet in bytes, not including 'mac' or the // 'packet_length' field itself. packetLength = reader.GetUInt32(); if (packetLength > MaxPacketSize) throw new Exception($"Client tried to send a packet bigger than MaxPacketSize ({MaxPacketSize} bytes): {packetLength} bytes"); // byte padding_length // padding_length // Length of 'random padding' (bytes). paddingLength = reader.GetByte(); } // byte[n1] payload; n1 = packet_length - padding_length - 1 // payload // The useful contents of the packet. If compression has been // negotiated, this field is compressed. Initially, compression // MUST be "none". uint bytesToRead = packetLength - blockSize + 4; byte[] restOfPacket = new byte[bytesToRead]; bytesRead = socket.Receive(restOfPacket); if (bytesRead != bytesToRead) throw new Exception("Failed to read from socket."); // TODO: Decrypt the blocks using the ClientToServer cipher uint payloadLength = packetLength - paddingLength - 1; byte[] fullPacket = firstBlock.Concat(restOfPacket).ToArray(); // TODO: Track total bytes read byte[] payload = fullPacket.Skip(s_PacketHeaderSize).Take((int)(packetLength - paddingLength - 1)).ToArray(); // byte[n2] random padding; n2 = padding_length // random padding // Arbitrary-length padding, such that the total length of // (packet_length || padding_length || payload || random padding) // is a multiple of the cipher block size or 8, whichever is // larger. There MUST be at least four bytes of padding. The // padding SHOULD consist of random bytes. The maximum amount of // padding is 255 bytes. // byte[m] mac (Message Authentication Code - MAC); m = mac_length // mac // Message Authentication Code. If message authentication has // been negotiated, this field contains the MAC bytes. Initially, // the MAC algorithm MUST be "none". // TODO: Keep track of the received packet sequence (used for MAC) // TODO: Read MAC if present // TODO: Decompress the payload if necessary using (ByteReader packetReader = new ByteReader(payload)) { // TODO: Create a packet object and return it } return null; } }
And now, all we need to do is update our Client's Poll() method to process packets:
if (m_HasCompletedProtocolVersionExchange) { try { Packet packet = Packet.ReadPacket(m_Socket); while (packet != null) { // TODO: Handle specific packets packet = Packet.ReadPacket(m_Socket); } } catch (Exception ex) { m_Logger.LogError(ex.Message); Disconnect(); return; } }
The code can be viewed with the tag Reading a Packet.
Running the server at this point gives us the same output, but if you attach a debugger and step through the code, you can see that it is reading the first packet of data! All we need to do next, is look at the packet type, and being processing! So, what is the packet? This is the first part of the Key Exchange Method. Which we will cover in the next section: Key Exchange
If you'd like to give me a tip, donate at:
- Bitcoin (BTC): 1NdnffxFC7G7qMrvUYc1x4R5sqXuJhVFR7
- Etherium (ETH): 0xcF0a3f130ba0f8c4CC3A02F782805A448D45388f
- Litecoin (LTC): LV7JL8yA4fAZ3Lib9VoX1tuFPmPVrfFueT