SSH Packets

This part of the implementation will be following RFC 4253, which documents the transport layer of the SSH protocol and the steps required to use it.

The previous post outlined the logical layers in an active SSH session. These layers are activated only after negotiating their parameters (windowing sizes, channel IDs, ciphers, hashes, and so on). Initially, the stream consists of simple ASCII text delimited by newlines and spaces.

			⌨620 c ⌨128 a ⌨183 r ⌨148 g ⌨75 o ⌨105   ⌨121 r ⌨49 u ⌨131 n ⌨75   ⌨82 b ⌨76 e ⌨77 l ⌨129 a ⌨204 f ⌨156 o ⌨181 n ⌨287 t ⌨103 e ⌨232 : ⌨256 2 ⌨121 2 ⌨164 ⏎
			␤ ⎙54   ⎙0    Finished dev [unoptimized + debuginfo] target(s) in 0.01s⏎
			␤ ⎙1   ⎙0     Running `target\debug\main.exe 'belafonte:22'`⏎
			␤⏎
			␤ ⎙73 ▲ SSH-2.0-WhyHelloThere Its_SSH_Time!⏎
			␤ ⎙32 ▼ SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3⏎
			␤ ␃
		

Live data! How exciting. Now that the server and I have been properly introduced, the first layer — the transport layer — springs into action. All subsequent transmissions are framed in packets that add size fields, randomization, and padding.

			⌨574 c ⌨80 a ⌨100 r ⌨122 g ⌨101 o ⌨92   ⌨176 r ⌨97 u ⌨137 n ⌨104   ⌨235 b ⌨151 e ⌨59 l ⌨94 a ⌨170 f ⌨299 o ⌨179 n ⌨125 t ⌨105 e ⌨223 : ⌨200 2 ⌨121 2 ⌨172 ⏎
			␤ ⎙53   ⎙0    Finished dev [unoptimized + debuginfo] target(s) in 0.01s⏎
			␤ ⎙1   ⎙0     Running `target\debug\main.exe 'belafonte:22'`⏎
			␤⏎
			␤ ⎙118 ▲ SSH-2.0-WhyHelloThere Its_SSH_Time!⏎
			␤ ⎙56 ▼ SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3⏎
			␤ ⎙92 ▼ Packet of 1081 bytes⏎
			␤ ⎙0          Content: 1069 bytes⏎
			␤         Padding: 6 bytes⏎
			␤ ␃
		

There was a brief moment of temptation to make the transport stream implement Rust's std::io::Read/Write traits, but they are clearly designed for streams rather than packets. Adhering to these interfaces would require callers to know a magic convention (e.g. ignore the standard library documentation and only call Write with exactly one packet of data, making sure to never chain this writer with anything that assumes it behaves as the documentation describes).

Although the Read/Write traits aren't used for reading/writing packets themselves, the traits are used for the packet payloads. As mentioned earlier, I do not want to buffer entire packets at each layer, so layers will operate on streams of data wherever possible.

pub fn write_packet<S>(&mut selfcontent_serializer: &S) -> Result<()>
where
    SSerialize<T>,
{
    // ...
}

The packet writer takes care of the headers and padding around the payload, deferring the actual payload writing to something that can Serialize, as shown below. During the handshake, only simple data structures will be Serialized, but later stages of the connection will have several nested layers (multiplexing, windowing, etc.) behind the Serialize trait. A Serializeable type must also be able to compute its length ahead of time, as the packet's length field precedes the content (and I don't want to buffer the entire payload just to discover its length).

pub trait Serialize<T>
where
    TWrite,
{
    fn serialize(&selfwriter: &mut T) -> Result<()>;
    fn length(&self) -> u32;
}

It feels nice to have generics again after a few years of Go. With packets being received and unwrapped, it's time to interpret the payload.

			⌨569 c ⌨80 a ⌨115 r ⌨123 g ⌨132 o ⌨93   ⌨218 r ⌨73 u ⌨102 n ⌨85   ⌨101 b ⌨214 e ⌨148 l ⌨137 a ⌨163 f ⌨70 o ⌨151 n ⌨46 t ⌨105 e ⌨214 : ⌨298 2 ⌨129 2 ⌨231 ⏎
			␤ ⎙56   ⎙0    Finished dev [unoptimized + debuginfo] target(s) in 0.01s⏎
			␤ ⎙1   ⎙0     Running `target\debug\main.exe 'belafonte:22'`⏎
			␤ ⎙121 ⏎
			␤ ⎙0 ▲ SSH-2.0-WhyHelloThere Its_SSH_Time!⏎
			␤ ⎙37 ▼ SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3⏎
			␤ ⎙72 ▼ Packet of 1081 bytes⏎
			␤ ⎙0          Content: 1069 bytes⏎
			␤ ⎙0            Cookie: [57, 114, 58, 223, 42, 192, 117, 82, 50, 254, 38,⏎
			␤                    154, 137, 220, 221, 219]⏎
			␤ ⎙0            Key Exchange: ["curve25519-sha256",⏎
			␤                          "[email protected]",⏎
			␤                          "ecdh-sha2-nistp256",⏎
			␤                          "ecdh-sha2-nistp384",⏎
			␤                          "ecdh-sha2-nistp521",⏎
			␤                          "diffie-hellman-group-exchange-sha256",⏎
			␤                          "diffie-hellman-group16-sha512",⏎
			␤                          "diffie-hellman-group18-sha512",⏎
			␤                          "diffie-hellman-group14-sha256",⏎
			␤                          "diffie-hellman-group14-sha1"]⏎
			␤           Host Key: ["ssh-rsa", "rsa-sha2-512", "rsa-sha2-256",⏎
			␤                      "ecdsa-sha2-nistp256", "ssh-ed25519"]⏎
			␤           Encryption: ["[email protected]",⏎
			␤                        "aes128-ctr",⏎
			␤                        "aes192-ctr",⏎
			␤                        "aes256-ctr",⏎
			␤                        "[email protected]",⏎
			␤                        "[email protected]"]⏎
			␤           MAC: ["[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "[email protected]",⏎
			␤                 "hmac-sha2-256",⏎
			␤                 "hmac-sha2-512",⏎
			␤                 "hmac-sha1"]⏎
			␤           Compression: ["none", "[email protected]"]⏎
			␤           Language: [""]⏎
			␤         Padding: 6 bytes⏎
			␤ ␃
		

The following layers have come into use so far, transporting connection-negotiation structures wrapped in packets:

Integrity
Secrecy
Transport
Multiplexing
Windowing
Payload

With the list of capabilities in hand, the next step is to select from the set of available algorithms and negotiate some keys for their use. The next post will bring the Integrity and Secrecy layers to life.

< Find more posts at mattblagden.com