<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Cli-Over-Https on network-notes</title><link>https://network-notes.com/topics/cli-over-https/</link><description>Recent content in Cli-Over-Https 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, 30 Apr 2026 09:00:00 +0000</lastBuildDate><atom:link href="https://network-notes.com/topics/cli-over-https/feed.xml" rel="self" type="application/rss+xml"/><item><title>CLI Over HTTPS Part 2: Proving It</title><link>https://network-notes.com/posts/2026/cli-over-https-2/</link><pubDate>Thu, 30 Apr 2026 09:00:00 +0000</pubDate><author>brett@network-notes.com (Brett Lykins)</author><dc:creator>Brett Lykins</dc:creator><guid>https://network-notes.com/posts/2026/cli-over-https-2/</guid><description>&lt;p&gt;In &lt;a href="https://network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt;, I argued that SSH is a slow transport for network automation at scale and that HTTPS is fundamentally faster. Round-trip analysis and back-of-napkin math are useful, but they&amp;rsquo;re not proof. This post is the proof.&lt;/p&gt;
&lt;p&gt;I built &lt;a href="https://github.com/lykinsbd/clibench"&gt;clibench&lt;/a&gt;, a dual-protocol network device emulator and benchmark client that measures the difference at realistic latencies sourced from &lt;a href="https://www.verizon.com/business/terms/latency/"&gt;Verizon&amp;rsquo;s published backbone measurements&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="design-constraints"&gt;Design Constraints&lt;/h2&gt;
&lt;p&gt;For the comparison to mean anything, the test has to be fair. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Same device, same commands, same output.&lt;/strong&gt; Both transports hit the same &lt;code&gt;device.Device&lt;/code&gt; struct. The only variable is how the command arrives and how the response leaves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Same latency for both.&lt;/strong&gt; Delay is injected at the TCP connection level, not the application level. Both SSH and HTTPS experience identical network conditions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realistic latency values.&lt;/strong&gt; No made-up numbers. Every profile is sourced from published measurements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple modes per transport.&lt;/strong&gt; SSH gets tested with fresh connections and with connection reuse (ControlMaster-style). HTTPS gets tested with fresh connections, keep-alive, multi-command batching, and config push. Each mode represents a real-world usage pattern.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;clibench is written in Go. The project has nine packages:&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&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;internal/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; bench/ Benchmark runner: SSH, HTTPS, proxy, and PTY modes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; device/ Shared command engine: prefix matching, transcript loading
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sshserver/ crypto/ssh listener, CiSSHGo patterns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; httpserver/ net/http + TLS, ASA-style /admin/exec/ and /admin/config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; proxy/ HTTPS→SSH edge proxy (fresh + pooled modes)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; netem/ tc netem latency injection (Linux, requires sudo)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; latency/ Userspace delay injection (fallback, no root)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; stats/ Benchmark statistics: percentile, summarize, parallel runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tlsutil/ Shared self-signed TLS config generator
&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;&lt;img src="https://network-notes.com/img/2026/cli-over-https-bench-architecture.5fda7576f27eb2b1430a4a744666530cfc1e3a520c633554f8ab7a4da0730954.svg" alt="Benchmark architecture: client on the left, latency injection in the middle, SSH and HTTPS servers both feeding into a shared device.Device on the right" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The benchmark client embeds its own server. No separate process needed. Latency is injected at the kernel level using Linux &lt;code&gt;tc netem&lt;/code&gt;, applied per-port on the loopback interface so both SSH and HTTPS experience identical network conditions.&lt;/p&gt;
&lt;h3 id="the-shared-command-engine"&gt;The Shared Command Engine&lt;/h3&gt;
&lt;p&gt;Both servers use the same &lt;code&gt;device.Device&lt;/code&gt;:&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&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-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// &amp;#34;show version&amp;#34; -&amp;gt; transcript text&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;transcriptDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// exact match first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// prefix match (&amp;#34;sh ver&amp;#34; -&amp;gt; &amp;#34;show version&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// returns &amp;#34;% Ambiguous command&amp;#34; or &amp;#34;% Unknown command&amp;#34; on miss&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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;Command transcripts are plain text files loaded from a directory. The filename convention maps to the command: &lt;code&gt;show_version.txt&lt;/code&gt; becomes &lt;code&gt;show version&lt;/code&gt;. Templates support &lt;code&gt;{{.Hostname}}&lt;/code&gt; substitution. This follows the same pattern as &lt;a href="https://network-notes.com/posts/2026/cisshgo-ssh-device-emulator/"&gt;CiSSHGo&lt;/a&gt;, which I wrote about recently.&lt;/p&gt;
&lt;h3 id="the-ssh-server"&gt;The SSH Server&lt;/h3&gt;
&lt;p&gt;The SSH side uses Go&amp;rsquo;s &lt;code&gt;crypto/ssh&lt;/code&gt; package with an ed25519 host key generated at startup. It supports both exec mode (&lt;code&gt;ssh host &amp;quot;show version&amp;quot;&lt;/code&gt;) and interactive shell sessions with prompt rendering and command matching. The benchmark client tests both, since real-world tools are split: libraries like Go&amp;rsquo;s &lt;code&gt;x/crypto/ssh&lt;/code&gt; use exec mode, while Netmiko, Ansible, and Scrapli use PTY/shell.&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&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-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Exec mode: split newline-delimited payloads for batch support&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;exec&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execCmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execCmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;execCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;exit-status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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;h3 id="the-https-server"&gt;The HTTPS Server&lt;/h3&gt;
&lt;p&gt;The HTTPS side generates a self-signed P-256 ECDSA certificate at startup (negotiating TLS 1.3 with &lt;code&gt;TLS_AES_128_GCM_SHA256&lt;/code&gt;) and exposes the same endpoints as the &lt;a href="https://www.cisco.com/c/en/us/td/docs/security/asa/misc/http-interface/asa-http-interface.html"&gt;Cisco ASA HTTP interface&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /admin/exec/show+version&lt;/code&gt;. Single command, URL-encoded&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /admin/exec/cmd1/cmd2/cmd3&lt;/code&gt;. Multiple commands, slash-separated&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/config&lt;/code&gt;. Bulk commands, newline-delimited body&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Authentication is HTTP Basic over TLS, matching the ASA&amp;rsquo;s behavior.&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&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-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handleExec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/admin/exec/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReplaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;+&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;text/plain&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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;h3 id="the-benchmark-client"&gt;The Benchmark Client&lt;/h3&gt;
&lt;p&gt;The client calls both transports with the same commands and measures wall-clock time.
The key difference is visible in the code: SSH requires connection setup, auth, channel open, and per-command exec requests.
HTTPS is a single HTTP call:&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&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-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// SSH: connect + auth + exec per command&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sshConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombinedOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// HTTPS: one request, all commands&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/admin/exec/&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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;h3 id="latency-injection"&gt;Latency Injection&lt;/h3&gt;
&lt;p&gt;Latency is injected using Linux &lt;code&gt;tc netem&lt;/code&gt; on the loopback interface, configured entirely via the &lt;a href="https://github.com/vishvananda/netlink"&gt;&lt;code&gt;vishvananda/netlink&lt;/code&gt;&lt;/a&gt; library, the same netlink library used by Docker and Kubernetes.
The tool sets up a &lt;code&gt;prio&lt;/code&gt; qdisc with per-port &lt;code&gt;u32&lt;/code&gt; filters so that traffic to the SSH and HTTPS server ports gets the configured one-way delay, while other loopback traffic is unaffected.
This requires root or &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt;, the same requirement as most raw-socket networking tools.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Netlink qdisc setup code (click to expand)&lt;/summary&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&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-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Qdisc setup via netlink - no shell-out to tc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;prio&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewPrio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;QdiscAttrs&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LinkIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;loopbackIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MakeHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HANDLE_ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;prio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Bands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;prio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PriorityMap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// unmatched traffic → band 0 (no delay)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QdiscAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;netem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewNetem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;QdiscAttrs&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MakeHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NetemQdiscAttrs&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Microseconds&lt;/span&gt;&lt;span class="p"&gt;())},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QdiscAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;netem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Per-port u32 filter: match dport, classify to delayed band&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FilterAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;U32&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FilterAttrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FilterAttrs&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MakeHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0800&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ClassId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MakeHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TcU32Sel&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TC_U32_TERMINAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;netlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TcU32Key&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;Val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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;/details&gt;
&lt;p&gt;Because &lt;code&gt;netem&lt;/code&gt; operates at the kernel&amp;rsquo;s network stack, it captures real TCP behavior: Nagle&amp;rsquo;s algorithm, delayed ACKs, TCP window scaling, and proper per-packet delay.
Every packet in both directions, client-to-server and server-to-client, experiences the configured delay.
This is more accurate than userspace delay injection, which can&amp;rsquo;t distinguish between logically separate protocol exchanges that happen to be coalesced into a single write.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;-userspace&lt;/code&gt; flag is available as a fallback for environments where root isn&amp;rsquo;t available, but the published numbers all use &lt;code&gt;tc netem&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="latency-profiles"&gt;Latency Profiles&lt;/h2&gt;
&lt;p&gt;Each profile corresponds to a real network path, sourced from &lt;a href="https://www.verizon.com/business/terms/latency/"&gt;Verizon Enterprise&amp;rsquo;s monthly IP latency statistics&lt;/a&gt; (March 2026).
The simulated RTT values are rounded for readability; the Verizon measured column shows the exact source data:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Simulated RTT&lt;/th&gt;
&lt;th&gt;Real-world path&lt;/th&gt;
&lt;th&gt;Verizon measured RTT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;Co-located&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;campus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;Same data center&lt;/td&gt;
&lt;td&gt;AWS/Prisma: 1-2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;US backbone&lt;/td&gt;
&lt;td&gt;29.9ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;continental&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;NYC ↔ London&lt;/td&gt;
&lt;td&gt;70.2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;intercontinental&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;US ↔ Hong Kong&lt;/td&gt;
&lt;td&gt;145.5ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transpacific&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;175ms&lt;/td&gt;
&lt;td&gt;NA ↔ Taiwan&lt;/td&gt;
&lt;td&gt;175.2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="benchmark-modes"&gt;Benchmark Modes&lt;/h2&gt;
&lt;p&gt;The client tests these scenarios across both transports (plus a multi-command GET mode when running more than one command per iteration):&lt;/p&gt;
&lt;h3 id="ssh-exec-modes"&gt;SSH Exec Modes&lt;/h3&gt;
&lt;p&gt;SSH exec mode opens a channel, sends a command, and reads the output. This is 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 OpenSSH&amp;rsquo;s &lt;code&gt;ssh host &amp;quot;cmd&amp;quot;&lt;/code&gt; use. Each command gets its own channel.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh/fresh-conn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full SSH lifecycle per iteration: TCP + handshake + auth + channel + exec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh/reuse-conn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One SSH connection shared across all iterations (ControlMaster-style)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh/batch-exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-line command string over a single exec session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="ssh-ptyshell-modes"&gt;SSH PTY/Shell Modes&lt;/h3&gt;
&lt;p&gt;SSH PTY mode opens an interactive shell with a pseudo-terminal, sends commands as keystrokes, and detects the prompt after each command.
This is what &lt;strong&gt;Netmiko&lt;/strong&gt;, &lt;strong&gt;Ansible &lt;code&gt;network_cli&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;Scrapli&lt;/strong&gt;, and most real-world network automation tools use.
Many network devices don&amp;rsquo;t support exec mode properly, and automation tools need prompt detection, pagination control, and mode transitions.
(Part 1 called this the &lt;a href="https://network-notes.com/posts/2026/cli-over-https-1/#the-screen-scraping-tax"&gt;&amp;ldquo;screen-scraping tax&amp;rdquo;&lt;/a&gt;, the cost of parsing an unstructured byte stream.)&lt;/p&gt;
&lt;p&gt;The PTY benchmark includes session preparation (sending &lt;code&gt;terminal length 0&lt;/code&gt; and &lt;code&gt;terminal width 511&lt;/code&gt; before the first command) and per-command echo verification (reading until the echoed command appears, then reading until the prompt), matching the protocol-level behavior common to all major tools.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh/pty-fresh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full SSH lifecycle + PTY + shell + session prep + commands with echo verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh/pty-reuse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared connection, new PTY/shell per iteration with session prep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="https-modes"&gt;HTTPS Modes&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https/fresh-conn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New TCP + TLS handshake per iteration (&lt;code&gt;DisableKeepAlives: true&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https/keep-alive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single TCP + TLS connection reused across all iterations (default HTTP behavior)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https/batch-post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All commands in one POST body (&lt;code&gt;/admin/config&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https/multi-cmd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All commands in one GET request (ASA &lt;code&gt;/admin/exec/cmd1/cmd2&lt;/code&gt; syntax)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each mode runs N iterations, each executing 5 &lt;code&gt;show version&lt;/code&gt; commands. The client reports min, max, average, p50, p95, and standard deviation.&lt;/p&gt;
&lt;h2 id="running-it-yourself"&gt;Running It Yourself&lt;/h2&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;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/lykinsbd/clibench.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; clibench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -o bin/bench ./cmd/bench/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Baseline - no added latency (no root needed)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/bench -latency &lt;span class="nb"&gt;local&lt;/span&gt; -iterations &lt;span class="m"&gt;20&lt;/span&gt; -commands &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# US backbone - 30ms RTT (requires root for tc netem)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/bench -latency regional -iterations &lt;span class="m"&gt;20&lt;/span&gt; -commands &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# US to Hong Kong - 150ms RTT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/bench -latency intercontinental -iterations &lt;span class="m"&gt;20&lt;/span&gt; -commands &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fallback: userspace delay injection (no root, less accurate)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/bench -latency regional -iterations &lt;span class="m"&gt;20&lt;/span&gt; -commands &lt;span class="m"&gt;5&lt;/span&gt; -userspace
&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;clibench embeds its own server. No separate process needed. Non-local profiles require root (or &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt;) for &lt;code&gt;tc netem&lt;/code&gt; on the loopback interface. Output is JSON.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;5 commands per iteration, all times in milliseconds (average of 20 iterations). Latency injected via &lt;code&gt;tc netem&lt;/code&gt; on the loopback interface.&lt;/p&gt;
&lt;p&gt;At zero latency, SSH exec mode is the fastest option. There&amp;rsquo;s no round-trip penalty, and SSH&amp;rsquo;s binary framing has less per-message overhead than HTTP headers + TLS:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/cli-over-https-bench-local.016c463cda2ffbff3ef237d3f2986ee2b27c37a09c455f2c394e3c0b416ba37c.svg" alt="Bar chart at 0ms RTT showing SSH exec-fresh at 3.9ms, PTY-fresh at 4.3ms, and HTTPS fresh at 12.0ms" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The moment real network latency enters the picture, the result flips. At 30ms RTT, a US backbone path per Verizon&amp;rsquo;s March 2026 measurements, HTTPS batch is 16.8x faster than SSH PTY fresh and 15.9x faster than SSH exec fresh-conn:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/cli-over-https-bench-regional.eb891e20708c51e9da713ce909b2088de07d106485a700f823d35833d01957f0.svg" alt="Bar chart at 30ms RTT showing SSH exec-fresh at 494ms, PTY-fresh at 522ms, and HTTPS batch at 31ms" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;At intercontinental distances (US ↔ Hong Kong, 150ms RTT), SSH PTY fresh takes 2.6 seconds for 5 commands. SSH exec fresh takes 2.4 seconds. HTTPS batch does it in 151ms:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/cli-over-https-bench-intercontinental.5a06f3614b9f61e6c31b509722e421529c638a65a69e5f8545afcafdfff06c33.svg" alt="Bar chart at 150ms RTT showing SSH PTY-fresh at 2,565ms, exec-fresh at 2,412ms, and HTTPS batch at 151ms" loading="lazy" /&gt;&lt;/p&gt;
&lt;h3 id="exec-mode-vs-ptyshell-mode"&gt;Exec Mode vs PTY/Shell Mode&lt;/h3&gt;
&lt;p&gt;The PTY overhead comes from session preparation (&lt;code&gt;terminal length 0&lt;/code&gt;, &lt;code&gt;terminal width 511&lt;/code&gt;) and per-command echo verification. At higher latencies, this adds up:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;RTT&lt;/th&gt;
&lt;th&gt;SSH exec fresh&lt;/th&gt;
&lt;th&gt;SSH PTY fresh&lt;/th&gt;
&lt;th&gt;PTY overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;3.9ms&lt;/td&gt;
&lt;td&gt;4.3ms&lt;/td&gt;
&lt;td&gt;+0.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;campus&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;td&gt;42ms&lt;/td&gt;
&lt;td&gt;+2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regional&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;494ms&lt;/td&gt;
&lt;td&gt;522ms&lt;/td&gt;
&lt;td&gt;+28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;continental&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;1,144ms&lt;/td&gt;
&lt;td&gt;1,213ms&lt;/td&gt;
&lt;td&gt;+69ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intercontinental&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;2,412ms&lt;/td&gt;
&lt;td&gt;2,565ms&lt;/td&gt;
&lt;td&gt;+153ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The PTY overhead scales linearly with RTT because the session prep commands add roughly one extra round trip of overhead before the first real command runs. At 150ms RTT, that&amp;rsquo;s ~150ms of pure protocol overhead. And this is the best case. Real devices add processing time, ANSI escape codes, and prompt detection regex that the emulator doesn&amp;rsquo;t capture.&lt;/p&gt;
&lt;h3 id="speedup-vs-ssh-pty-fresh-what-most-tools-actually-use"&gt;Speedup vs SSH PTY fresh (what most tools actually use)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;RTT&lt;/th&gt;
&lt;th&gt;SSH exec fresh&lt;/th&gt;
&lt;th&gt;SSH reuse&lt;/th&gt;
&lt;th&gt;HTTPS keep-alive&lt;/th&gt;
&lt;th&gt;HTTPS batch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;3.9x&lt;/td&gt;
&lt;td&gt;7.2x&lt;/td&gt;
&lt;td&gt;19.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;campus&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;1.8x&lt;/td&gt;
&lt;td&gt;3.3x&lt;/td&gt;
&lt;td&gt;16.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regional&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;1.7x&lt;/td&gt;
&lt;td&gt;3.3x&lt;/td&gt;
&lt;td&gt;16.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;continental&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;1.7x&lt;/td&gt;
&lt;td&gt;3.4x&lt;/td&gt;
&lt;td&gt;16.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intercontinental&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;1.7x&lt;/td&gt;
&lt;td&gt;3.4x&lt;/td&gt;
&lt;td&gt;17.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="what-the-numbers-say"&gt;What the Numbers Say&lt;/h2&gt;
&lt;p&gt;All results are from 20 iterations per profile. Variance was low, at regional (30ms), SSH exec fresh-conn p50 was 492ms with p95 at 508ms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At zero latency, SSH exec wins.&lt;/strong&gt; When there&amp;rsquo;s no network delay, TLS handshake overhead dominates. SSH exec fresh-conn takes 3.9ms; HTTPS fresh-conn takes 12.0ms. But PTY mode is already slower at 4.3ms due to session prep overhead. The reuse modes tell a different story: SSH exec reuse (1.1ms) and HTTPS keep-alive (0.6ms) are both sub-millisecond. Once the handshake is amortized, both protocols are fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Most automation tools don&amp;rsquo;t use exec mode.&lt;/strong&gt; As covered &lt;a href="https://network-notes.com/posts/2026/cli-over-https-2/#ssh-ptyshell-modes"&gt;above&lt;/a&gt;, they use PTY/shell mode for prompt detection, pagination control, and mode transitions. The PTY numbers are what your automation actually experiences.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSH reuse helps, but not enough.&lt;/strong&gt; Sharing one SSH connection (the ControlMaster pattern) eliminates the handshake cost, but each command still requires its own round trips. The improvement is consistent at ~1.7x. Real, but modest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTPS keep-alive is ~3.4x faster at any real latency.&lt;/strong&gt; Every HTTP client library does connection pooling by default. You don&amp;rsquo;t have to configure anything special. Just reuse the &lt;code&gt;http.Client&lt;/code&gt;. At 30ms RTT, that&amp;rsquo;s 158ms vs 522ms (PTY fresh).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTPS batch is ~17x faster.&lt;/strong&gt; Batching all commands into a single HTTP request eliminates per-command round trips entirely. The entire exchange costs one round trip regardless of command count. At 150ms RTT, that&amp;rsquo;s 151ms vs 2,565ms (PTY fresh). Unlike keep-alive (which still pays one round trip per command), batch mode pays a fixed cost regardless of command count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The advantage grows with command count, for per-command modes.&lt;/strong&gt; At 30ms RTT with 50 commands, SSH exec fresh-conn takes 3,253ms. SSH PTY fresh takes 1,912ms (PTY avoids per-command channel overhead but pays per-command echo verification). HTTPS keep-alive takes 1,548ms. But HTTPS batch takes just 33ms. A ~99x improvement over exec fresh and ~58x over PTY fresh. SSH batch-exec shows the same flat scaling (~250ms regardless of command count), confirming this is a property of batching, not the transport.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/cli-over-https-command-scaling.7c056e6f62149c833ff6d10a41480272480f71fb76eaa5acb3ddc1d8a7df97dc.svg" alt="Time vs command count at 30ms RTT: batch modes stay flat while per-command modes scale linearly" loading="lazy" /&gt;&lt;/p&gt;
&lt;h2 id="what-this-means-at-scale"&gt;What This Means at Scale&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re managing 100 devices serially (worst case, no concurrency), using PTY mode (what Netmiko/Ansible actually do):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;RTT&lt;/th&gt;
&lt;th&gt;SSH PTY fresh (total)&lt;/th&gt;
&lt;th&gt;HTTPS batch (total)&lt;/th&gt;
&lt;th&gt;Time saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;regional&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;52s&lt;/td&gt;
&lt;td&gt;3.1s&lt;/td&gt;
&lt;td&gt;49s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;continental&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;121s (2.0 min)&lt;/td&gt;
&lt;td&gt;7.4s&lt;/td&gt;
&lt;td&gt;114s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intercontinental&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;257s (4.3 min)&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;td&gt;242s (4.0 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Concurrency shrinks the wall time, but the per-device cost stays the same. At 150ms RTT with 10 concurrent workers against 1,000 devices, SSH PTY takes ~4.3 minutes of wall time. HTTPS batch takes ~15 seconds.&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;This benchmark measures transport overhead, not device processing time. Real network devices add their own latency to command execution: parsing the command, generating output, writing to the terminal. That cost is the same regardless of transport, so it doesn&amp;rsquo;t affect the relative comparison.&lt;/p&gt;
&lt;p&gt;The HTTPS server uses a self-signed certificate with no session resumption. TLS 1.3 0-RTT resumption would make the HTTPS numbers even better on repeated connections, but I didn&amp;rsquo;t implement it because most device management scenarios don&amp;rsquo;t maintain long-lived TLS sessions.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;-userspace&lt;/code&gt; flag is available as a fallback for environments where root isn&amp;rsquo;t available, but it under-counts SSH round trips due to write coalescing in Go&amp;rsquo;s &lt;code&gt;crypto/ssh&lt;/code&gt;. The published numbers all use &lt;code&gt;tc netem&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p&gt;In Part 3, I&amp;rsquo;ll look at what happens when you can&amp;rsquo;t change the device: the proxy pattern. Move SSH to the edge, talk HTTPS over the WAN, and capture most of the improvement without touching a single device config.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/lykinsbd/clibench"&gt;benchmark code&lt;/a&gt; already supports proxy mode. Try it yourself and see what your numbers look like.&lt;/p&gt;</description></item><item><title>CLI Over HTTPS Part 1: The Protocol Tax</title><link>https://network-notes.com/posts/2026/cli-over-https-1/</link><pubDate>Tue, 28 Apr 2026 09:00:00 +0000</pubDate><author>brett@network-notes.com (Brett Lykins)</author><dc:creator>Brett Lykins</dc:creator><guid>https://network-notes.com/posts/2026/cli-over-https-1/</guid><description>&lt;p&gt;During my six years at Rackspace, we spent a lot of time thinking about how to interact with network devices faster.
We had tens of thousands of them; firewalls, load balancers, switches, routers and more, all spread across multiple data centers on four continents.&lt;/p&gt;
&lt;p&gt;In the early days, managing devices across these data centers meant running shell scripts, Expect, or Perl from local machines and centralized bastions over the WAN.
It was operationally painful.
SSH connections to devices on other continents were slow enough that teams scheduled automation runs around maintenance windows not just because of the change itself, but because of how long it took to deliver the changes.&lt;/p&gt;
&lt;p&gt;An Erlang-based platform solved this by co-locating the SSH connections with the devices.
They ran inside each data center, talking SSH to devices over local links where the protocol overhead was negligible.
Phil Toland &lt;a href="https://www.infoq.com/presentations/Erlang-Ruby-Rackspace/"&gt;presented the Erlang architecture at Erlang Factory 2012&lt;/a&gt;, detailing Erlang and Ruby managing backups and automation for 20,000+ network devices across 8 data centers.
My team later supplemented it with a Go microservices architecture to provide API-driven access to device CLIs.
Both systems were effective not just because of language capabilities; crucially they were fast because SSH stayed local.&lt;/p&gt;
&lt;p&gt;But, even with co-located endpoints, the Cisco ASA fleet was a special case which tested our capabilities due to the extreme size of some of the Access Lists.
That&amp;rsquo;s when someone discovered that the ASA has an HTTP interface we could use.
Not the ineffective ASA Java-based REST API, but an actual CLI-over-HTTPS endpoint used by the ASDM client.
There was a URL on the device where you could send the same commands you&amp;rsquo;d type into an SSH session, but over an HTTPS request.
We tried it as an experiment, and it was remarkably faster.
What started as a curiosity became a production lifesaver for our ASA fleet.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been thinking about that experience ever since, and I finally decided to quantify it properly.
In this series of posts, I will quantify the performance difference between SSH and HTTPS as CLI transports and explain why the gap exists.&lt;/p&gt;
&lt;h2 id="the-ssh-tax"&gt;The SSH Tax&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;For a deeper look at SSH&amp;rsquo;s protocol layers, channel types, and how NETCONF and RESTCONF fit in, see &lt;a href="https://network-notes.com/posts/2026/ssh-under-the-hood/"&gt;What Actually Happens When You SSH Into a Router&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When your automation tool opens an SSH connection to a network device, here&amp;rsquo;s what actually happens on the wire before a single byte of command output comes back.
(Throughout this series, &amp;ldquo;RT&amp;rdquo; means a round trip: one message out, one message back. &amp;ldquo;RTT&amp;rdquo; is the round-trip time in milliseconds for a given network path.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. TCP three-way handshake.&lt;/strong&gt; SYN, SYN-ACK, ACK. One round trip. (The ACK can piggyback on the first data segment, but the connection isn&amp;rsquo;t usable until the handshake completes.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Protocol version exchange.&lt;/strong&gt; Client and server each send an identification string (&lt;code&gt;SSH-2.0-OpenSSH_9.6&lt;/code&gt;). Another round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Key exchange.&lt;/strong&gt; The client and server negotiate algorithms (encryption, MAC, compression) via KEXINIT, then perform a Diffie-Hellman key exchange. This takes 1-3 round trips depending on whether the KEXINIT messages cross in flight and which DH group is negotiated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Service request.&lt;/strong&gt; The client requests the &lt;code&gt;ssh-userauth&lt;/code&gt; service. One round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. User authentication.&lt;/strong&gt; Password or public key auth. 1-3 round trips depending on how many methods the server probes (GSSAPI, publickey, then password, each one a round trip).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Channel open.&lt;/strong&gt; SSH multiplexes channels over a single connection. Opening a session channel is another round trip.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s &lt;strong&gt;6-10 round trips&lt;/strong&gt; just to get an authenticated channel. If you&amp;rsquo;re running a single exec-style command, add one more round trip for the request/response and you&amp;rsquo;re done.&lt;/p&gt;
&lt;p&gt;But automation tools don&amp;rsquo;t use exec mode. Netmiko, Ansible, and Scrapli open a PTY/shell channel, which adds:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. PTY request.&lt;/strong&gt; Ask the server for a pseudo-terminal. One round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. Shell request.&lt;/strong&gt; Start an interactive shell on that PTY. One round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9. Session prep.&lt;/strong&gt; Send &lt;code&gt;terminal length 0&lt;/code&gt;, &lt;code&gt;terminal width 511&lt;/code&gt;, and wait for each prompt. Two to three more round trips.&lt;/p&gt;
&lt;p&gt;Add it up: &lt;strong&gt;10-15 round trips&lt;/strong&gt; before you see the output of &lt;code&gt;show version&lt;/code&gt;, with most real-world automation sessions landing at 10-12. The &lt;a href="https://github.com/francoismichel/ssh3"&gt;SSH3 project&lt;/a&gt; cites similar overhead in their motivation for building SSH over HTTP/3.&lt;/p&gt;
&lt;p&gt;SSH multiplexing (ControlMaster) amortizes the connection setup cost across sessions, but the first connection still pays the full overhead. It helps for repeated connections to the same device, but doesn&amp;rsquo;t solve the problem at scale across thousands of hosts.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/ssh-connection-lifecycle.c495d377728639bd46d3e72a152d0060cd70f07f15913d5753113b9d35d43035.svg" alt="SSH connection lifecycle showing 10-12 round trips: TCP handshake, version exchange, key exchange, authentication, channel open, PTY request, shell request, session prep, and first command" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;At zero latency (localhost), nobody cares. At real-world distances, it compounds fast.&lt;/p&gt;
&lt;h2 id="what-this-costs-at-real-distances"&gt;What This Costs at Real Distances&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.verizon.com/business/terms/latency/"&gt;Verizon Enterprise publishes monthly backbone latency measurements&lt;/a&gt; from their global network. Here&amp;rsquo;s what SSH connection setup costs at those measured round-trip times, assuming 10-15 round trips for a typical PTY/shell automation session:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Measured RTT&lt;/th&gt;
&lt;th&gt;SSH setup (10-15 RT)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US backbone (intra-region)&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;300-450ms&lt;/td&gt;
&lt;td&gt;Verizon Mar 2026: 29.9ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transatlantic (NYC ↔ London)&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;700-1,050ms&lt;/td&gt;
&lt;td&gt;Verizon Mar 2026: 70.2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US ↔ Hong Kong&lt;/td&gt;
&lt;td&gt;146ms&lt;/td&gt;
&lt;td&gt;1,460-2,190ms&lt;/td&gt;
&lt;td&gt;Verizon Mar 2026: 145.5ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US ↔ New Zealand&lt;/td&gt;
&lt;td&gt;174ms&lt;/td&gt;
&lt;td&gt;1,740-2,610ms&lt;/td&gt;
&lt;td&gt;Verizon Mar 2026: 174.2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That&amp;rsquo;s just connection setup.
You haven&amp;rsquo;t sent a command yet.
And if your automation opens a fresh SSH connection per device, or per task, which is &lt;a href="https://www.howtouselinux.com/post/the-hidden-ssh-setting-that-makes-ansible-playbooks-faster"&gt;Ansible&amp;rsquo;s default behavior without ControlMaster&lt;/a&gt;, you pay this cost repeatedly.&lt;/p&gt;
&lt;p&gt;A thousand devices at 30ms RTT without concurrency: 300-450 seconds of pure SSH handshake overhead. At 150ms RTT: 1,500-2,250 seconds. Concurrency reduces wall time, but every device still pays the full per-connection cost.&lt;/p&gt;
&lt;h2 id="the-screen-scraping-tax"&gt;The Screen-Scraping Tax&lt;/h2&gt;
&lt;p&gt;SSH&amp;rsquo;s round-trip overhead is only half the story. The other half is what happens after you connect.&lt;/p&gt;
&lt;p&gt;SSH gives you a byte stream. A pseudo-terminal. Your automation tool has to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Detect the prompt&lt;/strong&gt; to know when command output is complete. Netmiko does this with regex matching on every chunk of bytes received, waiting for a pattern like &lt;code&gt;hostname#&lt;/code&gt;. If the output is large or arrives in small TCP segments, this means multiple read cycles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handle pagination.&lt;/strong&gt; &lt;code&gt;--More--&lt;/code&gt; prompts. Most tools send &lt;code&gt;terminal length 0&lt;/code&gt; first, which is another command round trip.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigate mode transitions.&lt;/strong&gt; &lt;code&gt;enable&lt;/code&gt;, &lt;code&gt;configure terminal&lt;/code&gt;, &lt;code&gt;interface GigabitEthernet0/0&lt;/code&gt;. Each one is a command, a prompt change, and a round trip.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait for command completion.&lt;/strong&gt; Netmiko&amp;rsquo;s default &lt;code&gt;read_timeout&lt;/code&gt; adds deliberate delays to avoid reading partial output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is Netmiko&amp;rsquo;s fault. It&amp;rsquo;s doing the best it can with what SSH gives it: an unstructured byte stream with no framing, no content-length, no end-of-message delimiter. The tool has to infer when the device is done talking. Compare that to HTTPS (&lt;code&gt;Content-Length&lt;/code&gt;), NETCONF (&lt;code&gt;]]&amp;gt;]]&amp;gt;&lt;/code&gt; delimiter), or even SSH exec mode (exit codes). PTY is the only mode where the client has to guess when the response is complete.&lt;/p&gt;
&lt;h2 id="the-https-alternative"&gt;The HTTPS Alternative&lt;/h2&gt;
&lt;p&gt;Now consider what happens when you send the same command over HTTPS:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. TCP three-way handshake.&lt;/strong&gt; Same as SSH. One round trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. TLS 1.3 handshake.&lt;/strong&gt; 1 round trip. (TLS 1.2 was 2 round trips; &lt;a href="https://datatracker.ietf.org/doc/html/rfc8446"&gt;TLS 1.3 cut it in half&lt;/a&gt;. With session resumption, it&amp;rsquo;s 0 round trips.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. HTTP request/response.&lt;/strong&gt; Send the command, get the output. 1 round trip.&lt;/p&gt;
&lt;p&gt;Total: &lt;strong&gt;about 3 round trips&lt;/strong&gt; for a fresh connection. With connection reuse (HTTP keep-alive, which every HTTP client library does by default): &lt;strong&gt;1 round trip per command&lt;/strong&gt; after the first.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/https-connection-lifecycle.6eccac0763c450bcb998f810f0e09b2a21b3807bb16af02efe6e84a79d3f3784.svg" alt="HTTPS connection lifecycle showing ~3 round trips: TCP handshake, TLS 1.3 handshake, HTTP request and response" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Side by side, the difference is stark.
Here&amp;rsquo;s the same operation (5 commands at 30ms RTT) over both protocols:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/ssh-vs-https-timeline.58a901d90c485d14c887a2dcde7437eb25d29c9a37727c56f65aba89df56a0e0.svg" alt="Side-by-side timeline: SSH PTY/shell takes 15 round trips and 522ms, HTTPS batch takes 3 round trips and 31ms" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the part that changes the math entirely: &lt;strong&gt;you can batch commands&lt;/strong&gt;. The Cisco ASA HTTP interface accepts multiple commands in a single request, either slash-separated in the URL or newline-delimited in a POST body. Ten commands, one request, one response. That&amp;rsquo;s 1 round trip for 10 commands, vs SSH where each command requires its own channel-open and exec round trips.&lt;/p&gt;
&lt;p&gt;No prompt detection. No pagination handling. No mode transitions. The response body is the command output, with a proper HTTP &lt;code&gt;Content-Length&lt;/code&gt; header. Your client knows exactly when the response is complete.&lt;/p&gt;
&lt;h2 id="prior-art-the-cisco-asa-http-interface"&gt;Prior Art: The Cisco ASA HTTP Interface&lt;/h2&gt;
&lt;p&gt;This isn&amp;rsquo;t a theoretical proposal. Cisco has shipped an HTTP-based CLI interface on the ASA for years. Their &lt;a href="https://www.cisco.com/c/en/us/td/docs/security/asa/misc/http-interface/asa-http-interface.html"&gt;own documentation&lt;/a&gt; opens with this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One way to interface with most network appliances including ASAs is via CLI. An automated tool could Telnet or SSH into a device, authenticate and execute commands, one at a time. This method has a number of drawbacks, however. The tool must maintain the state of the Telnet and SSH connection, and if that connection is broken, the login process has to be repeated. Using CLI, it is only possible to send one command at a time, so administering many firewalls would be time consuming, &lt;strong&gt;especially when the firewalls are some latency away from the management station&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cisco identified the exact problem and shipped a solution. The interface is straightforward:&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;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Single command&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sk -u admin:admin https://asa.example.com/admin/exec/show+version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Multiple commands in one request&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sk -u admin:admin https://asa.example.com/admin/exec/show+version/show+ip+interface+brief
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Bulk config push&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sk -u admin:admin -X POST --data-binary @config.txt &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://asa.example.com/admin/config
&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;Basic auth over TLS. URL-encoded commands in the path. Newline-delimited commands in a POST body. No SDK, no client library, no special protocol. Just HTTPS.&lt;/p&gt;
&lt;p&gt;Aaron Hackney, another principal engineer at Rackspace at the time who was dealing with the same fleet of ASAs, went deep on this interface in a &lt;a href="https://community.cisco.com/t5/security-blogs/script-an-asdm-session-part-i/bc-p/3663026"&gt;BRKSEC-2031 session at Cisco Live Orlando 2018&lt;/a&gt;.
The work involved reverse-engineering the ASDM client&amp;rsquo;s HTTP calls and building Python tooling to script against the same interface.
The two-part Cisco community blog series that followed is still one of the best references for anyone looking to automate ASAs over HTTPS instead of SSH.&lt;/p&gt;
&lt;h2 id="great-but-my-switches-dont-have-an-https-cli"&gt;&amp;ldquo;Great, but My Switches Don&amp;rsquo;t Have an HTTPS CLI&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;The network automation community has spent a decade building tooling around SSH.
Netmiko, Paramiko, Ansible&amp;rsquo;s &lt;code&gt;network_cli&lt;/code&gt;, Nornir, NAPALM. All SSH-based for CLI interaction.
That investment is real and valuable. NETCONF and gNMI exist as alternatives, but they require device support for structured data models.
A different paradigm entirely. Many organizations have thousands of CLI commands, templates, and playbooks that work.
They don&amp;rsquo;t need a new data model.
They need a faster pipe.&lt;/p&gt;
&lt;p&gt;And the ASA&amp;rsquo;s HTTP interface is the exception, not the rule.
Most network platforms (IOS, NX-OS, EOS, Junos) don&amp;rsquo;t expose their CLI over HTTPS.
Realistically, that&amp;rsquo;s not going to change across the industry without a major shift in how vendors think about management plane interfaces.
I&amp;rsquo;m not holding my breath.&lt;/p&gt;
&lt;p&gt;But the protocol overhead problem doesn&amp;rsquo;t go away just because the devices don&amp;rsquo;t support the better transport natively.
So what do you do?&lt;/p&gt;
&lt;p&gt;You move the expensive SSH transactions to the edge.
Put a lightweight proxy, an API gateway, a microservice, whatever you want to call it, close to the devices it manages.
That proxy talks SSH to the devices over a low-latency local network where the round-trip overhead is negligible.
Your automation platform talks HTTPS to the proxy over the WAN, where the round-trip savings actually matter.&lt;/p&gt;
&lt;p&gt;The pattern looks like this: your centralized automation (Ansible, Nornir, custom tooling) sends an HTTPS request to a proxy co-located with the devices.
The proxy opens an SSH session to the device on a 1-2ms local link, executes the commands, and returns the output in the HTTP response.
Your automation never touches SSH directly.
It gets the speed of HTTPS over the WAN and the compatibility of SSH on the last hop.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://network-notes.com/img/2026/cli-over-https-proxy-concept.ce050a5b98dc507728b570b47f6e8daadd209ff85ebba3533b93cf6be936e078.svg" alt="Edge proxy architecture: automation talks HTTPS over the WAN to a co-located proxy, which talks SSH to devices over a 1-2ms local link" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t hypothetical.
It&amp;rsquo;s the architecture behind tools like my &lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS (Netmiko as a Service)&lt;/a&gt; application, which wraps Netmiko&amp;rsquo;s SSH sessions behind a REST API.
Deploy a NAAS instance in each region or data center, and your automation talks HTTP to the nearest one.
The SSH overhead stays local.
The WAN traffic is pure HTTPS.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://network-notes.com/posts/2026/cli-over-https-2/"&gt;Part 2&lt;/a&gt;, I&amp;rsquo;ll detail a dual-protocol device emulator I built in Go that serves the same commands over both SSH and HTTPS, and a benchmark client that measures the difference at realistic latencies.
The &lt;a href="https://github.com/lykinsbd/clibench"&gt;code is already public&lt;/a&gt; if you want to run ahead.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My take:&lt;/strong&gt; SSH is a fine protocol for interactive terminal sessions.
It&amp;rsquo;s a poor protocol for automation at scale.
The round-trip overhead is baked into the protocol design, and no amount of connection pooling or multiplexing fully eliminates it.
HTTPS with TLS 1.3 is a strictly better transport for the &amp;ldquo;send command, get output&amp;rdquo; pattern that defines CLI automation.
The industry should be building toward it.&lt;/p&gt;
&lt;/blockquote&gt;</description></item></channel></rss>