From b9c8b5d1bf60e6faf12e2e454b27ff21449d6d38 Mon Sep 17 00:00:00 2001
From: GeiserX <9169332+GeiserX@users.noreply.github.com>
Date: Tue, 24 Feb 2026 14:31:50 +0100
Subject: [PATCH] fix: Zscaler and WARP rejected in CGNAT IP range
Zscaler uses 100.64.x.x which falls in the CGNAT range (100.64.0.0/10).
This range was treated as Tailscale-only, so isCorporateVPNIP() called
isTailscaleExitNodeActive() which returned false, rejecting the interface.
The Cloudflare WARP check (100.96-111) was also dead code since it's a
subset of the CGNAT range that returned before reaching it.
Fix: pass the process-detection hint into isCorporateVPNIP. When a known
non-Tailscale VPN is running, trust it for CGNAT IPs. Only fall back to
Tailscale exit-node check when no other VPN process is detected.
Closes #7
---
Casks/vpn-bypass.rb | 2 +-
Info.plist | 2 +-
README.md | 2 +-
Sources/RouteManager.swift | 28 ++++++++++------------------
Sources/SettingsView.swift | 4 ++--
docs/CHANGELOG.md | 5 +++++
6 files changed, 20 insertions(+), 23 deletions(-)
diff --git a/Casks/vpn-bypass.rb b/Casks/vpn-bypass.rb
index 181b161..59d1665 100644
--- a/Casks/vpn-bypass.rb
+++ b/Casks/vpn-bypass.rb
@@ -3,7 +3,7 @@
# Or if using local tap: brew install --cask --no-quarantine ./Casks/vpn-bypass.rb
cask "vpn-bypass" do
- version "1.7.0"
+ version "1.7.1"
sha256 "37b127a55aec0bdb80e824e59e840ce5b529c09086aac7fc24dc4616abb817bd"
url "https://github.com/GeiserX/VPN-Bypass/releases/download/v#{version}/VPN-Bypass-#{version}.dmg"
diff --git a/Info.plist b/Info.plist
index a3f1f59..b573d25 100644
--- a/Info.plist
+++ b/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.7.0
+ 1.7.1
CFBundleVersion
19
LSMinimumSystemVersion
diff --git a/README.md b/README.md
index 643312b..2b26e7a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
## Why?
diff --git a/Sources/RouteManager.swift b/Sources/RouteManager.swift
index 4cd1d55..fbd801e 100644
--- a/Sources/RouteManager.swift
+++ b/Sources/RouteManager.swift
@@ -712,7 +712,7 @@ final class RouteManager: ObservableObject {
var vpnCandidates: [(name: String, ip: String, isValid: Bool)] = []
for i in interfaces.indices {
- let isValid = await isCorporateVPNIP(interfaces[i].ip)
+ let isValid = await isCorporateVPNIP(interfaces[i].ip, hintType: hintType)
interfaces[i].isValidCorporateIP = isValid
// Track VPN candidates for debugging
@@ -765,7 +765,8 @@ final class RouteManager: ObservableObject {
}
/// Check if IP is likely a corporate VPN (not Tailscale mesh, not localhost, etc.)
- private func isCorporateVPNIP(_ ip: String) async -> Bool {
+ /// hintType comes from process detection -- used to distinguish Zscaler/WARP from Tailscale in the shared CGNAT range.
+ private func isCorporateVPNIP(_ ip: String, hintType: VPNType?) async -> Bool {
let parts = ip.components(separatedBy: ".")
guard parts.count == 4,
let first = Int(parts[0]),
@@ -779,29 +780,20 @@ final class RouteManager: ObservableObject {
// Skip link-local
if first == 169 && second == 254 { return false }
- // Tailscale CGNAT range (100.64.0.0/10 = 100.64-127.x.x)
- // Only consider Tailscale as VPN if it's using an exit node (routing all traffic)
+ // CGNAT range (100.64.0.0/10 = 100.64-127.x.x)
+ // Shared by Tailscale, Zscaler, Cloudflare WARP, and other VPNs.
+ // If a known non-Tailscale VPN process was detected, trust it.
+ // Otherwise fall back to Tailscale exit-node check.
if first == 100 && second >= 64 && second <= 127 {
+ if let hint = hintType, hint != .tailscale, hint != .unknown {
+ return true
+ }
return await isTailscaleExitNodeActive()
}
- // Cloudflare WARP range (check for WARP-specific IPs)
- // WARP uses 100.96.0.0/12 range
- if first == 100 && second >= 96 && second <= 111 {
- return true // WARP is active
- }
-
- // Zscaler typically uses 100.64.x.x or custom ranges
- // Already covered by CGNAT check above
-
// Corporate VPNs typically use private ranges
- // 10.0.0.0/8 - Most corporate VPNs use this
if first == 10 { return true }
-
- // 172.16.0.0/12 (172.16-31.x.x)
if first == 172 && second >= 16 && second <= 31 { return true }
-
- // 192.168.0.0/16 - Less common for VPN but possible
if first == 192 && second == 168 { return true }
return false
diff --git a/Sources/SettingsView.swift b/Sources/SettingsView.swift
index 1b0f920..78a7ffa 100644
--- a/Sources/SettingsView.swift
+++ b/Sources/SettingsView.swift
@@ -1263,7 +1263,7 @@ struct GeneralTab: View {
HStack {
VStack(alignment: .leading, spacing: 2) {
BrandedAppName(fontSize: 13)
- Text("Version 1.7.0")
+ Text("Version 1.7.1")
.font(.system(size: 11))
.foregroundColor(Color(hex: "6B7280"))
}
@@ -1721,7 +1721,7 @@ struct InfoTab: View {
// App name with branded colors
BrandedAppName(fontSize: 24)
- Text("v1.7.0")
+ Text("v1.7.1")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(Color(hex: "6B7280"))
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 742e915..92b520f 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -5,6 +5,11 @@ All notable changes to VPN Bypass will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.7.1] - 2026-02-24
+
+### Fixed
+- **Zscaler Detection** - Zscaler (and Cloudflare WARP) use CGNAT IPs (`100.64.x.x`) which were incorrectly treated as Tailscale-only, causing `valid=false` rejection. Now trusts the process-detection hint to distinguish Zscaler/WARP from Tailscale in the shared CGNAT range.
+
## [1.7.0] - 2026-02-22
### Added