Skip to content

Instantly share code, notes, and snippets.

@jlmalone
Created June 27, 2025 12:12
Show Gist options
  • Save jlmalone/22f07720f162d7711d6714770d285822 to your computer and use it in GitHub Desktop.
Save jlmalone/22f07720f162d7711d6714770d285822 to your computer and use it in GitHub Desktop.
darkmesh - a compatibility and network splitting tool for tailscale and other vpns eg ExpressVpn
#!/bin/zsh
#
# =============================================================================
# macOS Split-Tunnel Script for a Full VPN and Tailscale
# =============================================================================
#
# Copyright (c) 2025 Salient Vision Technologies, LLC
# Auhor: jlmalone
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# =============================================================================
#
# --- DISCLAIMER ---
#
# This script modifies your system's network routing table and DNS settings.
# Incorrectly modifying these settings can lead to network outages, DNS leaks,
# or other security vulnerabilities. The author provides this script "as is"
# without any warranty and assumes NO responsibility for any damages or security
# issues that may arise from its use. You are solely responsible for
# understanding what this script does and for the security of your system.
#
# ALWAYS REVIEW THE SCRIPT'S CODE BEFORE EXECUTING IT.
#
# =============================================================================
#
# --- DOCUMENTATION ---
#
# PROBLEM:
# When a commercial VPN is active, it routes ALL traffic through its tunnel,
# which breaks connectivity to your private Tailscale network.
#
# SOLUTION:
# This script intelligently finds the Tailscale network and adds a specific
# route for it, carving it out from the main VPN tunnel. It also configures
# DNS so that Tailscale's MagicDNS works alongside the VPN's DNS.
#
# PREREQUISITES:
# 1. Tailscale CLI is installed (`brew install tailscale`).
# 2. Your commercial VPN's "Kill Switch" feature is DISABLED. A kill
# switch works by blocking all traffic that doesn't use the VPN interface,
# which will block Tailscale even with the correct routes.
#
# USAGE:
# 1. Connect to Tailscale first.
# 2. Connect to your commercial VPN second.
# 3. Run this script: ./darkmesh.sh --apply
#
# TO REVERT:
# - Disconnect from your VPN and run: ./darkmesh.sh --undo
#
# EXAMPLE OUTPUT:
#
# user@macbook ~ % ./darkmesh.sh --apply
# › Step 1: Running prerequisite checks...
# › --------------------------------------------------------------------
# › NOTE: Your VPN's 'Kill Switch' feature must be disabled.
# › --------------------------------------------------------------------
# › Step 2: Finding the active Tailscale network interface...
# › ✅ Success! Found Tailscale on interface: utun4
# › Step 3: Configuring DNS for MagicDNS (*.ts.net)...
# Password:
# › ✅ DNS configured.
# › Step 4: Configuring network routing for Tailscale subnet...
# delete net 100.64.0.0: not in table
# add net 100.64.0.0: gateway utun4
# › ✅ Routing configured.
# › Step 5: Flushing system DNS cache...
# › ✅ Cache flushed.
#
# › ======================================================
# › ✅ Split-Tunnel is ACTIVE
# › To revert, disconnect the VPN and run: ./darkmesh.sh --undo
# › ======================================================
#
# =============================================================================
# Exit immediately if a command exits with a non-zero status.
set -euo pipefail
# --- Configuration ---
# These values are standard for Tailscale and should not need changing.
TS_SUBNET="100.64.0.0/10"
TS_DNS_IP="100.100.100.100"
TS_RESOLVER_FILE="/etc/resolver/ts.net"
# --- Helper Functions ---
msg() { echo "$1"; }
die() { echo "❌ ERROR: $1" >&2; exit 1; }
need_cmd() { command -v "$1" >/dev/null || die "'$1' command not found. Please ensure it's in your \$PATH."; }
# Dynamically finds the network interface name (e.g., "utun5") that corresponds
# to the machine's Tailscale IP address. This awk-based parser is highly robust.
get_tailscale_interface() {
local ts_ip
local ts_interface
ts_ip=$("$TS_CMD" ip -4) || die "Could not get Tailscale IP. Is Tailscale connected and authenticated?"
[[ -z "$ts_ip" ]] && die "Tailscale is not connected or has no IP address."
# The awk command searches for the interface associated with the Tailscale IP.
ts_interface=$(ifconfig | awk -v search_ip="inet $ts_ip " '
/^[a-z0-9]+:/ { iface = $1; gsub(":", "", iface) }
$0 ~ search_ip { print iface; exit }
')
[[ -z "$ts_interface" ]] && die "Could not find an active network interface for Tailscale IP '$ts_ip'."
echo "$ts_interface"
}
# --- Main Functions ---
function apply_split_tunnel() {
msg "Step 1: Running prerequisite checks..."
need_cmd tailscale
export TS_CMD=$(command -v tailscale)
msg "--------------------------------------------------------------------"
msg "NOTE: Your VPN's 'Kill Switch' feature must be disabled."
msg "--------------------------------------------------------------------"
sleep 1
msg "Step 2: Finding the active Tailscale network interface..."
TS_INTERFACE=$(get_tailscale_interface)
msg "✅ Success! Found Tailscale on interface: $TS_INTERFACE"
msg "Step 3: Configuring DNS for MagicDNS (*.ts.net)..."
sudo "$TS_CMD" set --accept-dns=false
sudo mkdir -p "$(dirname "$TS_RESOLVER_FILE")"
printf "nameserver %s\ndomain ts.net\n" "$TS_DNS_IP" | sudo tee "$TS_RESOLVER_FILE" > /dev/null
msg "✅ DNS configured."
msg "Step 4: Configuring network routing for Tailscale subnet..."
sudo route -n delete -net "$TS_SUBNET" 2>/dev/null || true
sudo route -n add -net "$TS_SUBNET" -interface "$TS_INTERFACE"
msg "✅ Routing configured."
msg "Step 5: Flushing system DNS cache..."
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder || true
msg "✅ Cache flushed."
echo
msg "======================================================"
msg "✅ Split-Tunnel is ACTIVE"
msg "To revert, disconnect the VPN and run: $0 --undo"
msg "======================================================"
}
function undo_split_tunnel() {
msg "Step 1: Running prerequisite checks..."
need_cmd tailscale
export TS_CMD=$(command -v tailscale)
msg "Step 2: Deleting the custom network route..."
sudo route -n delete -net "$TS_SUBNET" 2>/dev/null || true
msg "Step 3: Restoring Tailscale's default DNS management..."
sudo "$TS_CMD" set --accept-dns=true
msg "Step 4: Removing the custom DNS resolver file..."
sudo rm -f "$TS_RESOLVER_FILE"
msg "Step 5: Flushing system DNS cache..."
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder || true
echo
msg "======================================================"
msg "✅ Cleanup Complete. Network settings reverted."
msg "======================================================"
}
# --- SCRIPT ENTRY POINT ---
if [[ $# -ne 1 ]]; then
echo "Usage: $0 --apply | --undo"
exit 1
fi
case "$1" in
--apply) apply_split_tunnel ;;
--undo) undo_split_tunnel ;;
*) echo "Invalid argument: $1"; echo "Usage: $0 --apply | --undo"; exit 1 ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment