Skip to content

feat: VPN compatibility — auto-detect and guide split-routing#61

Open
Tinghecui wants to merge 5 commits intonmhjklnm:masterfrom
Tinghecui:feat/vpn-compat
Open

feat: VPN compatibility — auto-detect and guide split-routing#61
Tinghecui wants to merge 5 commits intonmhjklnm:masterfrom
Tinghecui:feat/vpn-compat

Conversation

@Tinghecui
Copy link
Copy Markdown
Collaborator

@Tinghecui Tinghecui commented Apr 2, 2026

Summary

VPN Compatibility

  • Auto-detect local VPN software (Shadowrocket, Clash/mihomo, sing-box, V2Ray, Surge, v2rayN) that may hijack cac proxy traffic via TUN mode
  • Clash auto-inject: automatically add IP-CIDR,<proxy_ip>/32,DIRECT rule via RESTful API + config reload
  • Manual guides: step-by-step instructions for Shadowrocket, v2rayN, sing-box, Surge with correct rule format
  • Cross-platform: Bash (macOS/Linux/Git Bash) + PowerShell (Windows native)
  • Wrapper startup hint: lightweight VPN detection on every claude invocation

IP Watchdog (continuous exit-IP monitoring)

  • Background process: checks exit IP every 60s through proxy (multi-source: ipify/ip.sb/ip.3322.net/ipinfo)
  • Baseline tracking: establishes expected IP on first run, detects changes
  • Alerts: macOS system notification + statusline turns red + log file
  • Statusline integration: shows IP:x.x.x.x (green=stable, red=changed)
  • cac check integration: compares current vs expected IP, shows watchdog status

Problem

When users run cac with a proxy behind local VPN software (Clash, Shadowrocket, etc.), the VPN's TUN mode hijacks proxy traffic, routing it through the VPN's exit IP instead of the configured clean proxy IP. Users need both prevention (VPN rule guidance) and detection (IP monitoring).

Platform coverage

Client macOS Linux Windows
Clash/mihomo/Verge/ClashX auto-inject auto-inject auto-inject
Shadowrocket manual guide - -
v2rayN - - manual guide
sing-box manual guide manual guide manual guide
Surge manual guide - -
V2Ray/Xray manual guide manual guide manual guide

Files changed

File Change
src/vpn_compat.sh New — VPN detection + split-routing module
src/templates.sh IP watchdog background process + statusline IP display + wrapper VPN hint
src/cmd_check.sh VPN status + IP baseline comparison + watchdog process status
cac.ps1 Full PowerShell VPN module + wrapper hint
src/cmd_env.sh Integrate VPN check into env create/set
src/cmd_self.sh vpn-ensure subcommand
src/cmd_help.sh Help documentation
build.sh Build order

Security

  • Clash API config path validated: absolute path, no traversal, yaml extension, directory allowlist
  • PowerShell: hostname resolved to IPv4 before rule generation
  • Best-effort: _vpn_ensure_compatible always returns 0, never fatal under set -e
  • All curl calls have --connect-timeout 3 --max-time 5
  • IP watchdog auto-exits on env switch / cac stop

Test plan

  • macOS: Shadowrocket detected, manual guide displayed
  • macOS: IP watchdog starts, baseline IP recorded
  • macOS: statusline shows green IP status
  • cac env create -p <proxy> triggers VPN check
  • Localhost proxy skipped (no VPN warning)
  • Rule generation correct for all VPN types
  • IPv4 validation rejects 999.999.999.999
  • Path traversal blocked in Clash config validation
  • bash build.sh passes
  • Windows: needs testing on actual Windows machine
  • IP change alert: needs VPN route change to trigger

🤖 Generated with Claude Code

Tinghecui and others added 4 commits April 2, 2026 13:34
… setup

When users run cac with a proxy behind local VPN software (Clash, Shadowrocket,
sing-box, V2Ray, Surge), the VPN's TUN mode can hijack proxy traffic, routing it
through the VPN's dirty exit IP instead of the configured clean proxy.

This adds automatic VPN detection and split-routing guidance:
- Detect running VPN processes (Shadowrocket, Clash/mihomo, sing-box, V2Ray, Surge)
- For Clash: auto-inject IP-CIDR DIRECT rule via RESTful API + config reload
- For others: show step-by-step manual guide with correct rule format
- macOS-first: Shadowrocket app guide, ClashX/Verge config paths
- Best-effort: never fatal, always returns 0 under set -e

Integrated into `cac env create -p` and `cac env set proxy`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…integration

- Wrapper startup: show VPN hint when Shadowrocket/Clash/sing-box detected
  (lightweight inline check, no dependency on vpn_compat.sh functions)
- Clash API path validation: reject traversal, non-yaml, and paths outside
  home/etc/usr/local/opt directories
- `cac env check`: show VPN detection status before proxy reachability test
- `cac self vpn-ensure`: documented in help text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bash (vpn_compat.sh):
- Windows process detection via tasklist.exe fallback
- Windows Clash config paths (%APPDATA%/clash-verge-rev, etc.)
- v2rayN detection, rule generation, and manual guide
- v2rayN config path hint on Windows

PowerShell (cac.ps1):
- Full VPN compatibility module: Detect-VPN, Find-ClashPort,
  Get-ClashSecret, Try-ClashAutoInject, Ensure-VPNCompatible
- Clash auto-inject via RESTful API (same logic as bash version)
- v2rayN, sing-box manual guides
- Integrated into Cmd-Add, Cmd-Check, wrapper (claude.cmd)
- New command: cac vpn-ensure <proxy>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rdening

Bash (vpn_compat.sh):
- Fix tasklist.exe CSV output: strip quotes before regex matching
- Support Windows absolute paths (C:/) in Clash API config validation
- Fix "Clash for Windows/profiles/" → "config.yaml" (was directory)

PowerShell (cac.ps1):
- Resolve hostname proxy to IPv4 before generating IP-CIDR rules
- Add ::1 to loopback skip list
- Harden Clash config path validation: absolute path, allowed prefixes
- Backup files with timestamp (no overwrite)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 2, 2026 07:54
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds VPN/TUN compatibility detection and split-routing guidance to prevent local VPN software (e.g., Clash/mihomo, Shadowrocket, sing-box, v2rayN) from hijacking traffic intended for the configured proxy.

Changes:

  • Introduces a Bash VPN detection + Clash auto-inject (DIRECT rule) module and integrates it into env create/set and env check flows.
  • Adds Windows PowerShell VPN detection + Clash auto-inject support and a new vpn-ensure command entrypoint.
  • Adds lightweight wrapper startup hints when a VPN client is detected.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/vpn_compat.sh New Bash module for VPN detection, rule generation, and Clash auto-inject/reload.
src/templates.sh Adds wrapper-time VPN detection hint messaging.
src/cmd_env.sh Hooks VPN compatibility check into env create and proxy set.
src/cmd_check.sh Surfaces VPN detection status in cac check.
src/cmd_self.sh Adds cac self vpn-ensure <proxy> subcommand.
src/cmd_help.sh Documents the new self subcommand in help output.
cac.ps1 Adds Windows VPN detection/auto-inject functions, vpn-ensure command, and wrapper hint changes.
build.sh Ensures vpn_compat.sh is included in the concatenation order for the built cac script.
cac Regenerated single-file script including the new VPN compatibility logic and wrapper hint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +329 to +337
out = []

for line in lines:
out.append(line)
if not inserted and line.rstrip() in ('rules:', 'rules: '):
out.append(' ' + rule)
inserted = True

if not inserted:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

In the YAML injection Python, insertion only triggers when the line is exactly 'rules:'; if the config has 'rules: # comment' or other trailing content, the rule won't be inserted and the fallback appends a second top-level 'rules:' key. Duplicate YAML keys can cause Clash to ignore the earlier rules or fail to load the config. Consider matching 'rules:' more flexibly (e.g., allow trailing whitespace/comments) and avoid adding a new 'rules:' section when one already exists but wasn’t matched.

Suggested change
out = []
for line in lines:
out.append(line)
if not inserted and line.rstrip() in ('rules:', 'rules: '):
out.append(' ' + rule)
inserted = True
if not inserted:
has_rules_section = False
out = []
for line in lines:
out.append(line)
# Consider lines like "rules:", "rules: ", or "rules: # comment"
header = line.split('#', 1)[0].strip()
if header == 'rules:':
has_rules_section = True
if not inserted:
out.append(' ' + rule)
inserted = True
if not inserted and not has_rules_section:

Copilot uses AI. Check for mistakes.
fi
if [[ -n "$_vpn_name" ]]; then
echo "[cac] hint: $_vpn_name detected. Ensure $_host has a DIRECT rule to avoid VPN hijacking" >&2
echo "[cac] hint: run 'cac self vpn-ensure' for guidance" >&2
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The wrapper hint suggests running 'cac self vpn-ensure' without arguments, but the command implementation requires a proxy URL argument and otherwise prints nothing. Either include the current proxy in the suggested command (e.g., pass $PROXY), or update the vpn-ensure subcommand to default to the active env’s proxy when is omitted.

Suggested change
echo "[cac] hint: run 'cac self vpn-ensure' for guidance" >&2
echo "[cac] hint: run 'cac self vpn-ensure \"$PROXY\"' for guidance" >&2

Copilot uses AI. Check for mistakes.
case "${1:-help}" in
update) _self_cmd_update ;;
delete|remove) cmd_delete ;;
vpn-ensure) _vpn_ensure_compatible "${2:-}" ;;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

vpn-ensure currently calls _vpn_ensure_compatible with only the second argument. If the user runs 'cac self vpn-ensure' (as suggested by the wrapper hint), ${2:-} is empty and the command becomes a no-op. Consider defaulting to the active environment’s configured proxy when no argument is provided, and/or print a usage message when is missing.

Suggested change
vpn-ensure) _vpn_ensure_compatible "${2:-}" ;;
vpn-ensure)
if [ -z "${2:-}" ]; then
_die "usage: cac self vpn-ensure <proxy>"
fi
_vpn_ensure_compatible "$2"
;;

Copilot uses AI. Check for mistakes.
Comment on lines +296 to +302
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$result = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
$success = $result.AsyncWaitHandle.WaitOne(1000)
$tcp.Close()
if ($success) { return $port }
} catch {}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Find-ClashPort's port probe treats any async connect that completes within 1s as success, but BeginConnect can complete quickly on connection refusal as well. Because EndConnect is never called (and $tcp.Connected isn’t checked), this can return 9090/9097 even when nothing is listening, leading to unreliable detection and unnecessary API calls. Consider using ConnectAsync().Wait(timeout) plus $tcp.Connected, or call EndConnect inside the try/catch to confirm the connection actually succeeded.

Suggested change
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$result = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
$success = $result.AsyncWaitHandle.WaitOne(1000)
$tcp.Close()
if ($success) { return $port }
} catch {}
$tcp = New-Object System.Net.Sockets.TcpClient
try {
# Use ConnectAsync with timeout and verify actual connection state
$connectTask = $tcp.ConnectAsync("127.0.0.1", $port)
$success = $connectTask.Wait(1000) -and $tcp.Connected
if ($success) { return $port }
} catch {
# Ignore connection errors and continue probing other ports
} finally {
$tcp.Close()
}

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +369
foreach ($line in $lines) {
$newLines += $line
if (-not $injected -and $line.Trim() -eq "rules:") {
$newLines += " $rule"
$injected = $true
}
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Auto-inject only inserts after a line where Trim() equals 'rules:'. If the config has 'rules: # comment' or other trailing content, injection will fall through to the fallback which appends a second 'rules:' section at the end of the file, potentially breaking the YAML or overriding the original rules. Consider matching 'rules:' with optional trailing whitespace/comments and never adding a new top-level rules key when one already exists.

Copilot uses AI. Check for mistakes.
Comment on lines +370 to +374
if (-not $injected) {
$newLines += ""
$newLines += "rules:"
$newLines += " $rule"
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The fallback path appends a new top-level 'rules:' section when injection didn't find an existing 'rules:' line. If the file already contains a rules section that wasn't matched (e.g., due to an inline comment), this will create duplicate keys and may cause Clash to ignore earlier rules or fail to load the config. Prefer detecting the existing rules key robustly and inserting into it, rather than creating a second one.

Copilot uses AI. Check for mistakes.
cac
fi
if [[ -n "$_vpn_name" ]]; then
echo "[cac] hint: $_vpn_name detected. Ensure $_host has a DIRECT rule to avoid VPN hijacking" >&2
echo "[cac] hint: run 'cac self vpn-ensure' for guidance" >&2
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The wrapper hint echoes 'run 'cac self vpn-ensure' for guidance', but vpn-ensure requires a proxy argument and otherwise is a no-op. Either include the active proxy in this hint (so it’s directly runnable), or make vpn-ensure default to the current env’s proxy when no argument is provided.

Suggested change
echo "[cac] hint: run 'cac self vpn-ensure' for guidance" >&2
echo "[cac] hint: run 'cac self vpn-ensure \"$PROXY\"' for guidance" >&2

Copilot uses AI. Check for mistakes.
…egration

- Background watchdog process: checks exit IP every 60s through proxy
- Establishes baseline IP on first run, saves to expected_ip
- IP change detection: macOS system notification + ip-alert file + log
- Statusline: shows green "IP:x.x.x.x" (stable) or red "IP:x.x.x.x!" (changed)
- cac check: compares current vs expected IP, shows watchdog process status
- Auto-exits on env switch or cac stop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants