Skip to content

fix: stop VPN Bypass's own route churn destabilising GlobalProtect (#65) - #66

Merged
GeiserX merged 4 commits into
mainfrom
fix/issue-65-route-churn
Aug 4, 2026
Merged

fix: stop VPN Bypass's own route churn destabilising GlobalProtect (#65)#66
GeiserX merged 4 commits into
mainfrom
fix/issue-65-route-churn

Conversation

@GeiserX

@GeiserX GeiserX commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Stop VPN Bypass destabilising other VPN clients (#65)

Fixes #65.

The problem, measured

On a live machine running GlobalProtect, PanGPS.log recorded 757 route-change events and 48 full tunnel teardowns in one day — about one disconnect every 8 minutes, ~27s of downtime each. 83% of the teardowns were preceded, within ~0.02s, by Failed to find route for <gateway>.

A/B: with the app running, ~18 GP route-change events/60s and 17–18 concurrent /sbin/route processes; with the helper booted out, 2 events and 0 processes. ~90% of the route-table churn was ours.

Every routing-table write raises a kernel route-change event. GlobalProtect re-validates its own gateway route on each one and tears the tunnel down when that lookup fails. So the fix is not to make our writes faster — it is to stop writing when nothing changed, and to never leave a destination momentarily without a route.

Two churn generators, both removed

1. Re-routing rebuilt everything instead of reconciling. performReroute() did removeAllRoutes() + applyAllRoutesInternal(). Because removeAllRoutes() clears activeRoutes, the shouldSkipReapply no-op guard was structurally unable to fire on that path — activePairs was always empty, so it never equalled desiredPairs. Every re-route re-issued the entire set (~3N writes).

In Bypass mode the routes egress via the local gateway, so the usual trigger — a VPN interface renumber — changes no destination|gateway pair at all. The whole storm rebuilt routes to the values they already had. It now reconciles: applies only genuine differences, does nothing when already correct.

2. The helper deleted every route before adding it. A blind route -n delete preceded every route -n add: two writes per route even when correct, and a brief window with no route for that destination — exactly what makes another client's gateway check fail. It could also delete a route this app never installed. Now: change in place → add if nothing was there → delete+add only on a genuine race.

Also fixed

  • Concurrent route operations. Helper-side route/hosts mutations are serialised across XPC connections (HelperToolDelegate creates a fresh HelperTool per connection, and the app recreates its connection on an XPC deadline — the condition behind the simultaneous route processes). getVersion is deliberately not serialised: it is the liveness probe, and queueing it behind a batch would time it out and force a spurious helper reinstall.
  • A gate leak that could wedge the app. performReroute was the only one of nine gate holders releasing the route-operation gate without defer. Since the body now suspends during a real apply, an unwind there would leak it permanently — and acquireRouteOperation() try-acquires, so every later apply, re-route and DNS refresh would silently give up for the life of the process.
  • The helper now logs. It previously emitted nothing at all: log stream --predicate 'subsystem == "com.geiserx.vpnbypass.helper"' --info.

Verification

  • swift build clean; make build-helper universal; 855 tests, 0 failures.
  • New RerouteChurnTests, proven non-vacuous: re-injecting the old teardown makes the regression test fail; restoring the fix makes it pass. The detector is routeEpoch, which removeAllRoutes() bumps as its unconditional first statement and which the apply path never touches, with a negative-control test proving the detector moves.
  • Leak-critical invariants untouched: the latch-clear-at-START ordering, catch-alls-first teardown, and the classic Bypass/VPN-Only route sets (ClassicRouteCompilerTests, RouteCompilerTests, ReapplySkipTests green).
  • Helper stays ad-hoc-signable with no Network Extension entitlements; RunAtLoad unchanged.

Note on test hermeticity

RouteManager is a private init singleton that loads the real on-disk config on first touch, so on a developer machine with a populated config, unrelated startup work can hold the route gate and make performReroute assertions vacuous. Tests that drive it now skip loudly rather than report a false pass or failure. CI has no user config, so they execute normally there. Making the suite hermetic is tracked as separate work.

Deliberately not in this PR

The routing-socket (PF_ROUTE) rewrite, kernel-truth diffing and contention backoff. Research showed diff-before-write without backoff would make this bug worse — we are currently blind to another client clobbering our routes, so diffing would start that fight at socket speed — and a hand-packed rt_msghdr writer inside the root daemon is a new brick class. Those land separately, measured against this baseline.

Helper upgrade

Helper version 1.8.0 → 1.9.0 so installed helpers actually pick up the fix. Expect one admin prompt on first launch after upgrading.

Summary by CodeRabbit

Release Notes v3.1.7

  • New Features

    • Added detailed logging for helper operations to enhance debugging and support visibility.
  • Bug Fixes

    • Improved route update efficiency through in-place reconciliation rather than full rebuild.
    • Enhanced reliability of route operations through coordinated execution.
    • Refined route installation with improved fallback handling logic.
  • Tests

    • Added comprehensive test coverage for route management behavior and concurrent scenarios.

GeiserX added 3 commits August 4, 2026 13:23
A re-route was a full teardown-and-rebuild: removeAllRoutes() followed by
applyAllRoutesInternal(). Because removeAllRoutes() clears activeRoutes, the
shouldSkipReapply no-op guard was structurally unable to fire on that path —
activePairs was always empty, so it never equalled desiredPairs — and every
re-route re-issued the ENTIRE route set (~3N kernel mutations: N deletes plus
N delete-before-adds).

In Bypass mode those routes egress via the LOCAL gateway, so the usual trigger
(a VPN interface renumber) changes no destination|gateway pair at all: the whole
storm rebuilt routes to the values they already had. Every mutation raises a
kernel route-change event, and GlobalProtect re-validates its own gateway route
on each one, tearing down and re-negotiating the tunnel when that lookup
transiently fails — measured on a live machine as 757 route-change events and 48
tunnel teardowns in one day, ~27s of downtime each.

performReroute() now calls applyAllRoutesInternal() directly, which computes the
desired set, skips entirely when it already matches (zero mutations), and drops
genuinely stale destinations via commitAppliedRoutes' orphan cleanup. A real
gateway change is still applied, in place, without a window where the route is
absent — which also closes a brief VPN-Only leak window the teardown opened.

Also release the route-operation gate via defer. performReroute was the only one
of nine gate holders releasing without it, so an unwind at any suspension point
leaked the gate permanently: acquireRouteOperation() try-acquires, so every later
apply, re-route and DNS refresh would give up silently for the life of the process.

The leak-critical latch-clear-at-START ordering is unchanged.

New RerouteChurnTests: 3 tests, proven non-vacuous (re-injecting the teardown
fails the regression test). Full suite shows no new failures against baseline.
The helper issued a blind `route -n delete` before every `route -n add`. That
cost two kernel routing-table writes per route even when the route was already
correct, and it briefly left the destination with NO route at all.

Both matter because every routing-table write raises a kernel route-change event,
and GlobalProtect's connection monitor re-validates its own gateway route on each
one. Its teardowns are preceded by `Failed to find route for <gateway>` — exactly
what a transient removal produces. Measured on a live machine: 757 route-change
events and 48 tunnel teardowns in a day, ~27s of downtime each. The blind delete
could also remove a route this app never installed.

installRoute() now walks a ladder that never opens a gap:
  1. `route change` — rewrite in place (one write, no gap); the re-apply case.
  2. `route add`    — only when nothing was there to change.
  3. `delete`+`add` — last resort, only when add reports the route exists (a race
     with another writer). The only remaining path that can open a gap, and now
     rare rather than universal.

Also:
- Serialise route/hosts mutations across XPC connections. HelperToolDelegate makes
  a fresh HelperTool per connection and the app recreates its connection on an XPC
  deadline, so handlers could previously mutate the table concurrently — the
  condition behind the simultaneous /sbin/route processes reported in #65.
  getVersion is deliberately NOT serialised: it is the app's liveness probe, and
  queueing it behind a long batch would time it out and force a helper reinstall.
- Add os_log to the helper, which previously produced no diagnostics whatsoever:
  log stream --predicate 'subsystem == "com.geiserx.vpnbypass.helper"' --info
- Bump helperVersion 1.8.0 -> 1.9.0 (and Helper/Info.plist to match, enforced by
  HelperVersionDriftTests) so installed helpers actually pick this up.
- Guard RerouteLatchTimingTests with the same free-gate precondition used by the
  new churn tests: RouteManager is a private-init singleton that loads the real
  on-disk config on first touch, so on a machine with a populated config unrelated
  startup work can hold the route gate and make those assertions vacuous.

855 tests, 0 failures.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GeiserX, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f49c7d8c-c379-4391-96a9-3397f49dacd8

📥 Commits

Reviewing files that changed from the base of the PR and between 9962bf3 and 93bdc15.

📒 Files selected for processing (3)
  • Helper/HelperTool.swift
  • ROADMAP.md
  • docs/CHANGELOG.md
📝 Walkthrough

Walkthrough

The helper now serializes route mutations, logs route operations, and uses change-first installation. RouteManager reconciles routes in place with deferred cleanup. New tests cover reroute churn and route-gate timing. Versions and release notes are updated.

Changes

Routing stability

Layer / File(s) Summary
Serialized helper route mutations
Helper/HelperTool.swift
Route additions and removals use a shared serial queue. Installation tries change, then add, and replaces existing routes only when required.
In-place reroute reconciliation
Sources/VPNBypassCore/RouteManager.swift, Tests/VPNBypassTests/RerouteChurnTests.swift, Tests/VPNBypassTests/RerouteDeciderTests.swift
performReroute() applies route differences without full teardown and restores operation state with defer. Tests cover route epochs, gate availability, and apply execution.
Helper version and release metadata
Sources/VPNBypassCore/HelperProtocol.swift, Helper/Info.plist, ROADMAP.md, docs/CHANGELOG.md
The helper version changes to 1.9.0. Bundle metadata, roadmap entries, and the 3.1.7 changelog entry are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses reconciliation, transient route gaps, and serialization, but it still invokes /sbin/route and misses issue #65's no-subprocess acceptance criterion. Implement the routing-socket path, or split the remaining subprocess and kernel-event requirements into a separate tracked issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix for route churn affecting GlobalProtect.
Description check ✅ Passed The description provides a detailed summary, testing results, scope boundaries, and implementation context, despite not using the template headings and checkboxes.
Out of Scope Changes check ✅ Passed The code, tests, helper logging, version updates, documentation, and changelog changes support the stated route-churn fix.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-65-route-churn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
Tests/VPNBypassTests/RerouteChurnTests.swift (1)

69-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Track the hermeticity work with bd.

This comment uses .goal-loop/GOAL-WORKLOG.md as the task tracker. Replace the worklog reference with the applicable bd issue identifier.

As per coding guidelines, “Use bd for all task tracking.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/VPNBypassTests/RerouteChurnTests.swift` around lines 69 - 70, Update
the explanatory comment near the reroute churn tests to replace the
`.goal-loop/GOAL-WORKLOG.md` worklog reference with the applicable `bd` issue
identifier, while preserving the existing context about making the suite
hermetic.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/CHANGELOG.md`:
- Line 11: Correct the “GlobalProtect disconnect loop” changelog entry so the
reported disconnect frequency matches the stated 48 teardowns over a 24-hour
day: use approximately one disconnect every 30 minutes, or explicitly identify
the shorter active measurement window that justifies the existing eight-minute
figure.

In `@Helper/HelperTool.swift`:
- Around line 116-122: Update updateHostsFile so its complete hosts-file read,
filtering, and write transaction executes synchronously on the existing static
routeQueue. Preserve the current read-modify-write behavior and error handling
while ensuring concurrent XPC connections cannot interleave the transaction.
- Around line 245-251: The route replacement fallback in the exists-handling
branch must stop when deletion fails. In the visible `executeRoute` call within
`HelperTool`, retain its result, return failure with the deletion error when
`success` is false, and only execute the replacement add after successful
removal.

In `@ROADMAP.md`:
- Around line 3-10: Update the audit-token verification version attribution in
the Current State section of ROADMAP.md from 1.9.0 to 1.7.0, matching the
history documented by HelperProtocol.swift; leave the separate 1.9.0 route,
serialization, and logging attribution unchanged.

---

Nitpick comments:
In `@Tests/VPNBypassTests/RerouteChurnTests.swift`:
- Around line 69-70: Update the explanatory comment near the reroute churn tests
to replace the `.goal-loop/GOAL-WORKLOG.md` worklog reference with the
applicable `bd` issue identifier, while preserving the existing context about
making the suite hermetic.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d0bfcfcd-f7e3-4532-876b-0bac1f64701c

📥 Commits

Reviewing files that changed from the base of the PR and between 07c1733 and 9962bf3.

📒 Files selected for processing (9)
  • .gitignore
  • Helper/HelperTool.swift
  • Helper/Info.plist
  • ROADMAP.md
  • Sources/VPNBypassCore/HelperProtocol.swift
  • Sources/VPNBypassCore/RouteManager.swift
  • Tests/VPNBypassTests/RerouteChurnTests.swift
  • Tests/VPNBypassTests/RerouteDeciderTests.swift
  • docs/CHANGELOG.md

Comment thread docs/CHANGELOG.md Outdated
## [3.1.7] - 2026-08-04

### Fixed
- **VPN Bypass no longer destabilises other VPN clients (GlobalProtect disconnect loop).** Two habits made the app rewrite the routing table constantly even when nothing had changed, and every routing-table write is an event that enterprise VPN clients react to — one measured machine logged 757 route-change events and 48 tunnel teardowns in a single day, roughly one disconnect every eight minutes with ~27 seconds of downtime each.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'README availability:'
[ -f README.md ] && echo 'README.md exists' || echo 'README.md missing'
echo 'CHANGELOG availability:'
[ -f docs/CHANGELOG.md ] && echo 'docs/CHANGELOG.md exists' || echo 'docs/CHANGELOG.md missing'

if [ -f README.md ]; then
  echo 'README.md first 80 lines:'
  sed -n '1,80p' README.md | cat -n
fi

echo 'CHANGELOG relevant lines:'
sed -n '1,80p' docs/CHANGELOG.md | cat -n

echo 'Search for related terms:'
rg -n "disconnect|tunnel teardown|routing table|VPN Bypass|route-change|757|48|eight minutes|30 minutes" . || true

Repository: GeiserX/VPN-Bypass

Length of output: 1922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Working directory:'
pwd
echo 'Git remote/config:'
git remote -v 2>/dev/null || true
git status --short 2>/dev/null || true

echo 'README availability:'
[ -f README.md ] && echo 'README.md exists' || echo 'README.md missing'
echo 'CHANGELOG availability:'
[ -f docs/CHANGELOG.md ] && echo 'docs/CHANGELOG.md exists' || echo 'docs/CHANGELOG.md missing'

if [ -f README.md ]; then
  echo 'README.md first 80 lines:'
  sed -n '1,80p' README.md | cat -n
fi

if [ -f docs/CHANGELOG.md ]; then
  echo 'CHANGELOG relevant lines:'
  sed -n '1,80p' docs/CHANGELOG.md | cat -n
fi

echo 'Search for related terms:'
rg -n "disconnect|tunnel teardown|routing table|VPN Bypass|route-change|757|48|eight minutes|30 minutes" . || true

Repository: GeiserX/VPN-Bypass

Length of output: 1922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'PWD:'
pwd
echo 'LS top-level files:'
ls -la | sed -n '1,60p'

echo 'Git config/remote:'
git config --list --show-scope 2>/dev/null || true
git remote -v 2>/dev/null || true

echo 'README existence:'
if [ -f README.md ]; then
  echo 'README.md bytes:'
  wc -c README.md
  sed -n '1,80p' README.md | cat -n
else
  echo 'README.md missing'
fi

echo 'CHANGELOG existence:'
if [ -f docs/CHANGELOG.md ]; then
  echo 'docs/CHANGELOG.md bytes:'
  wc -c docs/CHANGELOG.md
  sed -n '1,80p' docs/CHANGELOG.md | cat -n
else
  echo 'docs/CHANGELOG.md missing'
fi

echo 'Search related terms:'
rg -n "disconnect|tunnel teardown|routing table|VPN Bypass|route-change|757|48|eight minutes|30 minutes" . || true

Repository: GeiserX/VPN-Bypass

Length of output: 1922


Correct the disconnect-rate wording.

If “a single day” means 24 hours, 48 teardowns equal one disconnect every 30 minutes, not every eight minutes. Correct the rate or state the shorter active measurement window that supports eight minutes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/CHANGELOG.md` at line 11, Correct the “GlobalProtect disconnect loop”
changelog entry so the reported disconnect frequency matches the stated 48
teardowns over a 24-hour day: use approximately one disconnect every 30 minutes,
or explicitly identify the shorter active measurement window that justifies the
existing eight-minute figure.

Comment thread Helper/HelperTool.swift
Comment thread Helper/HelperTool.swift
Comment thread ROADMAP.md Outdated
…correct docs

CodeRabbit on #66:
- MAJOR: updateHostsFile did a read-modify-write of /etc/hosts outside routeQueue,
  so two concurrent XPC connections could read the same content and the later write
  would silently discard the earlier update — while the queue's own comment claimed
  it serialised hosts mutations. The transaction now runs on routeQueue.
- installRoute's last-resort replace discarded the delete result, so a failed delete
  surfaced as a secondary 'exists' error from the follow-up add, hiding the cause.
  It now returns the deletion error.
- CHANGELOG: 48 teardowns was over ~6.5h of active use, not a 24h day; stated the
  measurement window that supports 'every 8 minutes'.
- ROADMAP: audit-token verification shipped in 1.7.0 and the cdhash pin in 1.8.0;
  1.9.0 is the current helper. Attribution corrected.

855 tests, 0 failures; universal helper builds.
@GeiserX

GeiserX commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai All four addressed — thanks, the hosts-file one was a real hole and my own comment was over-claiming.

✅ MAJOR — updateHostsFile outside routeQueue. Correct, and the queue's comment claimed it covered hosts mutations while the read-modify-write ran unserialised. Two connections could read the same /etc/hosts and the later write would silently drop the earlier update. The transaction is now extracted into performHostsUpdate(entries:) and executed on routeQueue, so it is serialised with route mutations exactly as documented.

✅ Delete result discarded in the replace path. Fixed — a failed delete now returns its own error instead of letting the follow-up add fail with a secondary "exists" that hides the cause.

✅ CHANGELOG disconnect rate. You're right: 48 teardowns over 24h is one per 30 minutes. The "every 8 minutes" figure comes from ~6.5 hours of active use (the log window contained a ~4.7h sleep gap with no logging). Reworded to state that window explicitly.

✅ ROADMAP version attribution. Corrected: audit-token verification shipped in 1.7.0, the fail-closed cdhash pin in 1.8.0, and 1.9.0 is the current helper (route change-in-place, serialisation, os_log). The sentence now attributes each to its own version.

Full suite green (855 tests, 0 failures) and the universal helper builds.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@GeiserX Confirmed. The fixes address all four findings.

performHostsUpdate(entries:) now serializes the complete hosts-file transaction on routeQueue. The route replace path now preserves the original delete failure. The CHANGELOG and ROADMAP now state the correct operational and version-history context.

🐇 ✅


✏️ Learnings added
Learnt from: GeiserX
URL: https://github.com/GeiserX/VPN-Bypass/pull/66

Timestamp: 2026-08-04T12:32:01.425Z
Learning: In `Helper/HelperTool.swift`, `updateHostsFile` must execute the complete `/etc/hosts` read-modify-write transaction through `performHostsUpdate(entries:)` on `routeQueue`. This prevents concurrent XPC connections from overwriting each other's hosts-file updates and matches the queue's serialization contract for route and hosts mutations.

Learnt from: GeiserX
URL: https://github.com/GeiserX/VPN-Bypass/pull/66

Timestamp: 2026-08-04T12:32:01.425Z
Learning: In `Helper/HelperTool.swift`, when a route replacement falls back from `change` to `delete` plus `add`, return a failed delete operation's error directly. Do not attempt the add after a failed delete, because its possible "exists" error obscures the original failure.

Learnt from: GeiserX
URL: https://github.com/GeiserX/VPN-Bypass/pull/66

Timestamp: 2026-08-04T12:32:01.425Z
Learning: For the VPN Bypass 3.1.7 CHANGELOG, 48 route teardowns occurred over a 24-hour log window, equivalent to one teardown per 30 minutes. The approximately one-per-8-minute rate applies only to about 6.5 hours of active use because the log window included an approximately 4.7-hour sleep gap.

Learnt from: GeiserX
URL: https://github.com/GeiserX/VPN-Bypass/pull/66

Timestamp: 2026-08-04T12:32:01.425Z
Learning: In `ROADMAP.md`, attribute privileged-helper security and routing features by release: audit-token verification shipped in helper version 1.7.0, fail-closed cdhash pinning shipped in 1.8.0, and route change-in-place, route and hosts serialization, and `os_log` shipped in 1.9.0.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@GeiserX
GeiserX merged commit f8ab4ac into main Aug 4, 2026
3 checks passed
@GeiserX
GeiserX deleted the fix/issue-65-route-churn branch August 4, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Helper churns per-host kernel routes in steady state, destabilising coexisting VPN (reopens bead 3sc.4)

1 participant