Last active
October 7, 2026 15:41
-
-
Save dlenski/89028b8341ac5ac95b045dcf2c0a2386 to your computer and use it in GitHub Desktop.
Windows Powershell script to route some traffic away from a VPN (poor man's https://github.com/dlenski/vpn-slice for Windows)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -RunAsAdministrator | |
| ##### | |
| # | |
| # This is a Windows Powershell script to modify your computer's routing table to send traffic to certain IPv4 addresses | |
| # or network ranges through a physical network adapter rather than through a VPN. | |
| # | |
| # It's basically a poor-man's Windows-specific version of https://github.com/dlenski/vpn-slice | |
| # | |
| # For example, let's say that your workplace VPN blocks SSH connections to external hosts, even though | |
| # you use Azure DevOps git repositories for work (real use case for me 🤷🏻♂️), then you can use this to | |
| # route traffic to Azure DevOps git/ssh servers around the VPN. | |
| # | |
| # © 2026- Daniel Lenski <dlenski@gmail.com> | |
| # | |
| # Basic steps to use: | |
| # | |
| # 0) Modify the $dests array below to contain your desired list of VPN-avoiding hosts. | |
| # 1) AFTER connecting your workplace VPN... | |
| # 2) Run this script as Administrator. For example, using https://github.com/gerardog/gsudo: | |
| # `gsudo powershell -File "C:\Path\To\ThisScript.ps1"` | |
| # 3) Enjoy working network connections that bypass the VPN. | |
| # | |
| ##### | |
| $dests = @( | |
| # List of IPv4 addresses (e.g. 1.2.3.4),. IPv4 network ranges (e.g. 1.2.3.0/24) | |
| # or hostnames (e.g. server.company.com) to route via the physical adapter. | |
| # Hostnames will be resolved once, when this script is executed, via default | |
| # DNS servers, and all matching IPv4 addresses will be routed. | |
| "140.82.112.0/20", # ssh to github.com (see https://api.github.com/meta) | |
| "172.65.251.78", # ssh to gitlab.com (seems to be fixed IP) | |
| "ssh.dev.azure.com", "vs-ssh.visualstudio.com" # Azure DevOps git ssh | |
| ) | |
| ########### | |
| # Find all network adapters, including the first real and virtual adapters. | |
| $realnet = $null | |
| $vpnnet = $null | |
| foreach ($net in Get-NetAdapter | Where-Object Status -eq 'Up' | Sort-Object ifIndex) { | |
| if ($net.Virtual -and $vpnnet -eq $null) { $vpnnet = $net } | |
| elseif (!$net.Virtual -and $realnet -eq $null) { $realnet = $net } | |
| } | |
| if ($realnet -eq $null) { | |
| Write-Error "No currently-UP physical network adapter found, cannot proceed" | |
| exit 1 | |
| } | |
| if ($vpnnet -eq $null) { | |
| Write-Warning "No currently-UP virtual network adapter found. This script will not have the desired effect until after VPN is already started." | |
| exit 1 | |
| } | |
| # Find the default IPv4 route of the "real" network adapter | |
| $defroute = (Get-NetRoute -AddressFamily IPv4 ` | |
| -InterfaceIndex $realnet.ifIndex ` | |
| -DestinationPrefix '0.0.0.0/0' ` | |
| -ErrorAction SilentlyContinue) | |
| if ($defroute -eq $null) { | |
| Write-Error "No default IPv4 route (0.0.0.0/0) found for network adapter $($realnet.ifIndex) ($($realnet.Name))" | |
| exit 1 | |
| } | |
| $gw = $defroute.NextHop | |
| Write-Host "Found nexthop/gateway of $gw for default IPv4 route (0.0.0.0/0) on network adapter $($realnet.ifIndex) ($($realnet.Name))" | |
| # Add or modify routes | |
| foreach ($dest in $dests) { | |
| if ($dest -match "(\d+\.\d+\.\d+.\d+)(?:/(\d+))?") { | |
| # It's already an IP address or IP network range | |
| $netaddr = $Matches[1] | |
| if ($Matches[2] -eq $null) { $netmask = 32 } else { $netmask = $Matches[2] } | |
| Write-Host "Routing $netaddr/$netmask via $gw" | |
| try { | |
| Set-NetRoute -AddressFamily IPv4 -DestinationPrefix "$netaddr/$netmask" -InterfaceIndex $realnet.ifIndex -NextHop $gw -ErrorAction Stop | |
| Write-Host " (updated existing route)" | |
| } catch { | |
| New-NetRoute -AddressFamily IPv4 -DestinationPrefix "$netaddr/$netmask" -InterfaceIndex $realnet.ifIndex -NextHop $gw -ErrorAction Stop | |
| } | |
| } else { | |
| # Need to resolve hostname to IP address(es) | |
| $ips = (Resolve-DnsName -Type A $dest).IPAddress | |
| Write-Host "Got $($ips.Count) IPv4 addresses for $dest, routing via $gw" | |
| foreach ($ip in $ips) { | |
| try { | |
| Set-NetRoute -AddressFamily IPv4 -DestinationPrefix "$ip/32" -InterfaceIndex $realnet.ifIndex -NextHop $gw -ErrorAction Stop | |
| Write-Host " (updated existing route)" | |
| } catch { | |
| New-NetRoute -AddressFamily IPv4 -DestinationPrefix "$ip/32" -InterfaceIndex $realnet.ifIndex -NextHop $gw -ErrorAction Stop | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment