
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Wed, 20 May 2026 19:58:36 GMT</lastBuildDate>
        <item>
            <title><![CDATA[When "idle" isn't idle: how a Linux kernel optimization became a QUIC bug]]></title>
            <link>https://blog.cloudflare.com/quic-death-spiral-fix/</link>
            <pubDate>Tue, 12 May 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[ We investigated a bug where CUBIC's congestion window became pinned at its minimum floor, causing a performance to plummet. The fix involved correctly measuring idle periods to distinguish RTT wait times from actual application idleness. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>CUBIC, standardized in <a href="https://www.rfc-editor.org/rfc/rfc9438.html"><u>RFC 9438</u></a>, is the default congestion controller in Linux, and as a result governs how most TCP and QUIC connections on the public Internet probe for available bandwidth, back off when they detect loss, and recover afterward. At Cloudflare, our open-source implementation of QUIC,<a href="https://github.com/cloudflare/quiche"> <u>quiche</u></a>, uses CUBIC as its default congestion controller, meaning this code is in the critical path for a significant share of the traffic we serve.</p><p>In this post, we’ll tell the story of a bug in which CUBIC's congestion window (cwnd) gets permanently pinned at its minimum and never recovers from a congestion collapse event.</p><p>The story starts with a <a href="https://github.com/torvalds/linux/commit/30927520dbae297182990bb21d08762bcc35ce1d"><u>Linux kernel change</u></a> aimed at bringing CUBIC into line with the app-limited exclusion described in <a href="https://www.rfc-editor.org/rfc/rfc9438.html#section-4.2-12"><u>RFC 9438 §4.2-12</u></a> — a fix to a real problem in TCP that, when ported to our QUIC implementation, surfaced unexpected behaviors in quiche. It has a happy ending: an elegant (near-)one-line fix that broke the cycle.</p>
    <div>
      <h2>CUBIC's logic in a nutshell</h2>
      <a href="#cubics-logic-in-a-nutshell">
        
      </a>
    </div>
    <p>Before we dive into the core problem, a quick refresher on Congestion Control Algorithms (CCAs) may help to set the stage.</p><p>The central knob a CCA turns is the <b>congestion window</b> (<code>cwnd</code>): the sender-side cap on how many bytes can be in flight (sent but not yet acknowledged) at any moment. A larger <code>cwnd</code> lets the sender push more data per round trip; a smaller <code>cwnd</code> throttles it. Every loss-based CCA, CUBIC included, is ultimately a policy for how to grow <code>cwnd</code> when the network looks healthy and how to shrink it when it doesn't.</p><p>In essence, CCAs aim to maximize data transfer by inferring the "available bandwidth" of the network; because no one wants to pay for a 1 Gbps subscription and only use a fraction of it. The family of loss-based algorithms, to which CUBIC belongs, operate on a fundamental premise: (1) if there is no packet loss, increase the sending rate (i.e. increase the bandwidth utilization); (2) if there is loss, loss-based algorithms assume that the network's capacity has been exceeded, and the sender must back off (i.e. decrease the bandwidth utilization).</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2f9eyD4uXGCNm7Ni7yHhLB/55f2b686d474173dca610fa74796299d/BLOG-3273_image5.png" />
          </figure><p>This logic is built on several assumptions that have been revisited over the years. However, we'll save that discussion for another time.</p>
    <div>
      <h2>The symptom: a test that fails 61% of the time</h2>
      <a href="#the-symptom-a-test-that-fails-61-of-the-time">
        
      </a>
    </div>
    <p>Our investigation started with the report of unexpected failures in our ingress proxy integration test pipeline. This erratic behavior appeared in tests where CUBIC was evaluated in a scenario of heavy loss in the early part of the connection. </p><p>Recovery after congestion collapse is an uncommon regime, but it is exactly the regime a congestion controller exists to handle. Most congestion control tests exercise the steady-state and growth phases of an algorithm; far fewer probe what happens at minimum cwnd, after the connection has been beaten down. Bugs in this corner of the state space are invisible in throughput dashboards, undetectable by static review, and only surface when you deliberately drive a CCA into it and watch whether it can climb back out — which is exactly what this test did.</p><p>The simulated test setup includes the following details:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/67OlSQr35J5HGJwEEkrab/d0dd56faffd4b725a3d8654d856e67c9/BLOG-3273_image6.png" />
          </figure><ul><li><p>Quiche HTTP/3 client and server running at locally (localhost)</p></li><li><p>RTT = 10ms (set up in the configuration)</p></li><li><p>A 10 MB file download over HTTP/3</p></li><li><p>Using CUBIC congestion control</p></li><li><p>With 30% random packet loss injected during the first two seconds</p></li><li><p>After two seconds, loss stops entirely</p></li><li><p>The test has a generous 10-second timeout to complete the download, which is expected to be completed in four or five seconds</p></li></ul><p>The expected behavior is straightforward: CUBIC should take some hits during the loss phase, reduce its congestion window, and once loss stops, steadily ramp up and finish the download well within the timeout. Instead, we observed in multiple 100-time runs that around 60% of our tests were not able to complete the download within the generous 10-second timeout.</p>
    <div>
      <h2>The anomaly: 999 state transitions with zero loss</h2>
      <a href="#the-anomaly-999-state-transitions-with-zero-loss">
        
      </a>
    </div>
    <p>We instrumented<a href="https://github.com/cloudflare/quiche"> <u>quiche's qlog</u></a> output with packet loss events and built visualizations to understand what was happening inside the congestion controller:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1Lz0AbFsNqMz1c1HrB1n31/ecc425a25ab953bccbc3095768a38cc5/BLOG-3273_image10.png" />
          </figure><p><sup><i>Connection overview of a failing test. After T=2s, packet loss stops entirely — yet cwnd remains pinned at the minimum floor and the congestion state oscillates between recovery and congestion avoidance every ~14ms.</i></sup></p><p>After the two-second (2000 ms) mark, packet loss stops entirely. However, the number of bytes in flight remains flat, which contradicts the core logic of the CUBIC algorithm: in the absence of loss, apply more gas to increase throttle (more bytes in our world). <i>This raises the question: if the network is no longer dropping packets, why is the congestion window failing to grow?</i></p><p>When we zoom into that region, our analysis shows that CUBIC enters a rapid oscillation, shown in our plot as an extended recovery phase, between congestion avoidance state (the operational regime phase) and recovery state (the packet loss recovery state) — 999 transitions in approximately 6.7 seconds. That’s one transition every ~14ms — suspiciously close to the connection's RTT (10ms). Throughout this entire period, cwnd is locked at the minimum floor: 2700 bytes, or two full-size packets.</p><p>Clearly something in CUBIC's logic is misinterpreting the state of the connection. The key clue is the oscillation period: ~14ms matches the RTT. Whatever is triggering the recovery/avoidance flip is happening once per round trip, in lockstep with connection's ACK clock; the self-clocking rhythm in which each round-trip's ACKs from the client trigger the server's next send. Because this is a download (server to client), the ACKs in question travel client to server, and CUBIC's state machine runs on the server side: every time those ACKs land, bytes_in_flight drops to zero and the server sends the next two-packet burst, which is what triggers the bug.</p><p>To confirm this behavior was CUBIC-specific, we ran the same test with <a href="https://dl.acm.org/doi/10.1145/235160.235162"><u>Reno</u></a>, another member of the loss-based family but with a different growth rate. The results were conclusive: 100% pass rate, showing Reno recovered cleanly after the loss phase, and revealing that this is a CUBIC-related bug.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1p8xXGkZZaRL2DxTqmpGEb/6aab5696040c957caa790642101a8ac7/BLOG-3273_image8.png" />
          </figure><p><sup><i>Reno recovers cleanly after the loss phase ends at T=2s and completes the download by ~5s</i></sup></p>
    <div>
      <h2>Tracing the root cause</h2>
      <a href="#tracing-the-root-cause">
        
      </a>
    </div>
    <p>Loss-based algorithms have two pedals, gas and brake, with a difference in how they accelerate. Well, CUBIC comes with some extra features. Here we are going to focus on bytes_in_flight == 0.</p>
    <div>
      <h3>TCP CUBIC after idle (Linux, 2017)</h3>
      <a href="#tcp-cubic-after-idle-linux-2017">
        
      </a>
    </div>
    <p>To understand the bug, we first need to understand the optimization it came from. In 2017,an issue was found with Linux kernel's CUBIC implementation. The<a href="https://github.com/torvalds/linux/commit/30927520dbae297182990bb21d08762bcc35ce1d"> <u>commit message</u></a> explains:</p><blockquote><p>The epoch is only updated/reset initially and when experiencing losses. The delta "t" of <code>now - epoch_start</code> can be arbitrary large after app idle as well as the <code>bic_target</code>. Consequentially the slope (inverse of <code>ca-&gt;cnt</code>) would be really large, and eventually <code>ca-&gt;cnt</code> would be lower-bounded in the end to 2 to have delayed-ACK slow-start behavior.</p><p>This particularly shows up when <code>slow_start_after_idle</code> is disabled as a dangerous cwnd inflation (1.5 x RTT) after few seconds of idle time.</p></blockquote><p>The <b>epoch</b> is the reference timestamp CUBIC uses to anchor its growth curve: <code>W_cubic(delta_t)</code> is parameterized by <code>delta_t = now - epoch_start</code>, and the epoch is reset whenever CUBIC restarts its growth function — most notably after a loss event reduces <code>cwnd</code>. Between resets, <code>delta_t</code> grows monotonically with wall-clock time.</p><p>When an application goes idle (stops sending) for a while and then resumes, the CUBIC growth function <code>W_cubic(delta_t)</code> computes <code>delta_t</code> as <code>now - epoch_start</code>, as illustrated in the figure below. Since the epoch wasn't updated during idle, <code>delta_t</code> is huge, producing an enormous target window — and CUBIC would immediately try to inflate <code>cwnd</code> to an unreasonable value.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2zu6eD7MlJOqjZlEUiGhcj/09e84f66dc8a61f65e310f2e3071dafe/BLOG-3273_image7.png" />
          </figure><p>Jana Iyengar's initial fix was to reset `epoch_start` when the application resumes sending. But Neal Cardwell <a href="https://github.com/torvalds/linux/commit/30927520dbae297182990bb21d08762bcc35ce1d"><u>pointed out</u></a> the flaw in that approach:</p><blockquote><p>…it would ask the CUBIC algorithm to recalculate the curve so that we again start growing steeply upward from where cwnd is now (as CUBIC does just after a loss). Ideally we'd want the cwnd growth curve to be the same shape, just shifted later in time by the amount of the idle period.</p></blockquote><p>The elegant solution, authored by Eric Dumazet, Yuchung Cheng, and Neal Cardwell, was to shift the epoch forward by the idle duration rather than resetting it. This preserves the shape of the CUBIC growth curve — just sliding it in time so that the algorithm picks up where it left off.</p>
    <div>
      <h3>The port to quiche (2020)</h3>
      <a href="#the-port-to-quiche-2020">
        
      </a>
    </div>
    <p>When CUBIC was <a href="https://blog.cloudflare.com/cubic-and-hystart-support-in-quiche/"><u>first implemented</u></a> in quiche, this idle-period adjustment was ported. However, QUIC, which runs in the user space, doesn't have TCP's kernel-level <a href="https://github.com/torvalds/linux/commit/30927520dbae297182990bb21d08762bcc35ce1d"><code><u>CA_EVENT_TX_START</u></code></a> callback. Instead, the quiche implementation checks for the idle condition inside <code>on_packet_sent()</code>:</p>
            <pre><code>// cubic.rs — on_packet_sent() (simplified)
/// Updates the state when a packet is sent.
fn on_packet_sent(&amp;mut self, bytes_in_flight: usize, now: Instant, ...) {
    // If the sending burst is restarting (i.e., bytes_in_flight was zero before this send),
    // adjust the congestion recovery start time to account for the gap in sending.
    if bytes_in_flight == 0 {
        let delta = now - self.last_sent_time;
        self.congestion_recovery_start_time += delta;
    }
    // Record the time of this send event.
    self.last_sent_time = now;
}</code></pre>
            
    <div>
      <h3>Where it breaks: the QUIC difference</h3>
      <a href="#where-it-breaks-the-quic-difference">
        
      </a>
    </div>
    <p>The fix ported to quiche included a bug in the original kernel change which was fixed by a <a href="https://github.com/torvalds/linux/commit/c2e7204d180f8efc80f27959ca9cf16fa17f67db"><u>followup change to the kernel cubic module</u></a> about a week later. The commit message for the second fix explains:</p><blockquote><p><code>tcp_cubic</code>: do not set <code>epoch_start</code> in the future
	Tracking idle time in <code>bictcp_cwnd_event()</code> is imprecise, as <code>epoch_start</code>
	is normally set at ACK processing time, not at send time.</p><p>Doing a proper fix would need to add an additional state variable,
	and does not seem worth the trouble, given CUBIC bug has been there
	forever before Jana noticed it.</p><p>Let's simply not set <code>epoch_start</code> in the future, otherwise
	<code>bictcp_update()</code> could overflow and CUBIC would again
	grow <code>cwnd</code> too fast.</p></blockquote><p>As mentioned in the commit message, recovery start time is set during ACK processing, and the computation of the adjustment based on sent times can push the recovery start time into the future. This explains the oscillation between recovery and congestion avoidance seen on our test.  The trap only consistently triggers when every incoming ACK drives bytes_in_flight all the way to zero — which in practice means cwnd has collapsed to its minimum (two packets) and the application has data ready to send another full window the moment an ACK arrives. Outside this regime, bytes_in_flight == 0 is less likely to hold on every send, so it is less likely to trigger the bug. </p><p>Why doesn't this also happen at connection start? The bug only triggers when the connection exits slow-start and switches over to congestion avoidance. Before exiting slow-start, <code>congestion_recovery_start_time</code> is not set, so the buggy branch in <code>on_packet_sent</code> has no recovery boundary to advance. During slow start CUBIC's <code>cwnd</code> grows by the same Reno-style ack-based rule shared by all loss-based CCAs — the cubic curve and its sensitivity to <code>congestion_recovery_start_time</code> only enter the picture once the connection is in congestion avoidance, meaning the trap needs three things at once: a real loss event to set the recovery boundary, congestion avoidance to be running, and <code>cwnd</code> collapsed to the two-packet floor.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3ACAQF0nWRiZv8sFbCiAnE/a45b5ca6cd9322176c91e09c90600c2c/BLOG-3273_image3.png" />
          </figure><p><sup><i>The self-perpetuating recovery trap. At minimum cwnd, every ACK cycle triggers the idle period adjustment with an inflated delta.</i></sup></p><p>At a minimum cwnd (two packets), the dynamics of the connection shift into a "death spiral" where the idle period optimization becomes a self-fulfilling prophecy. This trap operates in a continuous loop:</p><ol><li><p><b>Send and ACK packets: </b>The sender transmits the entire two-packet window. After one RTT (~14ms), both packets are ACKed, causing bytes_in_flight to drop to zero.</p></li><li><p><b>False idle detection:</b> When the next burst is sent, on_packet_sent() sees bytes_in_flight == 0 and assumes the connection was idle, but it was congestion limited.</p></li><li><p><b>Inflated delta:</b> The calculation uses now - last_sent_time to determine the idle duration. When the congestion window (<code>cwnd</code>) is at its minimum, <code>last_sent_time</code> is the timestamp of the <i>start</i> of the previous RTT cycle. Therefore, the resulting delta is approximately 14ms (the connection's RTT + additional rounding errors). This RTT-sized delta is incorrectly applied as the "idle" time. The <i>actual</i> time the connection was idle (the processing gap between the last ACK arriving and the next packet being sent) is effectively 0. By measuring the full RTT instead of the true gap, the delta is inflated significantly, aggressively shifting the recovery start time forward, possibly into the future.</p></li><li><p><b>Perceived recovery:</b> Because the recovery start time is now in the future, the <code>in_congestion_recovery()</code> check returns true for every incoming ACK.  Processing of the next ACK exits recovery and sets the recovery start to the ACK time which is larger than last_sent_time, making it likely for the congestion controller to push the recovery time into the future when doing the next send.</p></li><li><p><b>Stagnation:</b> Since CUBIC skips <code>cwnd</code> growth for any packet perceived to be in a recovery period, the window remains pinned at two packets — ensuring the pipe drains completely on the next ACK and restarting the cycle.</p></li></ol><p>And this loop repeats for thousands of cycles until the accumulation of small deviations — from scheduler jitter and ACK processing variance — lets the &lt;= boundary in <code>in_congestion_recovery()</code> slip behind the next packet's send time, breaking the cycle.</p>
    <div>
      <h2>The fix: measuring idle from the right moment</h2>
      <a href="#the-fix-measuring-idle-from-the-right-moment">
        
      </a>
    </div>
    <p>Fixing the death spiral involves measuring the idle duration from when bytes_in_flight actually transitioned to zero (the last ACK processed) rather than the last packet sent.</p>
    <div>
      <h3>The code change</h3>
      <a href="#the-code-change">
        
      </a>
    </div>
    <ol><li><p>Add last_ack_time timestamp to the CUBIC state.</p></li><li><p>Update that timestamp when ACKs arrive.</p></li><li><p>Use it for the idle delta computation:</p></li></ol>
            <pre><code>// cubic.rs — on_packet_sent()
fn on_packet_sent(&amp;mut self, bytes_in_flight: usize, now: Instant, ...) {
    // Check if the connection was idle before this packet was sent.
    if bytes_in_flight == 0 {
        if let Some(recovery_start_time) = r.congestion_recovery_start_time {
            // Measure idle from the most recent activity: either the
            // last ACK (approximating when bif hit 0) or the last data
            // send, whichever is later. Using last_sent_time alone
            // would inflate the delta by a full RTT when cwnd is small
            // and bif transiently hits 0 between ACK and send.
            let idle_start = cmp::max(cubic.last_ack_time, cubic.last_sent_time);

            if let Some(idle_start) = idle_start {
                if idle_start &lt; now {
                    let delta = now - idle_start;
                    r.congestion_recovery_start_time =
                        Some(recovery_start_time + delta);
                }
            }
        }
}</code></pre>
            <p>With the delta now reflecting the actual gap since the last ACK, the recovery boundary stops chasing the send time:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6iJxCyk6eHvhxg13Z1XqQT/835dfb9b2b545362b39ec95041549333/BLOG-3273_image2.png" />
          </figure><p><sup><i>Old code: boundary advances one RTT per cycle, always landing on or ahead of the next send.</i></sup></p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3c3v2ZqsyLXzaoEJewZ8hj/c38dc57aa604c1a1dea1bcf2fdba1ed5/BLOG-3273_image1.png" />
          </figure><p><sup><i>Fix: boundary barely moves; the next send lands ahead of it and cwnd grows.</i></sup></p><p>For genuinely idle connections, <code>last_ack_time</code> is far in the past and the same expression captures the full idle duration, the original epoch-shift behavior is preserved.</p>
    <div>
      <h2>Validation</h2>
      <a href="#validation">
        
      </a>
    </div>
    <p>With the fix applied, the 100% pass rate of our quiche testing suite was restored.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/75Inh0VXIcfrw4Z7GbcAyw/64de17e59bda183dd65078c90d7e123d/BLOG-3273_image11.png" />
          </figure><p><sup><i>After the fix, cwnd grows along the expected CUBIC curve and the download completes in ~4-5 seconds.</i></sup></p><p>We don't worry about the losses at the end of the connection — that's expected because we fully utilized the router's allocated buffer. In other words, we are fully utilizing the available bandwidth in this test case.</p>
    <div>
      <h2>Takeaways</h2>
      <a href="#takeaways">
        
      </a>
    </div>
    <ul><li><p><b>"Idle" is harder to define than it sounds.</b> Normal pipeline delays at small windows can look like idleness to simple checks.</p></li><li><p><b>Minimum-cwnd dynamics are a unique corner case.</b> The bug was invisible at high speeds and only triggered after severe loss.</p></li><li><p><b>The fix was surprisingly small compared to the complexity of the behavior.</b> After weeks of instrumenting qlogs and analyzing visualizations to find the root cause, the solution required changing just three lines of code. As we noted during the investigation: the effort to find the bug was massive, but the fix itself was basically one line of logic.</p></li></ul><p>The fix described in this post has been contributed to <code><b><u>cloudflare/quiche</u></b></code>, Cloudflare's open-source implementation of QUIC and HTTP/3. Our CCA efforts go beyond loss-based algorithms: we also use quiche’s modular congestion control design to experiment with and tune our model-based <a href="https://blog.cloudflare.com/new-standards/#congestion-control"><u>BBRv3</u></a> implementation, now enabled for a growing percentage of our QUIC deployments. Stay tuned for further updates on QUIC congestion control implementation and performance.  </p><p>If you're interested in congestion control, transport protocols, or contributing to open-source networking code, check out the <b>quiche</b> repository. We're always looking for talented engineers who love digging into problems like these, please explore our<a href="https://www.cloudflare.com/careers/"> <u>open positions</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Congestion Control]]></category>
            <category><![CDATA[Debugging]]></category>
            <category><![CDATA[QUIC]]></category>
            <category><![CDATA[QUICHE]]></category>
            <category><![CDATA[Networking]]></category>
            <category><![CDATA[HTTP3]]></category>
            <category><![CDATA[Rust]]></category>
            <guid isPermaLink="false">1zBGqAHw4u2LDu0ldcF3AM</guid>
            <dc:creator>Esteban Carisimo</dc:creator>
            <dc:creator>Antonio Vicente</dc:creator>
        </item>
        <item>
            <title><![CDATA[Post-quantum encryption for Cloudflare IPsec is generally available]]></title>
            <link>https://blog.cloudflare.com/post-quantum-ipsec/</link>
            <pubDate>Thu, 30 Apr 2026 14:00:00 GMT</pubDate>
            <description><![CDATA[ Cloudflare IPsec now has generally available support for post-quantum encryption via hybrid ML-KEM. We’ve confirmed interoperability with Cisco and Fortinet. ]]></description>
            <content:encoded><![CDATA[ <p>While more than <a href="https://radar.cloudflare.com/post-quantum"><u>two-thirds</u></a> of human-generated TLS traffic to Cloudflare is already protected by post-quantum cryptography, the world of site-to-site networking has been a different story. For years, the IPsec community remained caught between the high bar of Internet-scale interoperability and the niche requirements of specialized hardware. That gap is now closing. </p><p>Earlier this month, we announced that Cloudflare has moved its target for full post-quantum security <a href="https://blog.cloudflare.com/post-quantum-roadmap/"><u>forward to 2029</u></a>, spurred by several recent advances in quantum computing. To advance that goal, we’ve made post-quantum encryption in Cloudflare IPsec generally available.</p><p>Using the new IETF draft for hybrid ML-KEM (<a href="https://csrc.nist.gov/pubs/fips/203/final"><u>FIPS 203</u></a>), we’ve successfully tested interoperability with branch connectors from <a href="https://docs.fortinet.com/document/fortigate/7.6.6/fortios-release-notes/760203/introduction-and-supported-models"><u>Fortinet</u></a> and <a href="https://www.cisco.com/site/us/en/products/networking/sdwan-routers/8000-series/index.html"><u>Cisco</u></a> — meaning you can start protecting your wide-area network (WAN) against harvest-now-decrypt-later attacks today using hardware you already have.</p><p>This post explains how we implemented the new hybrid IPsec handshake, why it took four years longer to land than its TLS counterpart, and how the industry is finally consolidating around a standard that works at Internet scale.</p>
    <div>
      <h3>Cloudflare IPsec</h3>
      <a href="#cloudflare-ipsec">
        
      </a>
    </div>
    <p><a href="https://developers.cloudflare.com/magic-wan/reference/gre-ipsec-tunnels/"><u>Cloudflare IPsec</u></a> is a <a href="https://www.cloudflare.com/learning/network-layer/network-as-a-service-naas/"><u>WAN Network-as-a-Service</u></a> that replaces legacy network architectures by connecting data centers, branch offices, and cloud VPCs to Cloudflare's global <a href="https://www.cloudflare.com/learning/cdn/glossary/anycast-network"><u>IP Anycast</u></a> network. Customers get simplified configuration, high availability (if a data center becomes unavailable, traffic is automatically rerouted to the nearest healthy one), and the scale of Cloudflare's global network. This is done through encrypted IPsec tunnels that support both site-to-site WAN, outbound Internet connections, and connectivity to the <a href="https://www.cloudflare.com/sase/"><u>Cloudflare One SASE platform</u></a>. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1y7QgyWMG5xGTtYGcwyqTX/48b625d0178c746dbdfba70ee1d2e259/image1.png" />
          </figure>
    <div>
      <h3>Post-quantum encryption in IPsec</h3>
      <a href="#post-quantum-encryption-in-ipsec">
        
      </a>
    </div>
    <p>Cloudflare IPsec now uses post-quantum encryption with hybrid ML-KEM (<a href="https://csrc.nist.gov/pubs/fips/203/final"><u>FIPS 203</u></a>) to stop harvest-now-decrypt-later attacks. These are attacks where an adversary harvests data today and then decrypts later, after Q-Day, when there are powerful quantum computers that can break the classical public key cryptography used across the Internet.  Harvest-now-decrypt-later attacks are becoming a concern for more organizations as <a href="https://blog.cloudflare.com/post-quantum-roadmap/"><u>Q-Day approaches faster than expected</u></a>.</p><p>ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism) is a post-quantum cryptography algorithm that is based on mathematical assumptions that are not known to be vulnerable to attacks by quantum computers. It does not require special hardware or a dedicated physical link between sender and receiver. ML-KEM is intentionally designed to be implemented in software across standard processors to provide post-quantum encryption of network traffic. </p><p><a href="https://datatracker.ietf.org/doc/draft-ietf-ipsecme-ikev2-mlkem/"><u>Draft-ietf-ipsecme-ikev2-mlkem</u></a> specifies post-quantum encryption for IPsec using <i>hybrid </i>ML-KEM, which combines the well-understood security of classical Diffie-Hellman and the post-quantum security of ML-KEM in a single, standards-compliant handshake. Specifically, a classical Diffie-Hellman exchange runs first, its derived key encrypts a second exchange that runs ML-KEM, and the outputs of both are mixed into the session keys that secure IPsec data plane traffic sent using the Encapsulating Security Payload (ESP) protocol. </p>
    <div>
      <h3>Our interoperable implementation </h3>
      <a href="#our-interoperable-implementation">
        
      </a>
    </div>
    <p>Earlier we announced <a href="https://blog.cloudflare.com/post-quantum-sase/"><u>the closed beta</u></a> of our implementation of draft-ietf-ipsecme-ikev2-mlkem in production in our Cloudflare IPsec product and tested it against a reference implementation (strongswan). Now that we have made this implementation generally available, we have also confirmed interoperability with several other vendors, including Cisco and Fortinet, which is a big win for this new standard.</p><p><b>Cisco: </b>Customers using <a href="https://www.cisco.com/c/en/us/td/docs/routers/secure-routers/cisco-8000-series-secure-routers-release-26-1-x.html"><u>Cisco 8000 Series Secure Routers after version 26.1.1</u></a> as their branch connector can also now establish post-quantum Cloudflare IPsec tunnels per draft-ietf-ipsecme-ikev2-mlkem.</p><p><b>Fortinet: </b>Customers using <a href="https://docs.fortinet.com/document/fortigate/7.6.6/fortios-release-notes/760203/introduction-and-supported-models"><u>Fortinet FortiOS 7.6.6 and later</u></a> as their branch connector can now establish post-quantum Cloudflare IPsec tunnels to Cloudflare's global network per draft-ietf-ipsecme-ikev2-mlkem.</p>
    <div>
      <h3>The importance of being interoperable</h3>
      <a href="#the-importance-of-being-interoperable">
        
      </a>
    </div>
    <p>Given that upgrading cryptography is hard and can take years, our 2029 target date for a full update to post-quantum cryptography is going to require concentrated effort. That’s why we hope the IPsec community continues to focus on the development of interoperable standards like draft-ietf-ipsecme-ikev2-mlkem.</p><p>Let us explain why these standards are vitally important. A full specification for hybrid ML-KEM in IPsec, draft-ietf-ipsecme-ikev2-mlkem, became available only in late 2025. That's roughly four years after support for hybrid ML-KEM landed in TLS. (In fact, Cloudflare <a href="https://blog.cloudflare.com/post-quantum-for-all/"><u>turned on</u></a> hybrid post-quantum key agreement with TLS in 2022, even before NIST finalized the standardization of ML-KEM, because the TLS community quickly converged on a single, interoperable approach and pushed it into production. Today more than <a href="https://radar.cloudflare.com/adoption-and-usage#post-quantum-encryption"><u>two-thirds</u></a> of the human-generated TLS traffic to Cloudflare's network is protected with hybrid ML-KEM.)</p><p>The four-year delay is likely due in part to the IPsec community's continued interest in Quantum Key Distribution (QKD), as codified in <a href="https://datatracker.ietf.org/doc/html/rfc8784"><u>RFC 8784</u></a>, published in 2020. We've <a href="https://blog.cloudflare.com/you-dont-need-quantum-hardware"><u>written before</u></a> about why QKD is not part of our post-quantum strategy: QKD requires specialized hardware and a dedicated physical link between the two parties, which fundamentally means it will not operate at Internet scale. Also, QKD does not provide authentication, so you still need post-quantum cryptography anyway to stop active attackers. It’s difficult to find implementations of QKD that interoperate across vendors.   </p><p>The U.S. <a href="https://www.nsa.gov/Cybersecurity/Quantum-Key-Distribution-QKD-and-Quantum-Cryptography-QC/"><u>NSA</u></a>, Germany's <a href="https://www.bsi.bund.de/EN/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Quantentechnologien-und-Post-Quanten-Kryptografie/quantentechnologien-und-post-quanten-kryptografie_node.html"><u>BSI</u></a>, and the UK's <a href="https://www.ncsc.gov.uk/whitepaper/quantum-security-technologies"><u>NCSC</u></a> have all warned against solely relying on QKD. Post-quantum cryptography, by contrast, runs on the hardware you already have, authenticates the parties at both ends, and works end-to-end across the Internet. </p><p>RFC 9370, published in 2023, opened the door to post-quantum cryptography in IPsec, allowing up to seven key exchanges to be run in parallel with classical Diffie-Hellman. However, RFC 9370 did not specify which ciphersuites should be used in these parallel key exchanges. In the absence of that specification, some vendors shipped early implementations under RFC 9370 before the hybrid ML-KEM draft was available, defining their own ciphersuites including some which are not NIST-standardized. This is exactly the kind of “ciphersuite bloat” <a href="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf"><u>NIST SP 800 52r2</u></a> warned against. And the risks to interoperability have played out in practice: Cloudflare IPsec does not yet interoperate with Palo Alto Networks' RFC 9370–based implementation, because it was launched before draft-ietf-ipsecme-ikev2-mlkem was available. </p><p>Fortunately, we now have draft-ietf-ipsecme-ikev2-mlkem that fills in the gaps in RFC 9370, specifying hybrid ML-KEM as one of the key exchange mechanisms that can be operated in parallel with classical Diffie-Hellman. We hope to add Palo Alto Networks to the list of interoperable post-quantum branch connectors as the industry continues to consolidate around draft-ietf-ipsecme-ikev2-mlkem.</p><p>But the journey towards interoperable post-quantum IPsec standards is not over yet. While draft-ietf-ipsecme-ikev2-mlkem supports post-quantum <i>encryption</i>, we still need IPsec standards for post-quantum <i>authentication, </i>so that we can stop attacks by quantum adversaries on live systems after Q-Day. Given the shortened timeline for full post-quantum readiness, we hope the IPsec community will continue to focus on interoperable PQC implementations, rather than diverting focus to niche use cases with QKD.</p>
    <div>
      <h3>Towards an interoperable post-quantum Internet</h3>
      <a href="#towards-an-interoperable-post-quantum-internet">
        
      </a>
    </div>
    <p>At Cloudflare, we’re helping make a secure and post-quantum Internet accessible to everyone, without <a href="https://blog.cloudflare.com/you-dont-need-quantum-hardware/"><u>specialized hardware</u></a> and at <a href="https://blog.cloudflare.com/post-quantum-crypto-should-be-free/"><u>no extra cost to our customers</u></a>. Post-quantum Cloudflare IPsec is one more step on our path to <a href="https://blog.cloudflare.com/post-quantum-roadmap/"><u>full post-quantum security by 2029</u></a>, and we’re doing it in a way that ensures that the Internet remains open and interoperable for years to come. </p> ]]></content:encoded>
            <category><![CDATA[Post-Quantum]]></category>
            <category><![CDATA[IPsec]]></category>
            <category><![CDATA[Cryptography]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Magic WAN]]></category>
            <category><![CDATA[Networking]]></category>
            <guid isPermaLink="false">45CJNnEddFGu89vwZzAJEl</guid>
            <dc:creator>Sharon Goldberg</dc:creator>
            <dc:creator>Amos Paul</dc:creator>
        </item>
    </channel>
</rss>