<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Protocols on network-notes</title><link>/tags/protocols/</link><description>Recent content in Protocols on network-notes</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>brett@network-notes.com (Brett Lykins)</managingEditor><webMaster>brett@network-notes.com (Brett Lykins)</webMaster><copyright>© 2015-2026 Brett Lykins</copyright><lastBuildDate>Thu, 23 Apr 2026 11:00:00 +0000</lastBuildDate><atom:link href="/tags/protocols/feed.xml" rel="self" type="application/rss+xml"/><item><title>What Actually Happens When You SSH Into a Router</title><link>/posts/2026/ssh-under-the-hood/</link><pubDate>Thu, 23 Apr 2026 11:00:00 +0000</pubDate><author>brett@network-notes.com (Brett Lykins)</author><guid>/posts/2026/ssh-under-the-hood/</guid><description>&lt;p&gt;You type &lt;code&gt;ssh router1&lt;/code&gt;, hit enter, and a prompt appears. Simple.&lt;/p&gt;
&lt;p&gt;Between those two events, your client and the device exchange dozens of packets across four protocol layers, negotiate cryptographic algorithms, perform a key exchange, authenticate you, open a channel, and (if you&amp;rsquo;re using a tool like Netmiko) request a pseudo-terminal, detect the prompt, disable pagination, and set the terminal width. All before a single &lt;code&gt;show&lt;/code&gt; command runs.&lt;/p&gt;
&lt;p&gt;Most network engineers use SSH every day without thinking about any of this. That&amp;rsquo;s fine for interactive use. But if you&amp;rsquo;re building automation that opens hundreds or thousands of SSH sessions, every one of those steps costs at least one network round trip (sometimes more), and round trip times are usually the single biggest factor in how fast your automation runs.&lt;/p&gt;
&lt;h2 id="ssh-is-four-protocols-not-one"&gt;SSH Is Four Protocols, Not One&lt;/h2&gt;
&lt;p&gt;SSH isn&amp;rsquo;t a single protocol. It&amp;rsquo;s a stack of four, defined across separate RFCs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;RFC&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc4251"&gt;RFC 4251&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Defines the overall design, terminology, and trust model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc4253"&gt;RFC 4253&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Key exchange, encryption, integrity, server authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc4252"&gt;RFC 4252&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;User authentication (password, publickey, keyboard-interactive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc4254"&gt;RFC 4254&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Channels, multiplexing, port forwarding, sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each layer runs on top of the previous one. The transport layer gives you an encrypted pipe. The authentication layer proves who you are. The connection layer multiplexes that pipe into channels, and channels are where the actual work happens.&lt;/p&gt;
&lt;p&gt;The application (your shell session, your &lt;code&gt;show version&lt;/code&gt; command, your NETCONF RPC) rides on top of a channel.&lt;/p&gt;
&lt;p&gt;&lt;img src="../../img/2026/ssh-protocol-stack.svg" alt="SSH protocol stack showing four layers from TCP up through Application"&gt;&lt;/p&gt;
&lt;h2 id="tcp"&gt;TCP&lt;/h2&gt;
&lt;p&gt;SSH runs over TCP; so before any SSH packets flow, you need a TCP connection:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; SYN&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; SYN-ACK&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; ACK&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1 round trip.&lt;/strong&gt; The ACK can piggyback on the first SSH data, but the connection isn&amp;rsquo;t usable until the handshake completes.&lt;/p&gt;
&lt;p&gt;This is the same cost every TCP-based protocol pays. Nothing SSH-specific yet.&lt;/p&gt;
&lt;h2 id="ssh-transport"&gt;SSH Transport&lt;/h2&gt;
&lt;h3 id="protocol-version-exchange"&gt;Protocol Version Exchange&lt;/h3&gt;
&lt;p&gt;Immediately after TCP connects, both sides send an identification string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SSH-2.0-OpenSSH_9.6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SSH-2.0-Cisco-1.25
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Both sides send simultaneously, so this can overlap with the TCP ACK. In practice, the client usually sends first and waits for the server&amp;rsquo;s response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1 round trip&lt;/strong&gt; for the version exchange. Implementations vary on whether KEXINIT is pipelined with the version string or sent after receiving the server&amp;rsquo;s version.&lt;/p&gt;
&lt;h3 id="algorithm-negotiation-kexinit"&gt;Algorithm Negotiation (KEXINIT)&lt;/h3&gt;
&lt;p&gt;Both sides send &lt;code&gt;SSH_MSG_KEXINIT&lt;/code&gt;, a packet listing every algorithm they support, in preference order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Key exchange methods (e.g., &lt;code&gt;curve25519-sha256&lt;/code&gt;, &lt;code&gt;diffie-hellman-group14-sha256&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Host key algorithms (e.g., &lt;code&gt;ssh-ed25519&lt;/code&gt;, &lt;code&gt;rsa-sha2-512&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Encryption algorithms (e.g., &lt;code&gt;aes256-gcm@openssh.com&lt;/code&gt;, &lt;code&gt;chacha20-poly1305@openssh.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;MAC algorithms (e.g., &lt;code&gt;hmac-sha2-256-etm@openssh.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Compression algorithms (usually &lt;code&gt;none&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RFC 4253 says &amp;ldquo;key exchange will begin immediately after sending [the version] identifier,&amp;rdquo; meaning each side can fire off its KEXINIT right after its version string without waiting. If both sides do this, the KEXINIT packets arrive during the same round trip as the version exchange, and the cost is zero additional round trips. If the client waits for the server&amp;rsquo;s version string before sending KEXINIT (which some implementations do), it costs a separate round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost: 0-1 round trips&lt;/strong&gt; depending on whether the implementation pipelines KEXINIT with the version string.&lt;/p&gt;
&lt;h3 id="key-exchange"&gt;Key Exchange&lt;/h3&gt;
&lt;p&gt;The exact packet sequence depends on the algorithm:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Modern (curve25519-sha256, ecdh-sha2-nistp256):&lt;/strong&gt; 1 round trip.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_ECDH_INIT&lt;/code&gt; (client&amp;rsquo;s ephemeral public key)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_ECDH_REPLY&lt;/code&gt; (server&amp;rsquo;s ephemeral public key + host key + signature)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both sides now have a shared secret. The server&amp;rsquo;s signature proves it holds the private key matching the host key the client expects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Legacy (diffie-hellman-group-exchange-sha256):&lt;/strong&gt; 2 round trips.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_DH_GEX_REQUEST&lt;/code&gt; (preferred group size)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_DH_GEX_GROUP&lt;/code&gt; (prime and generator)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_DH_GEX_INIT&lt;/code&gt; (client&amp;rsquo;s public value)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_KEX_DH_GEX_REPLY&lt;/code&gt; (server&amp;rsquo;s public value + signature)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The extra round trip exists because the client asks the server to choose a DH group, rather than using a fixed one. Older network devices that don&amp;rsquo;t support curve25519 or ECDH may force this path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1-2 round trips&lt;/strong&gt; depending on the algorithm.&lt;/p&gt;
&lt;p&gt;After key exchange completes, both sides send &lt;code&gt;SSH_MSG_NEWKEYS&lt;/code&gt; simultaneously to activate the new keys (no additional round trip). The client then requests the &lt;code&gt;ssh-userauth&lt;/code&gt; service via &lt;code&gt;SSH_MSG_SERVICE_REQUEST&lt;/code&gt; / &lt;code&gt;SSH_MSG_SERVICE_ACCEPT&lt;/code&gt;, costing 1 round trip. Some implementations (like OpenSSH) pipeline this with NEWKEYS, but many wait.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transport layer total: 3-5 round trips&lt;/strong&gt; (TCP + version exchange + KEXINIT + kex + service request).&lt;/p&gt;
&lt;h2 id="ssh-authentication"&gt;SSH Authentication&lt;/h2&gt;
&lt;p&gt;The client now needs to authenticate. This is where things get variable, because the server controls which methods it accepts and in what order.&lt;/p&gt;
&lt;h3 id="the-auth-dance"&gt;The Auth Dance&lt;/h3&gt;
&lt;p&gt;The client sends an authentication request. If it fails or the server wants to advertise available methods, the server responds with &lt;code&gt;SSH_MSG_USERAUTH_FAILURE&lt;/code&gt; plus a list of methods that can continue.&lt;/p&gt;
&lt;p&gt;A typical sequence for password auth:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_USERAUTH_REQUEST&lt;/code&gt; (method: &amp;ldquo;none&amp;rdquo;) to discover available methods&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_USERAUTH_FAILURE&lt;/code&gt; (methods: &amp;ldquo;publickey,password&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_USERAUTH_REQUEST&lt;/code&gt; (method: &amp;ldquo;password&amp;rdquo;, password: &amp;ldquo;&amp;hellip;&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_USERAUTH_SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 2 round trips&lt;/strong&gt; for password auth with a method probe.&lt;/p&gt;
&lt;p&gt;For public key auth, it&amp;rsquo;s often 2-3 round trips:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Probe (1 RT)&lt;/li&gt;
&lt;li&gt;Public key query: &amp;ldquo;will you accept this key?&amp;rdquo; (1 RT)&lt;/li&gt;
&lt;li&gt;Actual signature-based auth (1 RT)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Some servers skip the probe if the client guesses correctly on the first try. Some clients try multiple key types before finding one the server accepts. Each failed attempt is another round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentication total: 1-3 round trips&lt;/strong&gt; depending on method and implementation.&lt;/p&gt;
&lt;h2 id="ssh-connection"&gt;SSH Connection&lt;/h2&gt;
&lt;p&gt;With an authenticated, encrypted connection established, the client can now open channels. For automation, this layer matters most, because there are three fundamentally different ways to interact with a device.&lt;/p&gt;
&lt;h3 id="channel-basics"&gt;Channel Basics&lt;/h3&gt;
&lt;p&gt;Every channel follows the same lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_OPEN&lt;/code&gt; (channel type, initial window size)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_OPEN_CONFIRMATION&lt;/code&gt; (window size, max packet size)&lt;/li&gt;
&lt;li&gt;&amp;hellip; data flows &amp;hellip;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Either side:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_CLOSE&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1 round trip&lt;/strong&gt; to open a channel.&lt;/p&gt;
&lt;p&gt;SSH channels have their own flow control, independent of TCP, with each side advertising a receive window size.&lt;/p&gt;
&lt;h3 id="channel-type-1-shell-pty"&gt;Channel Type 1: Shell (PTY)&lt;/h3&gt;
&lt;p&gt;This is what happens when you type &lt;code&gt;ssh router1&lt;/code&gt; and get an interactive prompt. It&amp;rsquo;s also what &lt;strong&gt;Netmiko&lt;/strong&gt;, &lt;strong&gt;Ansible &lt;code&gt;network_cli&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;Scrapli&lt;/strong&gt; use for automation.&lt;/p&gt;
&lt;p&gt;After opening a session channel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_REQUEST&lt;/code&gt; (type: &amp;ldquo;pty-req&amp;rdquo;, terminal type, dimensions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_REQUEST&lt;/code&gt; (type: &amp;ldquo;shell&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 2 round trips&lt;/strong&gt; (PTY request + shell request).&lt;/p&gt;
&lt;p&gt;Now you have a byte stream. The server sends a prompt (&lt;code&gt;router1#&lt;/code&gt;). You send characters. The server echoes them back. You send a newline. The server processes the command and sends the output, followed by another prompt.&lt;/p&gt;
&lt;p&gt;The catch: &lt;strong&gt;there is no framing&lt;/strong&gt;. The server doesn&amp;rsquo;t tell you &amp;ldquo;the command output is 847 bytes long&amp;rdquo; or &amp;ldquo;I&amp;rsquo;m done sending.&amp;rdquo; It just sends bytes. Your automation tool has to figure out when the output is complete by pattern-matching the prompt, which is why Netmiko spends so much effort on prompt detection, and why it sends &lt;code&gt;terminal length 0&lt;/code&gt; (to disable pagination) and &lt;code&gt;terminal width 511&lt;/code&gt; (to prevent line wrapping) before running any real commands.&lt;/p&gt;
&lt;p&gt;Each of those setup commands is another write-read-prompt cycle on the channel.&lt;/p&gt;
&lt;h3 id="channel-type-2-exec"&gt;Channel Type 2: Exec&lt;/h3&gt;
&lt;p&gt;This is what happens when you run &lt;code&gt;ssh router1 &amp;quot;show version&amp;quot;&lt;/code&gt; from the command line. It&amp;rsquo;s also what Go&amp;rsquo;s &lt;code&gt;x/crypto/ssh&lt;/code&gt;, Paramiko&amp;rsquo;s &lt;code&gt;exec_command()&lt;/code&gt;, and some other programmatic SSH libraries in various languages use.&lt;/p&gt;
&lt;p&gt;After opening a session channel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_REQUEST&lt;/code&gt; (type: &amp;ldquo;exec&amp;rdquo;, command: &amp;ldquo;show version&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_DATA&lt;/code&gt; (command output)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_REQUEST&lt;/code&gt; (type: &amp;ldquo;exit-status&amp;rdquo;, code: 0)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_CLOSE&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1 round trip&lt;/strong&gt; for the exec request, then the output streams back.&lt;/p&gt;
&lt;p&gt;Exec mode is cleaner than shell mode: one command, one channel, structured exit. No prompt detection, no pagination, no terminal width issues. But there are some catches.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;each command needs its own channel&lt;/strong&gt;. If you want to run 5 commands, you open 5 channels, meaning 5 channel-open round trips plus 5 exec-request round trips.&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;many network devices don&amp;rsquo;t support exec mode properly (or at all)&lt;/strong&gt;, which is why most automation tools default to shell/PTY mode.&lt;/p&gt;
&lt;h3 id="channel-type-3-subsystem-netconf"&gt;Channel Type 3: Subsystem (NETCONF)&lt;/h3&gt;
&lt;p&gt;NETCONF (&lt;a href="https://datatracker.ietf.org/doc/html/rfc6241"&gt;RFC 6241&lt;/a&gt;) runs over SSH as a subsystem, a named service that the server knows how to handle.&lt;/p&gt;
&lt;p&gt;After opening a session channel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_REQUEST&lt;/code&gt; (type: &amp;ldquo;subsystem&amp;rdquo;, name: &amp;ldquo;netconf&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;SSH_MSG_CHANNEL_SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cost: 1 round trip.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then NETCONF has its own handshake on top of the SSH channel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Both sides simultaneously:&lt;/strong&gt; &lt;code&gt;&amp;lt;hello&amp;gt;&lt;/code&gt; messages listing capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt; &lt;code&gt;&amp;lt;rpc&amp;gt;&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt; &lt;code&gt;&amp;lt;rpc-reply&amp;gt;&lt;/code&gt; response&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;hello&amp;gt;&lt;/code&gt; exchange is 1 round trip. Each RPC is 1 round trip. Messages are delimited by &lt;code&gt;]]&amp;gt;]]&amp;gt;&lt;/code&gt; (the legacy base:1.0 framing) or chunked framing with &lt;code&gt;\n#length\n&lt;/code&gt; headers (base:1.1), both defined in &lt;a href="https://datatracker.ietf.org/doc/html/rfc6242"&gt;RFC 6242&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;NETCONF gives you structured XML data and transactional semantics (&lt;code&gt;&amp;lt;edit-config&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;commit&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;validate&amp;gt;&lt;/code&gt;), which beats screen-scraping CLI output by a wide margin. But it pays the full SSH transport cost to get there, plus its own capability exchange overhead.&lt;/p&gt;
&lt;p&gt;&lt;img src="../../img/2026/ssh-channel-types.svg" alt="SSH channel types compared: Shell/PTY, Exec, and Subsystem/NETCONF"&gt;&lt;/p&gt;
&lt;h2 id="the-round-trip-tally"&gt;The Round-Trip Tally&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what it all adds up to, from TCP SYN to first byte of command output:&lt;/p&gt;
&lt;p&gt;&lt;img src="../../img/2026/ssh-connection-lifecycle.svg" alt="SSH connection lifecycle showing round trips from TCP handshake through command execution"&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Round trips&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TCP handshake&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Same for any TCP protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH version exchange + KEXINIT&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;td&gt;Pipelined: 1 RT; sequential: 2 RT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key exchange (modern)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;curve25519/ECDH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key exchange (legacy)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;DH group exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEWKEYS + service request&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Often pipelined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;1-3&lt;/td&gt;
&lt;td&gt;Password: 2, pubkey: 2-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel open&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PTY + shell request&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Shell/PTY mode only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec request&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Exec mode only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subsystem request&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NETCONF only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NETCONF hello exchange&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NETCONF only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;SSH CLI (exec mode):&lt;/strong&gt; 6-10 round trips before the first command output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSH CLI (PTY/shell mode):&lt;/strong&gt; 7-11 round trips before the first command output, plus 2-3 more for session prep (&lt;code&gt;terminal length 0&lt;/code&gt;, &lt;code&gt;terminal width 511&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NETCONF over SSH:&lt;/strong&gt; 7-11 round trips before the first RPC response, plus 1 for the NETCONF hello exchange.&lt;/p&gt;
&lt;p&gt;For comparison:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTPS (TLS 1.3):&lt;/strong&gt; 3 round trips total: TCP (1) + TLS 1.3 handshake (1) + HTTP request (1). With connection reuse: 1 round trip per request.&lt;/p&gt;
&lt;h2 id="netconf-vs-restconf-same-data-different-pipe"&gt;NETCONF vs RESTCONF: Same Data, Different Pipe&lt;/h2&gt;
&lt;p&gt;So far this post has been about what happens inside an SSH connection. But SSH isn&amp;rsquo;t the only way to talk to a network device programmatically. NETCONF and RESTCONF both operate on YANG data models, and they can configure the same things on the same devices. The difference is the transport.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;NETCONF&lt;/th&gt;
&lt;th&gt;RESTCONF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;SSH (subsystem channel)&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data format&lt;/td&gt;
&lt;td&gt;XML&lt;/td&gt;
&lt;td&gt;JSON or XML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection setup&lt;/td&gt;
&lt;td&gt;7-11 RT (SSH)&lt;/td&gt;
&lt;td&gt;3 RT (TLS 1.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-operation cost&lt;/td&gt;
&lt;td&gt;1 RT (RPC request/reply)&lt;/td&gt;
&lt;td&gt;1 RT (HTTP request/response)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection reuse&lt;/td&gt;
&lt;td&gt;SSH connection persists&lt;/td&gt;
&lt;td&gt;HTTP keep-alive (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;]]&amp;gt;]]&amp;gt;&lt;/code&gt; or chunked&lt;/td&gt;
&lt;td&gt;HTTP Content-Length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capabilities&lt;/td&gt;
&lt;td&gt;Explicit hello exchange&lt;/td&gt;
&lt;td&gt;YANG library (&lt;a href="https://datatracker.ietf.org/doc/html/rfc7895"&gt;RFC 7895&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;commit&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;validate&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;discard-changes&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No candidate datastore; edits apply immediately to running config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;NETCONF&amp;rsquo;s advantage is its transactional model: candidate configs, commit/rollback, validation. RESTCONF trades some of that for a simpler transport with lower connection overhead and native compatibility with standard HTTP client libraries and load balancers.&lt;/p&gt;
&lt;p&gt;For automation at scale, the transport cost matters. If you&amp;rsquo;re making hundreds of NETCONF calls across a WAN, you&amp;rsquo;re paying the SSH handshake tax on every new connection. RESTCONF over HTTPS with connection reuse eliminates that entirely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;m planning to benchmark both SSH CLI vs HTTPS CLI and NETCONF vs RESTCONF with real measurements: same device, same operations, same latency profiles. Theory is useful, but packet captures don&amp;rsquo;t lie.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-this-matters-for-automation"&gt;Why This Matters for Automation&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re managing 1,000 devices across a WAN with 75ms latency, the math gets ugly. Each SSH connection pays 450-750ms in handshake overhead before it does anything useful. Parallelism helps with wall clock time, but it doesn&amp;rsquo;t reduce the per-device tax, and every one of those connections is burning round trips, file descriptors, and CPU on your automation host.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be measuring this gap with real numbers in an upcoming series on CLI over HTTPS. Early testing shows that at 30ms RTT, HTTPS batch mode is roughly 5x faster than SSH for the same CLI commands, because it pays 3 round trips instead of 7-11.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My take:&lt;/strong&gt; SSH is a good protocol for what it was built for: giving one person a secure shell on one machine. Every layer in the stack exists for a reason. But those layers add up to a lot of round trips, and that cost compounds when you&amp;rsquo;re hitting a thousand devices over a WAN. The protocol wasn&amp;rsquo;t designed for &amp;ldquo;blast 5 commands at 1,000 boxes as fast as possible.&amp;rdquo; Once you see the round-trip tax on the wire, the performance gap between SSH and HTTPS stops being surprising.&lt;/p&gt;
&lt;/blockquote&gt;</description></item></channel></rss>