<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Colitu VPN</title>
    <description>The latest articles on DEV Community by Colitu VPN (@colitu).</description>
    <link>https://dev.to/colitu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172267%2Fd7015862-7cc1-4b14-a02a-a8ea05d9f033.png</url>
      <title>DEV Community: Colitu VPN</title>
      <link>https://dev.to/colitu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/colitu"/>
    <language>en</language>
    <item>
      <title>Gamifying a VPN without breaking privacy: how we built Colitu Passport</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:23:01 +0000</pubDate>
      <link>https://dev.to/colitu/gamifying-a-vpn-without-breaking-privacy-how-we-built-colitu-passport-14gn</link>
      <guid>https://dev.to/colitu/gamifying-a-vpn-without-breaking-privacy-how-we-built-colitu-passport-14gn</guid>
      <description>&lt;p&gt;Most VPN apps look the same: there's a big connect button, a list of countries and a speed number. The product works well when you never think about it, and that's a problem for us as a VPN team. Users forget you exist until something breaks.&lt;/p&gt;

&lt;p&gt;We wanted to try something different. We didn't want another dashboard or another speed graph. We wanted a reason to &lt;em&gt;enjoy&lt;/em&gt; opening the app.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Colitu Passport&lt;/strong&gt;: every country you connect to becomes a stamp in a personal passport. You can level stamps up, complete collections and compare passports with friends.&lt;/p&gt;

&lt;p&gt;Gamification and privacy usually pull in opposite directions, though. Most engagement systems run on detailed behavioral data, and a VPN is the last product that should collect that. This post covers how we tried to build one without the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core loop
&lt;/h2&gt;

&lt;p&gt;The rules are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unlock:&lt;/strong&gt; stay connected to a country for &lt;strong&gt;5 minutes&lt;/strong&gt; and you earn its bronze stamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress:&lt;/strong&gt; while the VPN is on, you earn a puzzle piece at the &lt;strong&gt;10th minute&lt;/strong&gt; and another at the &lt;strong&gt;60th minute&lt;/strong&gt; of the day. That's a maximum of &lt;strong&gt;2 pieces per day&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level up:&lt;/strong&gt; each tier costs &lt;strong&gt;12 pieces&lt;/strong&gt;. The tiers are Bronze, Silver (12), Gold (24) and Diamond (36).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math is intentional. At 2 pieces per day, a single level takes at least &lt;strong&gt;6 days&lt;/strong&gt;. There's no way to grind faster and no way to pay to skip ahead. Progress is tied to calendar days, not hours spent online.&lt;/p&gt;

&lt;p&gt;That cap matters for a VPN in particular. We didn't want a system that rewards leaving the tunnel on 24/7 "for the points." Two pieces a day, earned early, means a normal day of use is enough to progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure: chapters and collections
&lt;/h2&gt;

&lt;p&gt;Stamps are organized on two axes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chapters&lt;/strong&gt; give the passport a progression arc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Origin:&lt;/strong&gt; the first 10 countries (Sweden, Germany, Japan, the US and others).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizons:&lt;/strong&gt; 10 more (Norway, Canada, Turkey, Australia and others).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy:&lt;/strong&gt; the mastery layer. Get every stamp to gold, then every stamp to diamond.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Collections&lt;/strong&gt; group countries by theme: &lt;em&gt;Nordic&lt;/em&gt;, &lt;em&gt;Baltic&lt;/em&gt;, &lt;em&gt;Across the Atlantic&lt;/em&gt;, &lt;em&gt;Heart of Europe&lt;/em&gt; and &lt;em&gt;Pacific Rim&lt;/em&gt;. Completing one unlocks its cover and seal. There's also one hidden collection, which we're not going to explain here.&lt;/p&gt;

&lt;p&gt;On top of that come weekly &lt;strong&gt;expeditions&lt;/strong&gt; (three small goals per week), &lt;strong&gt;achievements and titles&lt;/strong&gt;, selectable &lt;strong&gt;passport covers&lt;/strong&gt; and &lt;strong&gt;app icons&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule #1: count minutes on the server, not the client
&lt;/h2&gt;

&lt;p&gt;The first technical decision was where progress gets measured.&lt;/p&gt;

&lt;p&gt;If the app counted connection time, the system would be trivially gameable. You could patch the client, fake the clock or replay a request. Trusting the client for rewards is a well-known anti-pattern in game development.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;connection minutes are counted by Colitu's servers&lt;/strong&gt;, not by the apps. The VPN server already knows a tunnel is up, so it's the natural source of truth. The client only displays what the server has already decided.&lt;/p&gt;

&lt;p&gt;This also gives us &lt;strong&gt;cross-platform consistency for free&lt;/strong&gt;. The passport lives in the account, not on the device. Connect from your phone in the morning and your laptop at night, and the progress lands in the same passport. The feature ships on Android, iOS, Windows and Linux at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule #2: store totals, never sessions
&lt;/h2&gt;

&lt;p&gt;This is where a VPN differs from every other gamified app.&lt;/p&gt;

&lt;p&gt;A typical engagement system logs events like &lt;em&gt;user X connected to server Y at 14:02 for 37 minutes&lt;/em&gt;. That's exactly the kind of record a privacy-focused VPN should never create, even with good intentions.&lt;/p&gt;

&lt;p&gt;So the passport keeps &lt;strong&gt;only aggregate counters&lt;/strong&gt;. There's no per-minute record and no per-session log. Conceptually, the stored state looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;passport
├── stamps:   { country → tier, pieces }
├── today:    { pieces_earned: 0..2 }
└── profile:  { nickname, collector_no, title, cover }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;strong&gt;not&lt;/strong&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;connections:&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="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;duration&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="err"&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 class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;don't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is that totals tell you &lt;em&gt;how far someone has progressed&lt;/em&gt;. They don't tell you &lt;em&gt;when or how they used the VPN&lt;/em&gt;. You can't rebuild a timeline from "Germany: Silver, 7 pieces."&lt;/p&gt;

&lt;p&gt;When you delete your account, &lt;strong&gt;the passport is deleted with it&lt;/strong&gt;. There's no separate archive and no "anonymized" copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule #3: no dark patterns
&lt;/h2&gt;

&lt;p&gt;Gamification has a bad reputation, and it's mostly earned. Loot boxes, streak anxiety and pay-to-win make the experience worse for users. We wrote down what we would &lt;em&gt;not&lt;/em&gt; build before we wrote any code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No randomness you can pay for.&lt;/strong&gt; There are no loot boxes and no random paid rewards. Every reward is deterministic: you know exactly what you'll get and when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No streak punishment.&lt;/strong&gt; Skip a day, a week or a month and you lose nothing. There are no penalties and no broken streaks. Progress simply waits for you. We think a VPN should never make you feel guilty for not using it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No paywall on collecting.&lt;/strong&gt; The free plan can collect &lt;strong&gt;every stamp&lt;/strong&gt;. Paid plans only change the passport cover color. That's a cosmetic difference, not a progression advantage.&lt;/p&gt;

&lt;p&gt;These rules shaped the architecture as much as the UX. For example, a daily cap of 2 pieces plus no streak penalties means the system needs very little state. There's no streak counter, no "last active" timestamp to punish and no decay job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Social without a social graph
&lt;/h2&gt;

&lt;p&gt;We wanted people to be able to compare passports, but we didn't want to build a friend list, follower system or leaderboard. Each of those adds a social graph that has to be stored and protected.&lt;/p&gt;

&lt;p&gt;So comparison works &lt;strong&gt;in person, via QR code&lt;/strong&gt;. Your app shows a QR code that encodes a summary of your passport. A friend scans it, and the app shows the two passports side by side.&lt;/p&gt;

&lt;p&gt;No friend relationship is stored. There's no lookup by username and no public profile. The comparison happens at the moment of the scan, between two people standing next to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual language
&lt;/h2&gt;

&lt;p&gt;Each stamp is a small illustration of a landmark: Helsinki Cathedral for Finland, the Brandenburg Gate for Germany and so on. Each one is rendered in the material of its tier. Bronze, silver and gold are metallic finishes, and diamond turns the whole stamp into faceted crystal. Collection seals use a more classic, engraved, banknote-style treatment.&lt;/p&gt;

&lt;p&gt;The goal was for the passport to feel like an object worth keeping, not like a badge system attached to a settings screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;A few takeaways for anyone adding engagement mechanics to a privacy-sensitive product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide what you won't store before you decide what to show.&lt;/strong&gt; Our data model came from the privacy constraint, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side counting solves cheating and sync at once.&lt;/strong&gt; If the server is already the source of truth, use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily caps beat open-ended grinding.&lt;/strong&gt; They respect users' time and keep the system cheap to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing punishment removes state.&lt;/strong&gt; No streaks meant less data, less code and less anxiety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social features don't require a social graph.&lt;/strong&gt; A QR code exchanged in person can replace a whole friend system.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Colitu Passport is live on Android, iOS, Windows and Linux. Connect to a country, wait five minutes and you'll have your first stamp.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://colitu.com/en/passport" rel="noopener noreferrer"&gt;colitu.com/en/passport&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'd love to hear how others approach gamification in privacy-focused products. What would you add, and what would you never build?&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>privacy</category>
      <category>gamification</category>
      <category>productdesign</category>
    </item>
    <item>
      <title>Our Hysteria2 connections died after every upload speed test. The fix was 37 lines.</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Thu, 15 Oct 2026 23:46:44 +0000</pubDate>
      <link>https://dev.to/colitu/our-hysteria2-connections-died-after-every-upload-speed-test-the-fix-was-37-lines-2n81</link>
      <guid>https://dev.to/colitu/our-hysteria2-connections-died-after-every-upload-speed-test-the-fix-was-37-lines-2n81</guid>
      <description>&lt;p&gt;We build &lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;Colitu&lt;/a&gt;, a VPN with open-source apps for &lt;a href="https://github.com/colitu/android" rel="noopener noreferrer"&gt;Android&lt;/a&gt;, &lt;a href="https://github.com/colitu/ios" rel="noopener noreferrer"&gt;iOS&lt;/a&gt;, &lt;a href="https://github.com/colitu/windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;, &lt;a href="https://github.com/colitu/linux" rel="noopener noreferrer"&gt;Linux&lt;/a&gt; and a &lt;a href="https://github.com/colitu/extension" rel="noopener noreferrer"&gt;browser extension&lt;/a&gt;. The apps carry traffic over several transports. Hysteria2 is a QUIC-based one that our apps prefer on many networks, because it holds up well on lossy mobile links.&lt;/p&gt;

&lt;p&gt;The apps also check their own connection. Every 5 seconds they send a small request through the tunnel. If three checks in a row get no answer within 4 seconds, the transport is declared dead, and traffic moves to a TCP spare that is already connected. Users stay online. But a transport that keeps getting declared dead is a bug, even if nobody notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we noticed
&lt;/h2&gt;

&lt;p&gt;We were testing &lt;strong&gt;port hopping&lt;/strong&gt; for Hysteria2 on an Android phone on home Wi-Fi. With port hopping, the client moves to a new UDP port every ~30 seconds, so networks that slow down long-running UDP flows have a harder time doing it.&lt;/p&gt;

&lt;p&gt;Hopping itself worked. We saw five hops, and the server's replies arrived on every new port.&lt;/p&gt;

&lt;p&gt;Then we ran a speed test. Hysteria2 carried traffic for about &lt;strong&gt;50–60 seconds and then stopped for good&lt;/strong&gt;. The health check gave up and traffic went to the spare. It happened every time.&lt;/p&gt;

&lt;p&gt;We repeated the test on a server &lt;strong&gt;without&lt;/strong&gt; port hopping and got the same result. So hopping was not the cause; it was just the test that showed the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the packets said
&lt;/h2&gt;

&lt;p&gt;We captured traffic on the phone. After the stall:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the QUIC connection was &lt;strong&gt;alive&lt;/strong&gt;: ACKs and small control packets went both ways;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP datagrams still flowed&lt;/strong&gt;: DNS and the apps' own QUIC traffic, which Hysteria carries as datagrams;&lt;/li&gt;
&lt;li&gt;the client sent &lt;strong&gt;no new TCP stream data at all&lt;/strong&gt;, not even for brand-new requests;&lt;/li&gt;
&lt;li&gt;the server logged nothing unusual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A connection that is alive but will not send stream data points at &lt;strong&gt;flow control&lt;/strong&gt;, not at the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we ruled out first
&lt;/h2&gt;

&lt;p&gt;We tested each of these instead of assuming:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suspect&lt;/th&gt;
&lt;th&gt;How we tested it&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ISP shaping UDP&lt;/td&gt;
&lt;td&gt;read the capture&lt;/td&gt;
&lt;td&gt;packets kept flowing both ways; the &lt;em&gt;client&lt;/em&gt; stopped sending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe starvation under load&lt;/td&gt;
&lt;td&gt;watched after the load ended&lt;/td&gt;
&lt;td&gt;the stall lasted for minutes after the load was gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Congestion control&lt;/td&gt;
&lt;td&gt;A/B build with Reno instead of BBR&lt;/td&gt;
&lt;td&gt;died the same way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xray's "Chrome parrot" QUIC mode&lt;/td&gt;
&lt;td&gt;A/B build with it disabled&lt;/td&gt;
&lt;td&gt;died the same way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream limit&lt;/td&gt;
&lt;td&gt;counted streams&lt;/td&gt;
&lt;td&gt;~90 open against a limit of 1,024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many connections in general&lt;/td&gt;
&lt;td&gt;1,400 short sequential connections&lt;/td&gt;
&lt;td&gt;all fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One more thing we tried: a &lt;strong&gt;bigger flow-control window&lt;/strong&gt; (64 MB per connection, 4 MB per stream). It did not fix anything. It only &lt;strong&gt;delayed&lt;/strong&gt; the stall: with 6 parallel streams it failed in round 3, with 20 streams immediately. That was the strongest hint about where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it on one PC
&lt;/h2&gt;

&lt;p&gt;To get rid of the network entirely, we built the official Hysteria server &lt;strong&gt;v2.11.0 from source&lt;/strong&gt; and ran it on loopback with an &lt;strong&gt;Xray 26.10.10&lt;/strong&gt; client. A small Python harness opened parallel uploads through the client, aborted them with a TCP reset, and then checked whether new requests still worked.&lt;/p&gt;

&lt;p&gt;The only thing we changed between runs was how the upload's &lt;em&gt;destination&lt;/em&gt; behaved:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target mode&lt;/th&gt;
&lt;th&gt;What the destination does&lt;/th&gt;
&lt;th&gt;Connection stalled?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reads everything&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reads 1 MB/s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upclose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closes the connection first&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;stops reading but keeps the connection open&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes: 0 of 5 new requests worked, no recovery in 30 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An upload speed test behaves exactly like &lt;code&gt;stall&lt;/code&gt;. The test server stops reading once it has measured enough, while the browser's upload request is still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh3tvqhlxquy627eqx9l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh3tvqhlxquy627eqx9l.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;QUIC limits how much &lt;strong&gt;unread&lt;/strong&gt; data a peer may have in flight, per stream and for the whole connection. Hysteria's defaults are &lt;strong&gt;8 MB per stream&lt;/strong&gt; and &lt;strong&gt;20 MB per connection&lt;/strong&gt;. Received bytes count against both windows until the application actually reads them.&lt;/p&gt;

&lt;p&gt;Here is what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client (Xray)                 Hysteria server                 target
     |  upload on stream A  -&amp;gt;  |  io.Copy(target, stream A) -&amp;gt; |
     |                          |                               |  stops reading,
     |                          |  target's TCP buffer full     |  keeps conn open
     |                          |  write to target BLOCKS       |
     |                          |  -&amp;gt; server stops reading A    |
     |  A's unread bytes keep counting against the connection window
     |  (a few streams like A fill all 20 MB)
     |
     |  new request on stream B: blocked, no connection credit left
     |  writer for A: blocked on the same window, so the client
     |  never gets to cancel A either
     |
     |  UDP datagrams: not flow-controlled -&amp;gt; still flowing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The client uploads through a Hysteria2 stream. The server dials the target over TCP and copies the stream into it.&lt;/li&gt;
&lt;li&gt;The target stops reading but does not close. Its receive buffer fills, the server's &lt;code&gt;Write&lt;/code&gt; into the target blocks, and the server stops reading from the stream.&lt;/li&gt;
&lt;li&gt;The stream's unread upload data keeps occupying the &lt;strong&gt;connection-level&lt;/strong&gt; receive window. A few such streams use up the whole window. From then on the client cannot send on &lt;em&gt;any&lt;/em&gt; stream of the connection, including brand-new ones.&lt;/li&gt;
&lt;li&gt;The client never gives the stream up, because its own writer is blocked on the same window. On an instrumented server, the "stream given up" signal only arrived when we &lt;strong&gt;killed the client process&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;UDP datagrams are not covered by stream flow control, which is why DNS kept working and the connection looked healthy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interesting detail: we first suspected the server never calls &lt;code&gt;CancelRead&lt;/code&gt; on dead streams. That was wrong. The stream's &lt;code&gt;Close&lt;/code&gt; already does that. The real problem is that nothing ever reaches that &lt;code&gt;Close&lt;/code&gt;, because the copy into the target is stuck in a blocking &lt;code&gt;Write&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizllx662401f9x2x3o8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizllx662401f9x2x3o8m.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server cannot tell a stalled target from a slow one by looking at the stream. It &lt;strong&gt;can&lt;/strong&gt; tell by progress: a single write that makes no progress for a long time.&lt;/p&gt;

&lt;p&gt;So the patch does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every write into a TCP target gets a fresh 10-second budget.&lt;/strong&gt; If one write makes no progress for 10 s, the target is closed. The normal cleanup then closes the stream, which releases its share of the connection window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the client gives the stream up&lt;/strong&gt;, the target is closed immediately, so both copy directions end even when the target is silent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A slow target that keeps moving is never cut off, because every write starts a new timer. Only a target that has &lt;em&gt;stopped&lt;/em&gt; accepting data is closed.&lt;/p&gt;

&lt;p&gt;This is the version we sent upstream in &lt;a href="https://github.com/HyNetworks/hysteria/pull/1703" rel="noopener noreferrer"&gt;PR #1703&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCPStallTimeout&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newStallGuardConn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tConn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCPStallTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tConn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;
    &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AfterFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// stallGuardConn closes the wrapped connection when a single Write makes no&lt;/span&gt;
&lt;span class="c"&gt;// progress for timeout, and makes Close idempotent. Every Write gets a fresh&lt;/span&gt;
&lt;span class="c"&gt;// timeout, so a slow target that keeps accepting data is never closed.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;stallGuardConn&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;   &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;     &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timer&lt;/span&gt;
    &lt;span class="n"&gt;closeOnce&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&lt;/span&gt;
    &lt;span class="n"&gt;closeErr&lt;/span&gt;  &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newStallGuardConn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stallGuardConn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stallGuardConn&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AfterFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stallGuardConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stallGuardConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closeOnce&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closeErr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closeErr&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our production build (&lt;code&gt;v2.11.0-colitu1&lt;/code&gt;) does the same thing with &lt;code&gt;SetWriteDeadline&lt;/code&gt; before each write. For the upstream version we made three changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the check closes the connection from a timer, so it only needs &lt;code&gt;Close&lt;/code&gt; and works with any &lt;code&gt;net.Conn&lt;/code&gt; an outbound returns;&lt;/li&gt;
&lt;li&gt;the timeout is configurable as &lt;code&gt;server.Config.TCPStallTimeout&lt;/code&gt; (default 10 s, negative disables it);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Close&lt;/code&gt; is idempotent, because two paths can now close the target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PR also adds an &lt;strong&gt;integration test&lt;/strong&gt;. Four streams upload into targets that never read (one end of a &lt;code&gt;net.Pipe()&lt;/code&gt;), then a new request to an echo server must complete on the same connection. Without the fix it times out after 10 s. With it, it passes in about 3.5 s, with the stall timeout set to 2 s for the test.&lt;/p&gt;

&lt;p&gt;One gotcha while writing that test: with &lt;strong&gt;one&lt;/strong&gt; stalled stream it passed even without the fix. One stream can only fill its own 8 MB window, not the 20 MB connection window. It takes three or four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ij80od0qodh9lsfwoau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ij80od0qodh9lsfwoau.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the loopback lab:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Streams&lt;/th&gt;
&lt;th&gt;New requests work again after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;official v2.11.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;never&lt;/strong&gt; (&amp;gt; 30 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;patched&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.8 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;patched&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.8 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;patched&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fast&lt;/code&gt; / &lt;code&gt;slow&lt;/code&gt; / &lt;code&gt;upclose&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.0 s (no regression)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the phone, we ran &lt;strong&gt;four speed tests in a row over 160 seconds&lt;/strong&gt; on a patched server in Berlin. Hysteria2 was never declared dead. Before the patch it died at ~60 s every time, both in Berlin and on a server in Vilnius.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout without an app update
&lt;/h2&gt;

&lt;p&gt;Because the bug was on the server, the fix did not need a new app release. Our node image now builds Hysteria from the official source tag with the patch applied. We rolled it out the same night, one server at a time. Each server's VPN service restarted for about 2 seconds. Android, iOS, Windows and Linux users all got the fix at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upstream
&lt;/h2&gt;

&lt;p&gt;The bug is in the open-source Hysteria server, and its client-side half is in Xray, so every service that runs them is affected, not only ours. We reported both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/HyNetworks/hysteria/issues/1702" rel="noopener noreferrer"&gt;HyNetworks/hysteria#1702&lt;/a&gt;&lt;/strong&gt;: the bug, how to reproduce it, and everything we ruled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/HyNetworks/hysteria/pull/1703" rel="noopener noreferrer"&gt;HyNetworks/hysteria#1703&lt;/a&gt;&lt;/strong&gt;: the fix on the current development branch, with the configurable timeout and the integration test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/XTLS/Xray-core/issues/7169" rel="noopener noreferrer"&gt;XTLS/Xray-core#7169&lt;/a&gt;&lt;/strong&gt;: the client side. A client that cancels the write side of a stream it no longer needs (&lt;code&gt;CancelWrite&lt;/code&gt;, i.e. &lt;code&gt;RESET_STREAM&lt;/code&gt;) would release the window even on servers without the fix. A contributor has since traced it on Xray's main branch, reproduced the stall on loopback, and plans a &lt;code&gt;CancelWrite&lt;/code&gt;-based fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The lab runs on loopback: no packet loss, no added latency. It shows the mechanism and the effect of the patch, not real-world timings.&lt;/li&gt;
&lt;li&gt;The device result is one phone on one network, four speed tests over 160 seconds. Encouraging, not a long-term measurement.&lt;/li&gt;
&lt;li&gt;For up to 10 seconds before the timer fires, TCP over that connection can still pause. The patch makes the stall short; it does not make it zero. A client-side fix would complement it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Connected but silent" usually means flow control.&lt;/strong&gt; If ACKs and datagrams flow but stream data doesn't, look at windows before you blame the network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A bigger window that only delays a failure is a clue, not a fix.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the destination's behaviour a test variable.&lt;/strong&gt; Four target modes (&lt;code&gt;fast&lt;/code&gt;, &lt;code&gt;slow&lt;/code&gt;, &lt;code&gt;upclose&lt;/code&gt;, &lt;code&gt;stall&lt;/code&gt;) narrowed this down faster than any amount of packet reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the test that fails first.&lt;/strong&gt; Ours passed until we used enough streams to exhaust the connection window, which taught us something about the bug itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run Hysteria2 and your users see TCP die after big uploads while DNS keeps working, this is probably it. The PR is small and easy to apply to v2.11.0.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full report with setup details: &lt;a href="https://lab.colitu.com/research/hysteria2-stall-after-upload" rel="noopener noreferrer"&gt;lab.colitu.com/research/hysteria2-stall-after-upload&lt;/a&gt;. Our apps are open source at &lt;a href="https://github.com/colitu" rel="noopener noreferrer"&gt;github.com/colitu&lt;/a&gt;. Questions and corrections are welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>networking</category>
      <category>hysteria2</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We Broke Our VPN on Purpose: 11 Network Failures, 29 Servers, and What Adaptive Connect 2.0 Taught Us</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:04:37 +0000</pubDate>
      <link>https://dev.to/colitu/we-broke-our-vpn-on-purpose-11-network-failures-29-servers-and-what-adaptive-connect-20-taught-4fi3</link>
      <guid>https://dev.to/colitu/we-broke-our-vpn-on-purpose-11-network-failures-29-servers-and-what-adaptive-connect-20-taught-4fi3</guid>
      <description>&lt;p&gt;&lt;em&gt;An engineering deep dive into fault injection, silent VPN failures, protocol recovery, warm spare reliability, and the uncomfortable difference between implementing a fix and proving it works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By Colitu Engineering | October 10, 2026&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A VPN Can Be Connected and Still Be Broken
&lt;/h2&gt;

&lt;p&gt;There is a particularly frustrating kind of network failure.&lt;/p&gt;

&lt;p&gt;Your VPN application says &lt;strong&gt;Connected&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tunnel is established. The process is running. The connection has not been explicitly terminated.&lt;/p&gt;

&lt;p&gt;But websites stop loading. Messages remain undelivered. Requests time out.&lt;/p&gt;

&lt;p&gt;From the operating system's perspective, the connection may still exist. From the user's perspective, the internet is gone.&lt;/p&gt;

&lt;p&gt;This is one of the problems we are trying to solve with &lt;strong&gt;Colitu Adaptive Connect 2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern restrictive networks do not always block connections outright. Some allow the initial handshake, exchange a small amount of data, and then silently stop forwarding traffic.&lt;/p&gt;

&lt;p&gt;Others selectively interfere with UDP, terminate particular TCP connections, introduce packet loss, or make an otherwise healthy server temporarily unreachable.&lt;/p&gt;

&lt;p&gt;Supporting multiple protocols is useful, but it does not automatically solve these problems.&lt;/p&gt;

&lt;p&gt;A fallback system can select the wrong alternative. A health monitor can mistake an established socket for a working connection. A backup tunnel can silently die without being replaced.&lt;/p&gt;

&lt;p&gt;And sometimes the mechanisms designed to improve reliability become the reason recovery takes longer.&lt;/p&gt;

&lt;p&gt;We wanted to understand these failures at the level where they actually happen.&lt;/p&gt;

&lt;p&gt;So we built a fault-injection experiment around a real Android device on a restrictive residential network in Russia.&lt;/p&gt;

&lt;p&gt;We deliberately broke its network paths.&lt;/p&gt;

&lt;p&gt;Then we watched how Adaptive Connect responded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The experiment exposed two important flaws in our recovery logic. We implemented fixes, but the subsequent device test did not conclusively exercise either fix.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction is central to this report.&lt;/p&gt;

&lt;p&gt;This article explains what we tested, what failed, what we changed, and what still needs to be verified.&lt;/p&gt;

&lt;p&gt;The original research, including the raw measurements, is available at &lt;a href="https://lab.colitu.com/research/ac-regression-faultlab" rel="noopener noreferrer"&gt;Colitu Lab&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Engineering Problem: Connectivity Is Not a Boolean
&lt;/h2&gt;

&lt;p&gt;Many networking applications treat connectivity as a relatively simple state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connected&lt;/li&gt;
&lt;li&gt;Disconnected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real network behavior is considerably more complicated.&lt;/p&gt;

&lt;p&gt;A useful connection is not merely one that successfully negotiated a transport.&lt;/p&gt;

&lt;p&gt;It must continue carrying application traffic.&lt;/p&gt;

&lt;p&gt;Consider a simplified connection lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONNECTING
    |
    v
HANDSHAKE SUCCESSFUL
    |
    v
TUNNEL ESTABLISHED
    |
    v
APPLICATION TRAFFIC WORKING
    |
    v
TRANSPORT STALLS
    |
    v
SOCKET STILL APPEARS CONNECTED
    |
    v
APPLICATION TRAFFIC FAILS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dangerous transition happens between the last three states.&lt;/p&gt;

&lt;p&gt;The transport may remain established even though it is no longer useful.&lt;/p&gt;

&lt;p&gt;If the application relies on connection status alone, it may never recognize the failure promptly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The restrictive-network problem
&lt;/h3&gt;

&lt;p&gt;Our test environment exhibited a particularly interesting behavior.&lt;/p&gt;

&lt;p&gt;Several TCP-based VPN transports could establish connections successfully, but traffic stopped progressing approximately &lt;strong&gt;30–50 seconds later&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hysteria2, which uses UDP, remained functional under the observed baseline conditions.&lt;/p&gt;

&lt;p&gt;This created an unusual recovery problem.&lt;/p&gt;

&lt;p&gt;The fallback system had several transport options available, but most of them could not sustain traffic on that network.&lt;/p&gt;

&lt;p&gt;A successful handshake was therefore not sufficient evidence that a fallback would work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why adding more protocols is not enough
&lt;/h3&gt;

&lt;p&gt;Suppose a client supports four transport options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hysteria2
VLESS Reality
Trojan
VLESS XHTTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this appears to provide considerable redundancy.&lt;/p&gt;

&lt;p&gt;But imagine the following network conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hysteria2       WORKING
VLESS Reality   CONNECTS -&amp;gt; STALLS
Trojan          CONNECTS -&amp;gt; STALLS
VLESS XHTTP     CONNECTS -&amp;gt; STALLS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client has four choices, but only one is useful.&lt;/p&gt;

&lt;p&gt;If the working transport fails temporarily, blindly cycling through the other three may prolong the outage.&lt;/p&gt;

&lt;p&gt;Even worse, a cooldown mechanism might prevent the client from returning to the only transport that can actually carry traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocol diversity provides opportunities for recovery. It does not guarantee recovery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This distinction shaped our fault-lab design.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Why We Built a Fault-Injection Lab
&lt;/h2&gt;

&lt;p&gt;Traditional connectivity tests answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the client connect?&lt;/li&gt;
&lt;li&gt;Does the server respond?&lt;/li&gt;
&lt;li&gt;Can the tunnel carry a request?&lt;/li&gt;
&lt;li&gt;What is the latency?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are useful measurements.&lt;/p&gt;

&lt;p&gt;They are also insufficient for evaluating a system that claims to recover from failures.&lt;/p&gt;

&lt;p&gt;We needed answers to harder questions.&lt;/p&gt;

&lt;p&gt;What happens when the active protocol disappears?&lt;/p&gt;

&lt;p&gt;How quickly does the client detect a tunnel that has silently stopped forwarding data?&lt;/p&gt;

&lt;p&gt;Does a backup connection actually survive until it is needed?&lt;/p&gt;

&lt;p&gt;If a network restriction is removed, how quickly does the client return to a working transport?&lt;/p&gt;

&lt;p&gt;Can a recovery mechanism accidentally prevent its own recovery?&lt;/p&gt;

&lt;p&gt;These questions are difficult to answer using ordinary uptime monitoring.&lt;/p&gt;

&lt;p&gt;A server can remain healthy while a particular client's route to that server is unusable.&lt;/p&gt;

&lt;p&gt;A connection can remain established while application traffic is broken.&lt;/p&gt;

&lt;p&gt;And a failure may last only long enough to affect a user without appearing in coarse monitoring data.&lt;/p&gt;

&lt;p&gt;We therefore designed a controlled fault-injection suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experimental environment
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Colitu for Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline version&lt;/td&gt;
&lt;td&gt;2.8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test-build version&lt;/td&gt;
&lt;td&gt;2.8.0+ac2fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;VIA E30, Android 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Residential Wi-Fi in Russia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection mode&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servers in scope&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure scenarios&lt;/td&gt;
&lt;td&gt;11 in the baseline suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fault duration&lt;/td&gt;
&lt;td&gt;90–120 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic probe&lt;/td&gt;
&lt;td&gt;HTTP request through VPN every 5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurement&lt;/td&gt;
&lt;td&gt;Probe success and interruption duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test date&lt;/td&gt;
&lt;td&gt;October 10, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We selected a real device rather than relying solely on a simulated VPN client.&lt;/p&gt;

&lt;p&gt;That matters because actual devices generate background traffic, change connection states, and interact with the networking stack in ways that simplified test environments may not reproduce.&lt;/p&gt;

&lt;p&gt;As we discovered, ordinary background traffic was enough to expose one of our bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How the Fault Lab Works
&lt;/h2&gt;

&lt;p&gt;The fault injector operates through controlled firewall rules on Colitu infrastructure.&lt;/p&gt;

&lt;p&gt;For each scenario, rules are applied to the relevant servers, targeting the test connection's public IP address.&lt;/p&gt;

&lt;p&gt;Every rule is tagged so that it can be identified and removed.&lt;/p&gt;

&lt;p&gt;Server-side expiry timers provide an additional safety mechanism: faults are not intended to persist indefinitely if the controlling process fails.&lt;/p&gt;

&lt;p&gt;The test device continues sending real HTTP requests through its VPN tunnel at five-second intervals.&lt;/p&gt;

&lt;p&gt;The experiment records:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Whether each request succeeds.&lt;/li&gt;
&lt;li&gt;The longest observed interruption during the fault and its recovery window.&lt;/li&gt;
&lt;li&gt;The first successful request after fault injection begins.&lt;/li&gt;
&lt;li&gt;The time until successful traffic is observed after the fault is removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following diagram illustrates the measurement approach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ANDROID TEST DEVICE
                     |
                     |
             Adaptive Connect
                     |
                 VPN Tunnel
                     |
                     v
             COLITU SERVERS
                     |
             Fault Injector
                     |
       +-------------+-------------+
       |             |             |
   Drop UDP      Reset TCP     Drop Server
       |             |             |
       +-------------+-------------+
                     |
                     v
              HTTP PROBES
              Every 5 sec
                     |
                     v
               RESULT LOG
                     |
          +----------+----------+
          |          |          |
       Success    Outage     Recovery
         Rate     Duration      Time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fault injector and the traffic probes are conceptually separate.&lt;/p&gt;

&lt;p&gt;The injector changes network conditions.&lt;/p&gt;

&lt;p&gt;The probe system measures the resulting user-visible connectivity.&lt;/p&gt;

&lt;p&gt;This separation is important: a successful control operation does not necessarily mean that the intended application path was disrupted.&lt;/p&gt;

&lt;p&gt;The experiment must verify both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eleven failure scenarios
&lt;/h3&gt;

&lt;p&gt;The baseline suite covered the following conditions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Injected failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Block Hysteria2 on all servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Block VLESS Reality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Block Trojan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Inject TCP resets into VLESS Reality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Freeze TCP flows after 16 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Block UDP across all servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Introduce 5% packet loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Introduce 20% packet loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Attempt a selected-server outage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Cut the server carrying the active tunnel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Block VLESS XHTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The suite contains several distinct failure classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocol blocking&lt;/strong&gt; tests whether a client can recover when a transport family becomes unavailable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP resets&lt;/strong&gt; model abrupt connection termination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 16 KB freeze&lt;/strong&gt; models a more subtle situation: an initial exchange succeeds, then forwarding stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Packet loss&lt;/strong&gt; examines behavior under degraded transport conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server removal&lt;/strong&gt; tests whether the client can recover when the problem is not limited to a specific protocol.&lt;/p&gt;

&lt;p&gt;Not every injected fault is expected to interrupt active traffic.&lt;/p&gt;

&lt;p&gt;If the user is connected through Hysteria2 and Trojan is blocked, the existing connection may remain entirely unaffected.&lt;/p&gt;

&lt;p&gt;That is not a failed experiment.&lt;/p&gt;

&lt;p&gt;It is an important distinction between &lt;em&gt;injecting a fault&lt;/em&gt; and &lt;em&gt;causing an outage&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Baseline Results: Android 2.8.0
&lt;/h2&gt;

&lt;p&gt;We began with the released Android application running Adaptive Connect 2.0.&lt;/p&gt;

&lt;p&gt;The results immediately revealed areas requiring investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overall results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total HTTP probes&lt;/td&gt;
&lt;td&gt;466&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful probes&lt;/td&gt;
&lt;td&gt;397&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed probes&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe success rate&lt;/td&gt;
&lt;td&gt;85.19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest observed outage&lt;/td&gt;
&lt;td&gt;100.2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unrecovered scenarios&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A success rate of 85.19% is not particularly impressive in isolation.&lt;/p&gt;

&lt;p&gt;But this was not a normal-usage benchmark.&lt;/p&gt;

&lt;p&gt;The network was deliberately disrupted, repeatedly, using several different failure mechanisms.&lt;/p&gt;

&lt;p&gt;The important question was not simply how many requests succeeded.&lt;/p&gt;

&lt;p&gt;It was &lt;strong&gt;why particular interruptions lasted as long as they did&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Outage breakdown
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injected failure&lt;/th&gt;
&lt;th&gt;Longest observed outage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hysteria2 blocked globally&lt;/td&gt;
&lt;td&gt;95.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLESS Reality blocked&lt;/td&gt;
&lt;td&gt;25.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trojan blocked&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLESS Reality TCP resets&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCP frozen after 16 KB&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UDP blocked globally&lt;/td&gt;
&lt;td&gt;100.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5% packet loss&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20% packet loss&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First selected-server scenario&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active server removed&lt;/td&gt;
&lt;td&gt;45.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLESS XHTTP blocked&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The zero-second entries require careful interpretation.&lt;/p&gt;

&lt;p&gt;They indicate that the measurement did not observe a sustained interruption under the corresponding fault.&lt;/p&gt;

&lt;p&gt;They do not establish that a protocol or application is immune to that class of failure.&lt;/p&gt;

&lt;p&gt;With five-second probes, sufficiently short interruptions can also be missed.&lt;/p&gt;

&lt;p&gt;Two failures stood out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unusually slow return after UDP became available again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prolonged interruption when the active server disappeared.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We investigated both.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Failure #1: A Cooldown That Delayed Recovery
&lt;/h2&gt;

&lt;p&gt;One of the most revealing results came from the global UDP block.&lt;/p&gt;

&lt;p&gt;The injected fault lasted 120 seconds.&lt;/p&gt;

&lt;p&gt;The longest observed interruption was approximately 100 seconds.&lt;/p&gt;

&lt;p&gt;More importantly, after the fault was removed, traffic did not resume immediately.&lt;/p&gt;

&lt;p&gt;Recovery took approximately &lt;strong&gt;31 additional seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This was particularly interesting because Hysteria2 had previously been the transport that worked on the network.&lt;/p&gt;

&lt;p&gt;Why was the application not returning to a known-good transport promptly?&lt;/p&gt;

&lt;h3&gt;
  
  
  The ten-minute penalty
&lt;/h3&gt;

&lt;p&gt;When Hysteria2 stalled in the middle of an established session, Adaptive Connect placed the transport at the back of that server's candidate list for ten minutes.&lt;/p&gt;

&lt;p&gt;The intention was reasonable.&lt;/p&gt;

&lt;p&gt;If a transport fails, avoid repeatedly selecting it and entering an endless failure loop.&lt;/p&gt;

&lt;p&gt;However, the observed network conditions made that policy problematic.&lt;/p&gt;

&lt;p&gt;Hysteria2 had been the only transport capable of sustaining traffic in the original environment.&lt;/p&gt;

&lt;p&gt;The alternative TCP transports could establish connections, but subsequently stalled.&lt;/p&gt;

&lt;p&gt;The client therefore spent time exploring alternatives that could not reliably carry traffic.&lt;/p&gt;

&lt;p&gt;Meanwhile, the transport that had demonstrated actual connectivity remained penalized.&lt;/p&gt;

&lt;h3&gt;
  
  
  When failure memory becomes harmful
&lt;/h3&gt;

&lt;p&gt;This illustrates a broader networking problem.&lt;/p&gt;

&lt;p&gt;A recovery system needs memory.&lt;/p&gt;

&lt;p&gt;Without it, the client may repeatedly choose the same failing route.&lt;/p&gt;

&lt;p&gt;But memory can become harmful when it does not distinguish between different kinds of failure.&lt;/p&gt;

&lt;p&gt;Consider the difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A transport that has never successfully worked on the current network.&lt;/li&gt;
&lt;li&gt;A transport that previously worked and then failed during a temporary restriction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These situations should not necessarily receive identical penalties.&lt;/p&gt;

&lt;p&gt;The first may indicate a fundamentally unsuitable route.&lt;/p&gt;

&lt;p&gt;The second may indicate a temporary condition that has already disappeared.&lt;/p&gt;

&lt;p&gt;Treating both equally can delay recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  The additional missed-probe problem
&lt;/h3&gt;

&lt;p&gt;The logs exposed a second timing issue.&lt;/p&gt;

&lt;p&gt;Adaptive Connect enforced a 60-second minimum interval between automatic switches.&lt;/p&gt;

&lt;p&gt;That interval was intended to avoid excessive switching.&lt;/p&gt;

&lt;p&gt;However, the missed-probe counter was being reset before the minimum interval was evaluated.&lt;/p&gt;

&lt;p&gt;The consequence was subtle.&lt;/p&gt;

&lt;p&gt;A tunnel could already be demonstrably broken, yet the client would wait for the switching interval.&lt;/p&gt;

&lt;p&gt;After the interval ended, the client still required three more missed checks before initiating another switch.&lt;/p&gt;

&lt;p&gt;In other words, the system could spend time rediscovering a failure it already knew about.&lt;/p&gt;

&lt;h3&gt;
  
  
  The changes we implemented
&lt;/h3&gt;

&lt;p&gt;We adjusted the recovery logic in two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Shorter penalties for previously successful transports&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A transport that has already worked on the current network now receives a 90-second mid-session penalty in the test build.&lt;/p&gt;

&lt;p&gt;Other transports retain the ten-minute penalty.&lt;/p&gt;

&lt;p&gt;The intent is to preserve caution around historically unsuccessful paths while allowing previously functional transports to return sooner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Miss counters continue through the switching interval&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Missed checks are no longer discarded merely because the minimum switching interval has not yet elapsed.&lt;/p&gt;

&lt;p&gt;When that interval expires, the client can act on accumulated failure evidence rather than restarting the detection process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conceptual illustration
&lt;/h3&gt;

&lt;p&gt;The following pseudocode illustrates the policy difference. It is explanatory, not a copy of Colitu's production implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;failure_penalty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;previously_worked_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;  &lt;span class="c1"&gt;# seconds
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;  &lt;span class="c1"&gt;# seconds
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed_since_switch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consecutive_misses&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elapsed_since_switch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key idea is that &lt;code&gt;health.consecutive_misses&lt;/code&gt; remains meaningful while switching is temporarily restricted.&lt;/p&gt;

&lt;p&gt;It should not be reset simply because the application is observing a minimum transition interval.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deeper lesson
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A cooldown is not a substitute for understanding the failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recovery policy must balance the risk of retrying too aggressively against the risk of excluding the only usable path.&lt;/p&gt;

&lt;p&gt;The correct answer depends on what the transport has demonstrated on the current network.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Failure #2: Our Warm Spare Was Dead When We Needed It
&lt;/h2&gt;

&lt;p&gt;Adaptive Connect 2.0 includes a warm spare mechanism.&lt;/p&gt;

&lt;p&gt;The basic design maintains a primary path and a backup path within the running VPN connection.&lt;/p&gt;

&lt;p&gt;When the primary path stops working, traffic can move to the backup without requiring a full VPN disconnect-and-reconnect sequence.&lt;/p&gt;

&lt;p&gt;At least, that is the intended behavior.&lt;/p&gt;

&lt;p&gt;A simplified architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 DEVICE
                    |
                    v
              VPN BALANCER
                    |
          +---------+---------+
          |                   |
          v                   v
      MAIN PATH          WARM SPARE
          |                   |
          v                   v
       Server A             Server B
          |                   |
          +---------+---------+
                    |
                    v
                 INTERNET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backup provides useful redundancy only if it remains healthy.&lt;/p&gt;

&lt;p&gt;And this is where our implementation encountered a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The observed failure
&lt;/h3&gt;

&lt;p&gt;During the baseline test, we deliberately removed the server carrying the active tunnel.&lt;/p&gt;

&lt;p&gt;The client experienced a 45-second interruption.&lt;/p&gt;

&lt;p&gt;When we inspected the logs, the relevant backup path was already dead.&lt;/p&gt;

&lt;p&gt;It had not been replaced.&lt;/p&gt;

&lt;p&gt;The system had a warm spare mechanism, but it did not have a usable spare at the moment of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the spare was not replaced
&lt;/h3&gt;

&lt;p&gt;Replacing a spare requires reloading part of the underlying connection core.&lt;/p&gt;

&lt;p&gt;That operation can cause a brief interruption.&lt;/p&gt;

&lt;p&gt;We therefore designed the application to prefer an idle moment before replacing a dead spare.&lt;/p&gt;

&lt;p&gt;The original threshold required less than 10 KB of traffic over a ten-second interval.&lt;/p&gt;

&lt;p&gt;This was intended to protect active user sessions.&lt;/p&gt;

&lt;p&gt;However, the test device was not truly idle.&lt;/p&gt;

&lt;p&gt;Background synchronization, application traffic, and connectivity checks generated approximately 15–30 KB over each ten-second observation window.&lt;/p&gt;

&lt;p&gt;This was enough to keep the replacement operation deferred.&lt;/p&gt;

&lt;p&gt;And deferred.&lt;/p&gt;

&lt;p&gt;And deferred again.&lt;/p&gt;

&lt;p&gt;The application was trying to avoid disrupting traffic.&lt;/p&gt;

&lt;p&gt;As a consequence, it indefinitely postponed restoring redundancy.&lt;/p&gt;

&lt;h3&gt;
  
  
  A liveness problem hidden inside a safety mechanism
&lt;/h3&gt;

&lt;p&gt;This is a classic engineering tradeoff.&lt;/p&gt;

&lt;p&gt;We wanted to preserve two properties:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safety:&lt;/strong&gt; Avoid interrupting useful foreground traffic during backup replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liveness:&lt;/strong&gt; Eventually restore a healthy backup after the current one fails.&lt;/p&gt;

&lt;p&gt;Our original idle rule favored safety so heavily that liveness was not guaranteed.&lt;/p&gt;

&lt;p&gt;Under persistent low-volume background traffic, there might never be an eligible replacement window.&lt;/p&gt;

&lt;p&gt;The backup could remain unavailable indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our revised replacement policy
&lt;/h3&gt;

&lt;p&gt;The test build introduces a more flexible rule.&lt;/p&gt;

&lt;p&gt;Initially, the application still prefers an idle moment.&lt;/p&gt;

&lt;p&gt;However, after waiting two minutes, light traffic no longer prevents replacement.&lt;/p&gt;

&lt;p&gt;The revised threshold allows replacement below 32 KB per ten-second window.&lt;/p&gt;

&lt;p&gt;This is not a guarantee that every replacement will be invisible.&lt;/p&gt;

&lt;p&gt;It is an attempt to balance the risk of a brief maintenance disruption against the greater risk of remaining indefinitely without a working backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters beyond VPN software
&lt;/h3&gt;

&lt;p&gt;The same failure pattern can appear in many fault-tolerant systems.&lt;/p&gt;

&lt;p&gt;Imagine a database replica that is never rebuilt because the primary system is never fully idle.&lt;/p&gt;

&lt;p&gt;Or a service worker that is never replaced because the application continuously receives a small amount of traffic.&lt;/p&gt;

&lt;p&gt;Or a load balancer that indefinitely delays restoring a failed backend.&lt;/p&gt;

&lt;p&gt;In each case, a mechanism intended to reduce disruption can prevent the system from returning to a resilient state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redundancy is not only about having a backup. It is about continuously maintaining the conditions that make the backup usable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the engineering principle this failure exposed.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. A Separate Windows and Linux Finding: Network Memory Must Have Boundaries
&lt;/h2&gt;

&lt;p&gt;Adaptive Connect maintains information about transport behavior on different networks.&lt;/p&gt;

&lt;p&gt;That memory helps the application avoid repeating known failures.&lt;/p&gt;

&lt;p&gt;For example, a protocol that fails on a restrictive mobile network may still work perfectly well on home Wi-Fi.&lt;/p&gt;

&lt;p&gt;Those environments should not share the same recovery assumptions.&lt;/p&gt;

&lt;p&gt;A separate Windows investigation exposed a problem with how previous network state could influence a newly joined network.&lt;/p&gt;

&lt;p&gt;Information such as stalled transports and the last successful transport could persist into a different network context.&lt;/p&gt;

&lt;p&gt;We changed the relevant Windows and Linux source logic so that failure history from the previous network is not applied to the new one.&lt;/p&gt;

&lt;p&gt;These changes are covered by unit tests.&lt;/p&gt;

&lt;p&gt;However, they were not validated by the Android fault-injection experiment described here.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;The existence of unit tests supports confidence in specific implementation behavior.&lt;/p&gt;

&lt;p&gt;It does not establish end-to-end reliability across every operating system or network transition.&lt;/p&gt;

&lt;p&gt;A complete evaluation still requires device-level testing of Wi-Fi changes, mobile handovers, and other transitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. The Second Run Looked Dramatically Better
&lt;/h2&gt;

&lt;p&gt;After implementing the changes, we ran the suite again using a test build.&lt;/p&gt;

&lt;p&gt;The top-level results were striking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Released 2.8.0&lt;/th&gt;
&lt;th&gt;Test build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP probes&lt;/td&gt;
&lt;td&gt;466&lt;/td&gt;
&lt;td&gt;526&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful probes&lt;/td&gt;
&lt;td&gt;397&lt;/td&gt;
&lt;td&gt;518&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success rate&lt;/td&gt;
&lt;td&gt;85.19%&lt;/td&gt;
&lt;td&gt;98.48%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest outage&lt;/td&gt;
&lt;td&gt;100.2 s&lt;/td&gt;
&lt;td&gt;35.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unrecovered scenarios&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some of the individual failure results also appeared substantially better.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Released app&lt;/th&gt;
&lt;th&gt;Test build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hysteria2 globally blocked&lt;/td&gt;
&lt;td&gt;95.2 s&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLESS Reality blocked&lt;/td&gt;
&lt;td&gt;25.0 s&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UDP globally blocked&lt;/td&gt;
&lt;td&gt;100.2 s&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active server removed&lt;/td&gt;
&lt;td&gt;45.1 s&lt;/td&gt;
&lt;td&gt;35.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It would be tempting to publish this table with a headline such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our fixes increased connectivity from 85% to 98%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That would be an unsupported conclusion.&lt;/p&gt;

&lt;p&gt;And the reason is important.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two runs did not start from equivalent conditions
&lt;/h3&gt;

&lt;p&gt;During the second run, the device had settled on a different server.&lt;/p&gt;

&lt;p&gt;On that server, a TCP-based transport was functioning.&lt;/p&gt;

&lt;p&gt;The initial restrictive-network behavior was therefore not equivalent to the first run.&lt;/p&gt;

&lt;p&gt;When Hysteria2 was blocked, the device did not need Hysteria2.&lt;/p&gt;

&lt;p&gt;When UDP was blocked, the active TCP connection continued carrying traffic.&lt;/p&gt;

&lt;p&gt;These faults therefore did not recreate the condition that exposed the original cooldown problem.&lt;/p&gt;

&lt;p&gt;The improved numbers are real observations.&lt;/p&gt;

&lt;p&gt;But they are not proof that the cooldown fix caused the improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  The warm spare fix was not exercised either
&lt;/h3&gt;

&lt;p&gt;During the second run's active-server outage, the logs once again showed that the spare was unavailable.&lt;/p&gt;

&lt;p&gt;The client experienced an interruption of approximately 35 seconds.&lt;/p&gt;

&lt;p&gt;The revised spare-replacement mechanism did not get the opportunity to demonstrate the intended recovery behavior.&lt;/p&gt;

&lt;p&gt;As a result, this experiment cannot verify that the revised idle threshold solves the original problem on the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  One scenario was skipped
&lt;/h3&gt;

&lt;p&gt;The second run also did not execute every intended server-removal scenario.&lt;/p&gt;

&lt;p&gt;One additional active-server cut was skipped because the test tool could not identify the tunnel on a listed server during its capture window.&lt;/p&gt;

&lt;p&gt;That limitation is recorded in the raw result file.&lt;/p&gt;

&lt;p&gt;The second run therefore has a different number of executed fault episodes from the first run.&lt;/p&gt;

&lt;p&gt;It is not a perfectly matched before-and-after comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we can legitimately conclude
&lt;/h3&gt;

&lt;p&gt;The second run provides evidence that the test build continued functioning under the faults it actually encountered.&lt;/p&gt;

&lt;p&gt;It did not reveal a new failure in those observed conditions.&lt;/p&gt;

&lt;p&gt;However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The revised Hysteria2 recovery policy was not conclusively validated.&lt;/li&gt;
&lt;li&gt;The revised warm spare replacement policy was not conclusively validated.&lt;/li&gt;
&lt;li&gt;The two probe-success percentages cannot be attributed solely to the code changes.&lt;/li&gt;
&lt;li&gt;The second run is not a controlled A/B comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A better benchmark result is not the same thing as verified causality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is perhaps the most important methodological lesson from the experiment.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. We Also Found Problems in the Test Infrastructure
&lt;/h2&gt;

&lt;p&gt;Fault injection tests application behavior.&lt;/p&gt;

&lt;p&gt;But they also test the assumptions made by the people designing the experiments.&lt;/p&gt;

&lt;p&gt;Our first attempts exposed several problems in the testing infrastructure itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem A: Incomplete server coverage
&lt;/h3&gt;

&lt;p&gt;An early version of the suite targeted eight servers.&lt;/p&gt;

&lt;p&gt;The client eventually moved to a server outside the target list.&lt;/p&gt;

&lt;p&gt;Subsequent injected faults did not affect the active tunnel.&lt;/p&gt;

&lt;p&gt;The test tool continued working, but the experiment no longer exercised the intended application path.&lt;/p&gt;

&lt;p&gt;We expanded the target scope to all 29 servers the test device could select.&lt;/p&gt;

&lt;p&gt;We also added logging to identify which server was carrying the tunnel before each fault.&lt;/p&gt;

&lt;p&gt;This provides a basic but essential validation step:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Did the experiment actually affect the intended target?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem B: Public-IP targeting and NAT
&lt;/h3&gt;

&lt;p&gt;The firewall rules targeted the test device's public IP address.&lt;/p&gt;

&lt;p&gt;This seemed like a convenient way to isolate traffic belonging to a particular experiment.&lt;/p&gt;

&lt;p&gt;However, multiple devices on a residential network can share the same public IP through NAT.&lt;/p&gt;

&lt;p&gt;During testing, other computers in the same home also experienced connectivity interruptions.&lt;/p&gt;

&lt;p&gt;This did not imply that unrelated Colitu customers elsewhere were affected.&lt;/p&gt;

&lt;p&gt;It did reveal that public-IP isolation was not equivalent to individual-device isolation.&lt;/p&gt;

&lt;p&gt;The operational procedure was adjusted to announce fault runs in advance and move the control workstation to a server excluded from the experiment.&lt;/p&gt;

&lt;p&gt;This limitation should remain explicit in any description of test safety.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem C: Sequential fault application
&lt;/h3&gt;

&lt;p&gt;When a failure must affect many servers, sequentially applying firewall rules introduces timing differences.&lt;/p&gt;

&lt;p&gt;One server may start dropping packets before another.&lt;/p&gt;

&lt;p&gt;If an operation hangs, the entire sequence may stall.&lt;/p&gt;

&lt;p&gt;We updated the control mechanism to apply server-side changes in parallel.&lt;/p&gt;

&lt;p&gt;The tooling also supports continuing from a selected fault after interruption.&lt;/p&gt;

&lt;p&gt;This improves consistency and operational robustness.&lt;/p&gt;

&lt;p&gt;It does not eliminate every source of timing uncertainty, but it makes the injected scenarios more controlled.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. What This Experiment Taught Us About Failover Design
&lt;/h2&gt;

&lt;p&gt;Beyond the individual bugs, the experiment revealed several general principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 1: A working socket is not evidence of a working network
&lt;/h3&gt;

&lt;p&gt;Health checks should verify actual application-level progress.&lt;/p&gt;

&lt;p&gt;A handshake is useful evidence.&lt;/p&gt;

&lt;p&gt;An open socket is useful evidence.&lt;/p&gt;

&lt;p&gt;But neither establishes that payload traffic can continue flowing.&lt;/p&gt;

&lt;p&gt;This is especially important when failures are silent rather than explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 2: Recovery policy must remember context
&lt;/h3&gt;

&lt;p&gt;A route that has never worked should not necessarily be treated like one that worked successfully until a temporary outage.&lt;/p&gt;

&lt;p&gt;Failure memory must reflect observed behavior on the relevant network.&lt;/p&gt;

&lt;p&gt;Otherwise, a policy intended to prevent repeated failures may accidentally prolong them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 3: Backup availability is a continuously maintained property
&lt;/h3&gt;

&lt;p&gt;A spare path is not useful simply because it existed at some point.&lt;/p&gt;

&lt;p&gt;It must remain healthy and be replaced when necessary.&lt;/p&gt;

&lt;p&gt;Backup maintenance requires its own monitoring and eventual recovery guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 4: Avoid treating idle time as a guaranteed event
&lt;/h3&gt;

&lt;p&gt;Real devices are rarely completely quiet.&lt;/p&gt;

&lt;p&gt;Background synchronization, health checks, notifications, and system services may generate continuous low-volume traffic.&lt;/p&gt;

&lt;p&gt;Maintenance operations that require perfect idleness can suffer indefinite postponement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 5: The active network path matters more than the protocol list
&lt;/h3&gt;

&lt;p&gt;Blocking an unused protocol may have no effect on the user.&lt;/p&gt;

&lt;p&gt;Blocking the only viable transport may produce an unavoidable outage.&lt;/p&gt;

&lt;p&gt;A meaningful fault-injection report must record the active transport and server before and during each scenario.&lt;/p&gt;

&lt;p&gt;Without that context, the results can be misleading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 6: Recovery cannot create a working route where none exists
&lt;/h3&gt;

&lt;p&gt;During the baseline experiment, the network already exhibited widespread TCP transport freezes.&lt;/p&gt;

&lt;p&gt;When Hysteria2 was also unavailable, the client could temporarily have no usable route.&lt;/p&gt;

&lt;p&gt;No fallback policy can guarantee continued connectivity if every available path is unusable.&lt;/p&gt;

&lt;p&gt;The engineering objective is therefore not to promise impossible availability.&lt;/p&gt;

&lt;p&gt;It is to identify usable alternatives quickly, avoid unnecessary recovery delays, and restore traffic as soon as a viable path returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 7: A successful test result must be interpreted against its starting state
&lt;/h3&gt;

&lt;p&gt;Different network conditions can produce dramatically different results with identical software.&lt;/p&gt;

&lt;p&gt;Controlled comparisons require equivalent initial state, including the active server, active transport, relevant failure memory, and injected fault.&lt;/p&gt;

&lt;p&gt;Otherwise, a change in the environment can be mistaken for a change in software quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Designing the Next Experiment
&lt;/h2&gt;

&lt;p&gt;The immediate priority is not producing another large success-rate figure.&lt;/p&gt;

&lt;p&gt;It is verifying that the fixes actually address the failures they were designed to solve.&lt;/p&gt;

&lt;p&gt;We see several important next steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment A: Verify recovery after temporary UDP loss
&lt;/h3&gt;

&lt;p&gt;The test should deliberately begin with Hysteria2 carrying traffic.&lt;/p&gt;

&lt;p&gt;The sequence would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Establish a working Hysteria2 tunnel.

2. Verify continuous application traffic.

3. Block UDP to the relevant servers.

4. Confirm that Hysteria2 stops working.

5. Allow the client to enter its recovery process.

6. Remove the UDP restriction.

7. Measure the time until traffic resumes.

8. Inspect transport-selection and cooldown logs.

9. Repeat with the baseline and test builds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical measurement is recovery behavior after the original usable route becomes available again.&lt;/p&gt;

&lt;p&gt;We should compare the released version and revised version under equivalent conditions.&lt;/p&gt;

&lt;p&gt;Repeated trials would help distinguish a consistent improvement from ordinary network variability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment B: Verify warm spare replacement
&lt;/h3&gt;

&lt;p&gt;A separate test should target the spare-liveness problem directly.&lt;/p&gt;

&lt;p&gt;The setup must ensure that the client initially has a healthy main connection and a healthy spare.&lt;/p&gt;

&lt;p&gt;Then the experiment should deliberately make the spare unusable while the primary continues carrying traffic.&lt;/p&gt;

&lt;p&gt;After that, the client should be observed under controlled background-traffic levels.&lt;/p&gt;

&lt;p&gt;The essential questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the client recognize that the spare has failed?&lt;/li&gt;
&lt;li&gt;Does it attempt replacement?&lt;/li&gt;
&lt;li&gt;Can background traffic postpone that replacement indefinitely?&lt;/li&gt;
&lt;li&gt;Does the new threshold allow a replacement under light traffic?&lt;/li&gt;
&lt;li&gt;Is the replacement itself disruptive?&lt;/li&gt;
&lt;li&gt;Once a new spare is ready, does it carry traffic when the main path fails?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last step is crucial.&lt;/p&gt;

&lt;p&gt;A successful replacement log is not enough.&lt;/p&gt;

&lt;p&gt;The replacement must demonstrate that it can perform its intended function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment C: Improve measurement resolution
&lt;/h3&gt;

&lt;p&gt;Five-second HTTP probes provide a useful starting point.&lt;/p&gt;

&lt;p&gt;They do not capture every short-lived interruption.&lt;/p&gt;

&lt;p&gt;Future experiments could use more frequent probes and correlate them with transport-level events.&lt;/p&gt;

&lt;p&gt;Useful metrics include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells us&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failure detection latency&lt;/td&gt;
&lt;td&gt;How quickly the client identifies a broken path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover latency&lt;/td&gt;
&lt;td&gt;Time between detecting a failure and selecting an alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic restoration time&lt;/td&gt;
&lt;td&gt;When application traffic becomes usable again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup readiness&lt;/td&gt;
&lt;td&gt;Whether a healthy spare exists when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery p50 / p95 / p99&lt;/td&gt;
&lt;td&gt;Distribution of recovery times across repeated trials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe success ratio&lt;/td&gt;
&lt;td&gt;Fraction of successful application requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False-positive switches&lt;/td&gt;
&lt;td&gt;Healthy paths abandoned unnecessarily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switching frequency&lt;/td&gt;
&lt;td&gt;Whether recovery policy creates excessive oscillation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single maximum outage is useful.&lt;/p&gt;

&lt;p&gt;A distribution across repeated, controlled trials is far more informative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment D: Validate the security properties during recovery
&lt;/h3&gt;

&lt;p&gt;Connectivity is only one part of VPN reliability.&lt;/p&gt;

&lt;p&gt;A future security-focused experiment should also measure what happens when the tunnel fails.&lt;/p&gt;

&lt;p&gt;Important questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the operating system ever send protected traffic outside the VPN?&lt;/li&gt;
&lt;li&gt;Does the kill switch behave correctly during transitions?&lt;/li&gt;
&lt;li&gt;Are DNS requests handled safely during recovery?&lt;/li&gt;
&lt;li&gt;Can changing servers expose unexpected routing behavior?&lt;/li&gt;
&lt;li&gt;Do failed connections leave temporary routing or firewall inconsistencies?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These measurements are not provided by the present fault-injection dataset.&lt;/p&gt;

&lt;p&gt;They require their own test design and validation.&lt;/p&gt;

&lt;p&gt;A system should not trade away its intended privacy guarantees merely to restore connectivity more quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment E: Expand platform coverage
&lt;/h3&gt;

&lt;p&gt;The current experiment was performed on one Android 11 device.&lt;/p&gt;

&lt;p&gt;That is valuable, but narrow.&lt;/p&gt;

&lt;p&gt;A broader evaluation should eventually include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android devices with different networking stacks.&lt;/li&gt;
&lt;li&gt;Windows under Wi-Fi and Ethernet transitions.&lt;/li&gt;
&lt;li&gt;Linux under changing routes and interfaces.&lt;/li&gt;
&lt;li&gt;iOS under mobile and Wi-Fi handovers.&lt;/li&gt;
&lt;li&gt;Different network providers and filtering environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Platform-specific behavior matters.&lt;/p&gt;

&lt;p&gt;A recovery mechanism that succeeds in one operating system's networking environment may behave differently in another.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Reproducibility Is Part of the Engineering Work
&lt;/h2&gt;

&lt;p&gt;One reason we publish Colitu Lab research is to make the underlying measurements available for inspection.&lt;/p&gt;

&lt;p&gt;For this experiment, the raw baseline and test-build results are published as JSON.&lt;/p&gt;

&lt;p&gt;You can inspect the exact probe totals, fault definitions, durations, and recorded outages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lab.colitu.com/lab-data/ac-regression-v1/baseline-2.8.0.json" rel="noopener noreferrer"&gt;Android 2.8.0 baseline results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lab.colitu.com/lab-data/ac-regression-v1/fixed-test-build.json" rel="noopener noreferrer"&gt;Android test-build results&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Research and methodology:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lab.colitu.com/research/ac-regression-faultlab" rel="noopener noreferrer"&gt;Original fault-injection research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lab.colitu.com/methodology" rel="noopener noreferrer"&gt;Colitu Lab methodology&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://colitu.com/en/adaptive-connect" rel="noopener noreferrer"&gt;Adaptive Connect 2.0 technical report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/colitu/vpn-lab" rel="noopener noreferrer"&gt;Colitu VPN Lab GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GitHub repository contains broader VPN protocol experimentation resources. It should not be treated as proof that every script used in this particular Adaptive Connect regression experiment is already independently reproducible from that repository.&lt;/p&gt;

&lt;p&gt;Complete reproduction requires the experiment-specific scripts, compatible infrastructure, configuration, and documented execution procedure.&lt;/p&gt;

&lt;p&gt;Publishing result files is useful transparency.&lt;/p&gt;

&lt;p&gt;Making the complete experiment repeatable by independent engineers is a further step.&lt;/p&gt;

&lt;p&gt;Both matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. What We Are Not Claiming
&lt;/h2&gt;

&lt;p&gt;Technical reports become less useful when their conclusions extend beyond the evidence.&lt;/p&gt;

&lt;p&gt;So it is worth stating explicitly what this experiment does not establish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not claiming that Adaptive Connect delivers 98.48% availability across restrictive networks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That number describes successful HTTP probes during one specific test-build run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not claiming that the fixes reduced outages from 100 seconds to 35 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two runs did not begin from equivalent network conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not claiming that the warm spare replacement fix is verified on Android.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second device run did not exercise the intended replacement behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not claiming that eleven injected faults represent every real-world network restriction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real interference can involve more complex conditions, including latency manipulation, DNS behavior, route instability, and protocol-specific inspection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not claiming that the Windows and Linux changes have passed equivalent live-network tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The changes described here were covered by unit tests, while this particular fault lab targeted Android.&lt;/p&gt;

&lt;p&gt;Finally, the fixes described in this report were present in a test build at the time of the experiment.&lt;/p&gt;

&lt;p&gt;They should not be confused with independently verified behavior across every released Colitu client.&lt;/p&gt;

&lt;p&gt;These limitations do not make the experiment useless.&lt;/p&gt;

&lt;p&gt;They define what its results can responsibly support.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Why We Believe Testing Failure Is More Valuable Than Demonstrating Success
&lt;/h2&gt;

&lt;p&gt;It is relatively easy to demonstrate a successful VPN connection.&lt;/p&gt;

&lt;p&gt;Open the application.&lt;/p&gt;

&lt;p&gt;Select a server.&lt;/p&gt;

&lt;p&gt;Connect.&lt;/p&gt;

&lt;p&gt;Load a website.&lt;/p&gt;

&lt;p&gt;That verifies an important basic function.&lt;/p&gt;

&lt;p&gt;But it tells us very little about resilience.&lt;/p&gt;

&lt;p&gt;The harder questions only appear when something goes wrong.&lt;/p&gt;

&lt;p&gt;What if the server disappears after the connection has been established?&lt;/p&gt;

&lt;p&gt;What if a transport works for 30 seconds and then silently freezes?&lt;/p&gt;

&lt;p&gt;What if all UDP traffic is blocked?&lt;/p&gt;

&lt;p&gt;What if a backup is unavailable when it is needed?&lt;/p&gt;

&lt;p&gt;What if the network recovers, but the application's own cooldown policy delays reconnection?&lt;/p&gt;

&lt;p&gt;These are the situations in which recovery architecture is tested.&lt;/p&gt;

&lt;p&gt;And they are also the situations in which seemingly reasonable engineering assumptions can fail.&lt;/p&gt;

&lt;p&gt;Our fault lab found a ten-minute transport penalty that was poorly suited to a particular recovery scenario.&lt;/p&gt;

&lt;p&gt;It found a backup maintenance rule that could postpone replacement indefinitely.&lt;/p&gt;

&lt;p&gt;It found limitations in the test infrastructure itself.&lt;/p&gt;

&lt;p&gt;None of those findings is especially flattering when presented as a product benchmark.&lt;/p&gt;

&lt;p&gt;But they are useful engineering results.&lt;/p&gt;

&lt;p&gt;They identify specific behavior that can be changed, measured, and tested again.&lt;/p&gt;

&lt;p&gt;More importantly, they help turn vague claims about reliability into concrete, falsifiable expectations.&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The application automatically recovers from network failures.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can ask:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When Hysteria2 is the only working transport and UDP becomes available again, how quickly does the client restore application traffic?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The application maintains a backup tunnel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can ask:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After the backup fails under continuous low-volume traffic, does the client eventually replace it, and can that replacement take over when the primary path disappears?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those are questions that a serious fault-injection experiment can answer.&lt;/p&gt;

&lt;p&gt;And if it cannot answer them yet, the report should say so.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Where Adaptive Connect Goes From Here
&lt;/h2&gt;

&lt;p&gt;Adaptive Connect 2.0 is an attempt to make VPN connectivity more responsive to the conditions users actually encounter.&lt;/p&gt;

&lt;p&gt;Its mechanisms include transport selection, network-specific failure memory, application-level health monitoring, server fallback, and warm spare recovery.&lt;/p&gt;

&lt;p&gt;Each mechanism addresses a different part of the problem.&lt;/p&gt;

&lt;p&gt;Together, they create a more complex system.&lt;/p&gt;

&lt;p&gt;And complexity introduces interactions that are difficult to predict from isolated unit tests.&lt;/p&gt;

&lt;p&gt;A transport penalty interacts with failure detection.&lt;/p&gt;

&lt;p&gt;Failure detection interacts with switching intervals.&lt;/p&gt;

&lt;p&gt;Backup maintenance interacts with ordinary device traffic.&lt;/p&gt;

&lt;p&gt;Network memory interacts with interface transitions.&lt;/p&gt;

&lt;p&gt;A useful test strategy must examine those interactions rather than evaluating each component only in isolation.&lt;/p&gt;

&lt;p&gt;That is the direction we want Colitu Lab to pursue.&lt;/p&gt;

&lt;p&gt;The next meaningful milestone is not a larger marketing number.&lt;/p&gt;

&lt;p&gt;It is a controlled experiment demonstrating that the identified failure conditions are handled correctly by the revised implementation.&lt;/p&gt;

&lt;p&gt;From there, the same methodology can be extended to more devices, more networks, and more realistic combinations of faults.&lt;/p&gt;

&lt;p&gt;The long-term objective is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a usable network path exists, the client should find it, recognize when it stops working, and recover without unnecessary delay or loss of the protections users expect from a VPN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is an engineering objective, not a universal availability guarantee.&lt;/p&gt;

&lt;p&gt;And it is something we can continue testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;We began this experiment with a simple idea:&lt;/p&gt;

&lt;p&gt;Break the network deliberately and measure what happens.&lt;/p&gt;

&lt;p&gt;The baseline run produced 397 successful HTTP probes out of 466.&lt;/p&gt;

&lt;p&gt;The revised test build produced 518 successful probes out of 526.&lt;/p&gt;

&lt;p&gt;But the most useful results were not those percentages.&lt;/p&gt;

&lt;p&gt;They were the two bugs found in the recovery logic, the implementation changes made in response, and the realization that the second experiment did not actually verify those changes under equivalent conditions.&lt;/p&gt;

&lt;p&gt;That leaves us with unfinished work.&lt;/p&gt;

&lt;p&gt;The Hysteria2 recovery adjustment needs targeted device validation.&lt;/p&gt;

&lt;p&gt;The warm spare replacement policy needs a test that first kills the spare and then verifies replacement and takeover.&lt;/p&gt;

&lt;p&gt;The experiment needs stronger control over its starting state.&lt;/p&gt;

&lt;p&gt;And the measurement process can become more precise.&lt;/p&gt;

&lt;p&gt;This is what meaningful reliability engineering often looks like.&lt;/p&gt;

&lt;p&gt;Not a perfectly clean graph.&lt;/p&gt;

&lt;p&gt;Not an uninterrupted sequence of successful demonstrations.&lt;/p&gt;

&lt;p&gt;But a cycle of designing systems, breaking assumptions, examining evidence, correcting behavior, and testing again.&lt;/p&gt;

&lt;p&gt;At Colitu, we want to keep publishing that process, including the parts that do not yet work as intended.&lt;/p&gt;

&lt;p&gt;Because a VPN should not merely know how to connect.&lt;/p&gt;

&lt;p&gt;It should know when its connection has stopped being useful.&lt;/p&gt;

&lt;p&gt;And its recovery mechanisms should be tested just as aggressively as the networks they are designed to survive.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Built for networks that fight back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Colitu — Connection Liberty Tunnel&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the research:&lt;/strong&gt; &lt;a href="https://lab.colitu.com/research/ac-regression-faultlab" rel="noopener noreferrer"&gt;Colitu Lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the technical architecture:&lt;/strong&gt; &lt;a href="https://colitu.com/en/adaptive-connect" rel="noopener noreferrer"&gt;Adaptive Connect 2.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt; &lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;colitu.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-source networking experiments:&lt;/strong&gt; &lt;a href="https://github.com/colitu/vpn-lab" rel="noopener noreferrer"&gt;github.com/colitu/vpn-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is based on Colitu Lab's October 10, 2026 fault-injection investigation. Results describe the measured experimental conditions and should not be interpreted as universal performance or security guarantees.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>vpn</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We Put Colitu's VPN Verification to the Test. Here's What Changed.</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Sat, 10 Oct 2026 04:32:19 +0000</pubDate>
      <link>https://dev.to/colitu/we-put-colitus-vpn-verification-to-the-test-heres-what-changed-179m</link>
      <guid>https://dev.to/colitu/we-put-colitus-vpn-verification-to-the-test-heres-what-changed-179m</guid>
      <description>&lt;h1&gt;
  
  
  We Put Colitu's VPN Verification to the Test. Here's What Changed.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Good engineering gets stronger when people challenge it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On October 10, 2026, a DEV Community member, &lt;a href="https://dev.to/launchgatecheck"&gt;@launchgatecheck&lt;/a&gt;, suggested an interesting test for Colitu's VPN connection verification system.&lt;/p&gt;

&lt;p&gt;The question was simple but important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a VPN handshake succeeds, but the tunnel itself cannot carry traffic? Could the application mistakenly report a working connection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than speculate, we reproduced the scenario, examined the results, made improvements, and published our findings through Colitu Lab.&lt;/p&gt;

&lt;h3&gt;
  
  
  The test
&lt;/h3&gt;

&lt;p&gt;We built a controlled environment where a VPN server accepts the handshake but silently drops traffic inside the tunnel, while verification endpoints remain accessible through the normal internet connection.&lt;/p&gt;

&lt;p&gt;We also examined automatic fallback between servers, DNS routing, and network changes.&lt;/p&gt;

&lt;p&gt;The testing identified three areas requiring improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we improved
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Tunnel-bound verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We identified a case where certain follow-up verification requests could be affected by split-tunneling rules, potentially reporting a successful connection through the wrong route.&lt;/p&gt;

&lt;p&gt;We corrected this behavior. Connection checks now use a dedicated, tunnel-bound verification path that cannot be redirected by ordinary routing rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Network transition recovery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We improved how the Windows application responds when the same Wi-Fi adapter connects to a different network.&lt;/p&gt;

&lt;p&gt;Colitu now detects address and gateway changes, invalidates previous verification, and begins reconnecting within seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. DNS consistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our tests also revealed an opportunity to improve DNS handling on one Windows transport path.&lt;/p&gt;

&lt;p&gt;We standardized tunnel adapter handling so DNS resolution follows the VPN tunnel across the tested Windows transports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Released and documented
&lt;/h3&gt;

&lt;p&gt;These improvements are available in &lt;strong&gt;Colitu Windows 2.8.4 and Linux 1.4.3&lt;/strong&gt;, including fixes introduced in earlier releases.&lt;/p&gt;

&lt;p&gt;Our published research includes the test methodology, results, technical changes, and testing limitations. Windows was tested on real devices; Linux verification used automated tests and the same fixture logic. Mobile platforms were outside the scope of this investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks to the community
&lt;/h3&gt;

&lt;p&gt;Special thanks to &lt;strong&gt;&lt;a href="https://dev.to/launchgatecheck"&gt;@launchgatecheck&lt;/a&gt;&lt;/strong&gt; for proposing the test scenarios that prompted this investigation.&lt;/p&gt;

&lt;p&gt;This is exactly why we believe open technical discussion matters.&lt;/p&gt;

&lt;p&gt;At Colitu, constructive criticism is an opportunity to validate assumptions, improve our engineering, and make our results publicly available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full technical research:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://lab.colitu.com/research/route-provenance-test" rel="noopener noreferrer"&gt;Colitu Lab — Does the connection check really go through the tunnel?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Colitu — Connection Liberty Tunnel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for networks that fight back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://lab.colitu.com" rel="noopener noreferrer"&gt;Research Lab&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>security</category>
      <category>opensource</category>
      <category>networking</category>
    </item>
    <item>
      <title>When a VPN Says "Connected" but Traffic Is Dead: Building Adaptive Connect 2.0</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Sat, 10 Oct 2026 00:26:43 +0000</pubDate>
      <link>https://dev.to/colitu/when-a-vpn-says-connected-but-traffic-is-dead-building-adaptive-connect-20-pnm</link>
      <guid>https://dev.to/colitu/when-a-vpn-says-connected-but-traffic-is-dead-building-adaptive-connect-20-pnm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A connection is not proof of connectivity.&lt;/strong&gt; Here's what we learned while building a VPN that has to survive networks that silently stop forwarding traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A VPN can establish a tunnel, complete its handshake, display a reassuring green &lt;strong&gt;Connected&lt;/strong&gt; indicator, and still fail at its most important job: carrying traffic.&lt;/p&gt;

&lt;p&gt;This is not a theoretical edge case.&lt;/p&gt;

&lt;p&gt;While developing &lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;Colitu&lt;/a&gt;, an open-source VPN project focused on reliable connectivity in restrictive network environments, we encountered a frustrating failure mode: some transports connected successfully, then approximately &lt;strong&gt;30–50 seconds&lt;/strong&gt; later, application traffic stopped flowing.&lt;/p&gt;

&lt;p&gt;The TCP connection remained open. The VPN appeared connected. But websites would not load.&lt;/p&gt;

&lt;p&gt;Restarting the connection sometimes restored traffic, only for the problem to return.&lt;/p&gt;

&lt;p&gt;A successful handshake wasn't enough. A low-latency server wasn't enough. Even a backup route wasn't enough if the health monitor misunderstood what was happening.&lt;/p&gt;

&lt;p&gt;These experiences became the foundation of &lt;strong&gt;Colitu Adaptive Connect 2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we'll cover the engineering decisions, the mistakes, the experiments, and the problems we haven't solved yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The problem: a tunnel that looks alive but isn't
&lt;/h2&gt;

&lt;p&gt;Many VPN clients follow a familiar process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure server latency.&lt;/li&gt;
&lt;li&gt;Select a server.&lt;/li&gt;
&lt;li&gt;Establish a tunnel.&lt;/li&gt;
&lt;li&gt;Report that the VPN is connected.&lt;/li&gt;
&lt;li&gt;Reconnect when the tunnel terminates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That strategy assumes transport failure will be obvious. In restrictive networks, it often isn't.&lt;/p&gt;

&lt;p&gt;On one network, we observed VLESS Reality, VLESS XHTTP, and Trojan connections successfully establish sessions and then stop carrying traffic after roughly 30–50 seconds. Hysteria2 continued working on the same network.&lt;/p&gt;

&lt;p&gt;The important distinction is between &lt;strong&gt;transport state&lt;/strong&gt; and &lt;strong&gt;application-level reachability&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A socket remaining open does not prove that useful traffic is passing through it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This exposed a weakness in Adaptive Connect 1.x: in-session monitoring covered Hysteria2 but not every transport. We needed a health model based on what users actually depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Verify real traffic, not just handshakes
&lt;/h2&gt;

&lt;p&gt;Adaptive Connect 2.0 treats a connection as functional only after useful traffic successfully passes through the selected tunnel.&lt;/p&gt;

&lt;p&gt;Instead of relying exclusively on Colitu infrastructure, connection-time checks use established external connectivity endpoints to help distinguish three states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport established&lt;/td&gt;
&lt;td&gt;The protocol completed a connection attempt.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application traffic verified&lt;/td&gt;
&lt;td&gt;Actual requests can traverse the VPN.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying network unavailable&lt;/td&gt;
&lt;td&gt;The device itself may have lost access.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each attempt has a bounded time budget. Failed candidates are followed by alternatives rather than endless retries. On Android, the UI presents a verification phase instead of immediately claiming success.&lt;/p&gt;

&lt;p&gt;A critical detail: &lt;strong&gt;the primary route has its own verification path&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If normal traffic is already passing through a backup, a generic health check might report success even while the primary route is broken. The dedicated local-only verification entry point tests the primary route without letting the backup conceal a failure.&lt;/p&gt;

&lt;p&gt;That distinction informs future transport decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Give each network its own memory
&lt;/h2&gt;

&lt;p&gt;A transport that fails on home Wi-Fi may work perfectly over mobile data. Adaptive Connect 2.0 keeps bounded, device-local history based on network context.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Last working server&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last working transport per server&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport freeze marker&lt;/td&gt;
&lt;td&gt;Normally up to 6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-wide failure&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local history is capped at &lt;strong&gt;200 entries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These limits are intentional. A failed connection should affect subsequent ranking, but it should not make a transport permanently unusable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure history should guide exploration, not make recovery impossible.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Learn from network conditions without collecting browsing history
&lt;/h2&gt;

&lt;p&gt;Local memory helps a device make better decisions. Aggregated network observations can also help identify where particular transports are failing.&lt;/p&gt;

&lt;p&gt;Adaptive Connect 2.0's reporting design uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HMAC-signed network tokens carrying country and ASN information, valid for 48 hours.&lt;/li&gt;
&lt;li&gt;Hourly success/failure counters grouped by country, ASN, and transport.&lt;/li&gt;
&lt;li&gt;Temporary keyed pseudonyms to count distinct devices.&lt;/li&gt;
&lt;li&gt;Seven-day retention of aggregated counters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This design does &lt;strong&gt;not&lt;/strong&gt; require URLs visited, browsing history, or application payloads to produce transport recommendations.&lt;/p&gt;

&lt;p&gt;For example, the ASN-level rule requires observations from at least &lt;strong&gt;five devices&lt;/strong&gt; and a success rate below &lt;strong&gt;20%&lt;/strong&gt; over the previous 24 hours before deprioritizing a transport. With insufficient evidence, the system falls back to broader signals rather than declaring a protocol blocked.&lt;/p&gt;

&lt;p&gt;These are engineering safeguards, &lt;strong&gt;not&lt;/strong&gt; a claim of perfect anonymity or immunity to malicious telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Our first backup strategy was wrong
&lt;/h2&gt;

&lt;p&gt;At first, transport diversity seemed like the natural basis for failover.&lt;/p&gt;

&lt;p&gt;If the primary uses UDP, choose a TCP-based backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary: Hysteria2 (Server A)
Backup:  Trojan   (Server B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds sensible: different transports might fail differently.&lt;/p&gt;

&lt;p&gt;But on the network we were testing, multiple TCP-based transports suffered the same freezing behavior. A different protocol was not necessarily a working alternative.&lt;/p&gt;

&lt;p&gt;We had confused &lt;strong&gt;protocol diversity&lt;/strong&gt; with &lt;strong&gt;failure independence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The revised strategy prefers a transport already proven to work on the current network, even when that means using the same transport on a different server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary: Hysteria2 (Server A)
Backup:  Hysteria2 (Server B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not about declaring Hysteria2 universally superior. It was about choosing the best &lt;em&gt;observed&lt;/em&gt; independent route for the network at hand.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Protocol diversity does not guarantee failure independence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. A backup-aware health monitor
&lt;/h2&gt;

&lt;p&gt;One of our early failure-injection tests exposed a subtle bug.&lt;/p&gt;

&lt;p&gt;We disabled the primary route. After approximately &lt;strong&gt;12 seconds&lt;/strong&gt;, the backup began carrying traffic. That looked like successful failover.&lt;/p&gt;

&lt;p&gt;But the old health monitor still saw the failed primary, classified the whole VPN as unhealthy, and restarted the tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The watchdog destroyed the recovery the backup had achieved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We changed the health model to track two separate questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Primary health:&lt;/strong&gt; Can the original route carry traffic?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effective connection health:&lt;/strong&gt; Can traffic still pass through any usable route, including the backup?&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primary route&lt;/th&gt;
&lt;th&gt;Effective route&lt;/th&gt;
&lt;th&gt;Correct response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;Continue normally.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;Preserve the session; record primary failure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;Start recovery or reconnection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The backup is configured inside the running tunnel and used on demand. It is &lt;strong&gt;not&lt;/strong&gt; a continuously active second VPN tunnel.&lt;/p&gt;

&lt;p&gt;We also encountered an Xray observer/balancer detail: prefix-based tag matching could accidentally include the backup route in a primary-only health check. Making primary and backup tags non-overlapping removed that ambiguity.&lt;/p&gt;

&lt;p&gt;This is why a failover design cannot be validated just by reading its configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Parallel probing without unbounded retries
&lt;/h2&gt;

&lt;p&gt;Sequentially trying every transport on every server can be painfully slow when packets are silently dropped.&lt;/p&gt;

&lt;p&gt;Adaptive Connect 2.0 uses bounded selection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The best two connection candidates may be tested in parallel.&lt;/li&gt;
&lt;li&gt;The first candidate that successfully carries real traffic wins.&lt;/li&gt;
&lt;li&gt;Automatic server fallback is limited to three servers in one connection attempt.&lt;/li&gt;
&lt;li&gt;Transport, server, and overall attempts each have time budgets.&lt;/li&gt;
&lt;li&gt;Previous network observations influence the initial ordering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parallelism is inspired by the &lt;em&gt;Happy Eyeballs&lt;/em&gt; approach described in &lt;a href="https://www.rfc-editor.org/rfc/rfc8305" rel="noopener noreferrer"&gt;RFC 8305&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;not a newly invented connection algorithm&lt;/strong&gt;. The engineering value is in combining bounded candidate racing, network-specific history, and real-traffic verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Testing the system by deliberately breaking it
&lt;/h2&gt;

&lt;p&gt;We built an internal fault-injection tool to selectively disrupt one client's access to a server or transport, without intentionally affecting other users.&lt;/p&gt;

&lt;p&gt;The experiment took place on &lt;strong&gt;October 9–10, 2026&lt;/strong&gt;, using one Android 11 phone connected to one home Wi-Fi network in an environment where DPI was present.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;60-minute endurance test&lt;/strong&gt;, we used eight servers and injected 11 controlled failures lasting roughly 60–120 seconds each. An HTTP probe was sent through the VPN approximately every five seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test duration&lt;/td&gt;
&lt;td&gt;60 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servers involved&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controlled fault injections&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful HTTP probes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;741 / 750&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP probe success rate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary-server outage&lt;/td&gt;
&lt;td&gt;101 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interruption observed during that outage&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~5 seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest observed interruption&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40 seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most encouraging event was a &lt;strong&gt;101-second outage of the active primary server&lt;/strong&gt;. The VPN remained in its connected state, the backup carried traffic, and the observed interruption was approximately five seconds.&lt;/p&gt;

&lt;p&gt;However, a separate test revealed a hard limit: when Hysteria2 was disabled across all tested servers, the same network's TCP transport freezes left no usable alternative. That caused a &lt;strong&gt;40-second interruption&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Some injected faults targeted servers or transports that were not carrying the active session. Those produced zero observed downtime, but &lt;strong&gt;should not be represented as successful active-path recoveries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We believe that distinction matters more than a single flattering availability percentage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important test limitations
&lt;/h3&gt;

&lt;p&gt;This was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One device&lt;/strong&gt; and &lt;strong&gt;one home Wi-Fi network&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One hour&lt;/strong&gt; of controlled experiments.&lt;/li&gt;
&lt;li&gt;Mostly &lt;strong&gt;injected server/transport failures&lt;/strong&gt;, not a comprehensive reproduction of real-world censorship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a cross-platform or independently verified reliability benchmark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 98.8% figure describes successful HTTP probes &lt;strong&gt;within this specific test&lt;/strong&gt;, not worldwide Colitu availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. What Adaptive Connect 2.0 does not solve
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Existing TCP sessions are not guaranteed to survive server failover.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping the VPN interface active and keeping an established application connection alive are different engineering problems. A download may still stop when its underlying exit path changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We have not proven the same results across every platform or network.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Different mobile operators, Wi-Fi environments, devices, and censorship systems may produce very different outcomes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Controlled fault injection is not equivalent to every real censorship event.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Actual interference can be less predictable and may respond dynamically to a client's behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No software can route traffic through a path that does not exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If every accessible route is unusable, Adaptive Connect cannot manufacture connectivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Next: session continuity across transports
&lt;/h2&gt;

&lt;p&gt;Our next research question is harder than reconnecting a VPN:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can existing application sessions survive a transport change without having to reconnect?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The proposed &lt;strong&gt;Colitu Session Layer (CSL)&lt;/strong&gt; separates a logical session from its underlying transport. Early laboratory work is promising, but &lt;strong&gt;CSL is not a production capability of Adaptive Connect 2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is also an important distinction between switching protocols on one server and moving an established session to another server with a different egress address. The latter requires additional session and egress management.&lt;/p&gt;

&lt;p&gt;We are approaching that work as research, not as a feature we can already promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from Adaptive Connect 2.0 is not that one VPN protocol is better than another.&lt;/p&gt;

&lt;p&gt;It's that connectivity is a changing, measurable property of a network.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A working socket is not proof of working traffic.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A different protocol is not necessarily an independent backup.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A health monitor that ignores backup behavior can trigger avoidable outages.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure observations should inform decisions without preventing exploration.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controlled experiments should report failures as clearly as successes.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliable connectivity is less about choosing the perfect tunnel and more about continuously verifying that a usable path still exists.&lt;/p&gt;

&lt;p&gt;That's the problem we're trying to solve at Colitu.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the research and source code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Adaptive Connect 2.0 technical report:&lt;/strong&gt; &lt;a href="https://docs.colitu.com/en/adaptive-connect-paper" rel="noopener noreferrer"&gt;https://docs.colitu.com/en/adaptive-connect-paper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source Colitu clients:&lt;/strong&gt; &lt;a href="https://github.com/colitu" rel="noopener noreferrer"&gt;https://github.com/colitu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project website:&lt;/strong&gt; &lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;https://colitu.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're interested in feedback from developers working on networking, transport protocols, privacy-preserving telemetry, fault injection, and censorship circumvention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What failure modes would you test next? How would you design a health monitor that distinguishes a dead primary from a healthy session on its backup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Colitu Engineering&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for networks that fight back.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>networking</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Why a VPN's “Connected” Status Isn't Enough: Building Adaptive Protocol Fallback</title>
      <dc:creator>Colitu VPN</dc:creator>
      <pubDate>Fri, 09 Oct 2026 01:07:53 +0000</pubDate>
      <link>https://dev.to/colitu/why-a-vpns-connected-status-isnt-enough-building-adaptive-protocol-fallback-111e</link>
      <guid>https://dev.to/colitu/why-a-vpns-connected-status-isnt-enough-building-adaptive-protocol-fallback-111e</guid>
      <description>&lt;p&gt;A successful connection handshake doesn't always mean a working internet connection.&lt;/p&gt;

&lt;p&gt;On restrictive or unstable networks, a VPN tunnel can appear to establish successfully while actual traffic never reaches its destination.&lt;/p&gt;

&lt;p&gt;For developers building connectivity tools, this creates an interesting engineering problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you determine whether a connection actually works, rather than simply whether it connected?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is one of the problems we're addressing while developing &lt;strong&gt;Colitu&lt;/strong&gt;, an open-source VPN project designed for challenging network environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Trusting a Handshake
&lt;/h2&gt;

&lt;p&gt;Consider a typical connection sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client contacts a remote server.&lt;/li&gt;
&lt;li&gt;A protocol handshake succeeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The application reports that the connection is established.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The user attempts to load a website.&lt;/li&gt;
&lt;li&gt;Nothing happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A network filter may allow the initial handshake but interfere with subsequent traffic. Another network might block UDP while allowing certain TCP connections.&lt;/p&gt;

&lt;p&gt;From the application's perspective, the connection can look healthy even though it isn't useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why connection establishment and actual connectivity must be treated as separate states.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Different Networks Require Different Transports
&lt;/h2&gt;

&lt;p&gt;There isn't one transport protocol that works equally well in every environment.&lt;/p&gt;

&lt;p&gt;Colitu supports five connection modes across several protocol families:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hysteria2:&lt;/strong&gt; A QUIC-based transport designed for efficient communication, including under challenging network conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLESS Reality:&lt;/strong&gt; A transport configuration designed to make connections more resistant to certain forms of network filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLESS XHTTP:&lt;/strong&gt; An HTTP-based transport approach providing an alternative when other connection methods are disrupted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trojan:&lt;/strong&gt; A TLS-based proxy protocol offering another encrypted connection path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadowsocks 2022:&lt;/strong&gt; An encrypted proxy protocol designed with modern cryptographic mechanisms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each mode has its own characteristics, advantages, and limitations.&lt;/p&gt;

&lt;p&gt;The goal isn't to declare one protocol universally superior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is to make connection selection responsive to real network conditions.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Adaptive Connect
&lt;/h2&gt;

&lt;p&gt;Manually switching between protocols can be frustrating, especially for users who aren't familiar with networking technologies.&lt;/p&gt;

&lt;p&gt;Colitu's &lt;strong&gt;Adaptive Connect&lt;/strong&gt; system is designed to reduce that complexity.&lt;/p&gt;

&lt;p&gt;Instead of requiring users to troubleshoot every failed connection themselves, the application can attempt alternative connection modes when the selected method fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Process Works
&lt;/h3&gt;

&lt;p&gt;At a high level, adaptive connection management follows these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server Selection:&lt;/strong&gt; Obtain the available server configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Selection:&lt;/strong&gt; Choose a compatible connection mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Attempt:&lt;/strong&gt; Attempt to establish the encrypted connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity Verification:&lt;/strong&gt; Check whether real network traffic can pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success:&lt;/strong&gt; Continue using the working connection mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback:&lt;/strong&gt; If verification fails, attempt another available mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important distinction is that a successful handshake alone is not treated as sufficient evidence of usable connectivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Connectivity Verification Matters
&lt;/h3&gt;

&lt;p&gt;A VPN client should distinguish between several possible conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disconnected:&lt;/strong&gt; No active connection has been established.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connecting:&lt;/strong&gt; The client is attempting to establish a connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connected:&lt;/strong&gt; The selected tunnel has been established.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity Verified:&lt;/strong&gt; Traffic has successfully passed through the intended connection path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Failed:&lt;/strong&gt; The connection or its verification failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These distinctions make connection handling easier to reason about.&lt;/p&gt;

&lt;p&gt;They also help developers avoid giving users a false sense of connectivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge of Protocol Fallback
&lt;/h2&gt;

&lt;p&gt;Fallback sounds simple in theory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Protocol A fails, try Protocol B.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice, implementing reliable fallback requires several engineering decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Detecting Failure Correctly
&lt;/h3&gt;

&lt;p&gt;A timeout doesn't always indicate permanent failure.&lt;/p&gt;

&lt;p&gt;Temporary packet loss, high latency, DNS resolution problems, and server overload can produce similar symptoms.&lt;/p&gt;

&lt;p&gt;A good fallback strategy must balance responsiveness against unnecessary protocol switching.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Choosing the Next Protocol
&lt;/h3&gt;

&lt;p&gt;Different networks behave differently.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some networks restrict UDP traffic.&lt;/li&gt;
&lt;li&gt;Others interfere with specific TLS connection patterns.&lt;/li&gt;
&lt;li&gt;Certain environments allow initial connections but disrupt sustained traffic.&lt;/li&gt;
&lt;li&gt;Network conditions may change while the application is running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fallback system should consider these differences rather than assume every connection failure has the same cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Avoiding Endless Retry Loops
&lt;/h3&gt;

&lt;p&gt;Repeatedly attempting the same failing connection mode wastes time and resources.&lt;/p&gt;

&lt;p&gt;Retry limits, timeout handling, and failure-state management are important parts of a resilient connection system.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Managing Platform Differences
&lt;/h3&gt;

&lt;p&gt;VPN applications interact with different networking APIs across Windows, Linux, Android, and iOS.&lt;/p&gt;

&lt;p&gt;Connection orchestration, routing, tunnel management, and background execution constraints can differ substantially between platforms.&lt;/p&gt;

&lt;p&gt;An adaptive design must account for those differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Must Remain a Priority
&lt;/h2&gt;

&lt;p&gt;Reliability should never come at the expense of user security.&lt;/p&gt;

&lt;p&gt;A VPN must not silently fall back to an unprotected direct connection simply because an encrypted tunnel fails.&lt;/p&gt;

&lt;p&gt;This is especially important in restrictive network environments.&lt;/p&gt;

&lt;p&gt;Connection recovery should preserve the application's security guarantees and provide accurate status information to the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failed secure connection is preferable to an unannounced loss of protection.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Adaptive Fallback Cannot Solve
&lt;/h2&gt;

&lt;p&gt;Adaptive connectivity is useful, but it is not a universal solution.&lt;/p&gt;

&lt;p&gt;There are limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A network may block every available transport.&lt;/li&gt;
&lt;li&gt;Captive portals may require additional authentication.&lt;/li&gt;
&lt;li&gt;Some networks may disrupt encrypted traffic regardless of protocol.&lt;/li&gt;
&lt;li&gt;Trying multiple connection modes can increase initial connection time.&lt;/li&gt;
&lt;li&gt;Platform-specific routing behavior can affect the traffic that receives VPN protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No VPN should promise perfect connectivity under every possible network condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good engineering means understanding both what a system can do and where its limitations begin.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We're Building Colitu in the Open
&lt;/h2&gt;

&lt;p&gt;VPN software occupies a particularly sensitive position in the networking stack.&lt;/p&gt;

&lt;p&gt;Users trust it to handle their connections and protect their privacy.&lt;/p&gt;

&lt;p&gt;At Colitu, we're working to make our client technology transparent and open to technical scrutiny.&lt;/p&gt;

&lt;p&gt;We believe that publishing source code, documenting engineering decisions, and welcoming technical feedback are important parts of building trustworthy software.&lt;/p&gt;

&lt;p&gt;Of course, open-source client code alone does not verify server-side behavior or replace independent security audits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparency is a process, not a marketing claim.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next for Colitu?
&lt;/h2&gt;

&lt;p&gt;Our focus is on continuously improving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adaptive connection selection.&lt;/li&gt;
&lt;li&gt;Reliability under difficult network conditions.&lt;/li&gt;
&lt;li&gt;Connection failure detection and recovery.&lt;/li&gt;
&lt;li&gt;Clearer connection status reporting.&lt;/li&gt;
&lt;li&gt;Cross-platform compatibility.&lt;/li&gt;
&lt;li&gt;Open-source development and technical documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll continue sharing engineering insights, implementation challenges, and lessons learned through the &lt;strong&gt;Colitu Engineering&lt;/strong&gt; series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Talk Engineering
&lt;/h2&gt;

&lt;p&gt;We're interested in hearing from other developers working on networking, transport protocols, and secure connectivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you distinguish a successfully established connection from one that is actually carrying usable traffic?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do you use active connectivity probes, health checks, timeout-based fallback, or something else?&lt;/p&gt;

&lt;p&gt;We'd love to hear about your approach.&lt;/p&gt;




&lt;h3&gt;
  
  
  Explore Colitu
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Built for networks that fight back.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://colitu.com" rel="noopener noreferrer"&gt;Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://colitu.com/en/protocols/adaptive-connect" rel="noopener noreferrer"&gt;Adaptive Connect Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://colitu.com/en/protocols" rel="noopener noreferrer"&gt;Supported Protocols&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://colitu.com/en/download" rel="noopener noreferrer"&gt;Download Colitu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance and should be reviewed against the current implementation before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>showdev</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
