前言

上回说到了通过 VPN 进行加速,有不少朋友已经体会到了 VPN 的好处,许多网站从缓慢到无法访问,变得可以流畅的访问了。但是,我们经常面临一个问题,VPN 拨通后,所有流量都会流经 VPN,导致本地的网络访问可能会很不顺畅,有的速度非常缓慢,有的甚至不能访问。特别是对于那些在使用 VPN 同时还在进行本地下载的用户,这个问题更加明显。对于那些收费的 VPN 而言,因为它们速度较快,所以感觉问题不大。但是,对于那些免费的、速度比较缓慢、甚至限制流量的 VPN 来说,这就是一个比较严重的问题了。很多人不得不同时只干一件事情,要使用 VPN 就停止本地网络的访问,要访问本地网络就需要断开 VPN。那么,可不可以只有访问镇外的流量走 VPN,而本地网络依旧使用本地连接呢?答案自然是可以的。

解决方案

其实这个问题在使用 VPN 之前就存在过。使用过教育网的朋友肯定有经验,国内流量是免费的,而国外流量是收费的。一般来说大家都会访问国内网络,但是毕竟偶尔也需要访问国外网络。有的学校做的比较一刀切,干脆禁止国外流量,而有的学校则收取国外流量高昂的费用。所以,在教育网生活的时间,大家都到处寻找国内代理服务器,一时间代理服务器搜索软件、代理服务器列表比比皆是。当时我们学校面临这个问题的时候,我们采用了另外一种办法,除了教育网光纤接入外,我们还申请了电信的包月 ADSL 作为第二链路。当时对于非服务器的网段使用的是 NAT,分别在教育网和ADSL接入上做NAT。然后,修改路由设置,凡是国内流量,都路由到教育网光纤接口;凡是国外流量,都路由到电信的 ADSL 接口。虽然,国外流量的带宽不大,但是对于当时的学生对国外网站的需求不高的情况下,这个问题还是得到了很好的解决。

再回过来看我们 VPN 的问题,会发现其实是一个问题。我们可以添加路由,将所有本地的网络(如上例的所有国内网络范围),路由到我们使用 VPN 前的默认网关。而剩余的,也就是国外的网络范围,自动流经 VPN。这就很好的解决了 VPN 降低本地网络访问速度的问题。

然后,我们需要寻找到本地的 IP 地址范围,在本例中,我使用的是国内的地址范围。因为这个数据库属于公开数据,我们可以从 CNNIC 或者 APNIC 获得相关数据。剩下的就是对地址库进行分析,然后生成路由添加和删除的脚本就可以了,下列脚本只对 VPN 类的链路性的加速有意义,对于SOCKS类的代理是一点帮助也没有。

由于我的机器分别有 Ubuntu 和 Windows 7,因此,我将针对这两个系统写脚本。脚本语言不同,但是参数是一样的。

  • on    表明开启国内路由走默认网关的配置。也就是说添加国内网络的路由指向原默认网关。
  • once    表明只开启国内路由走默认网关的配置一次,重新启动系统后配置将丢失。
  • off    表明关闭这个功能。也就是说删除之前操作所添加的路由。
  • update    是指下载更新 IP 地址库。

代码编写以及使用

Linux

对于 Linux 最通用的莫过于 Shell 了,因此,我使用 Bash 完成了这个脚本。虽然我是在 Ubuntu 10.10 系统上测试的,但是它应该支持大部分 Debian 派生系统。我也写了 Redhat 及其派生系统的支持代码,不过没有进行测试罢了,有环境的朋友可以测试后告诉我在什么版本上可以执行,或者是有什么问题,我会修改的。

#!/bin/bash
LANG=C
PROG=$0
URL_CNNIC="http://ipwhois.cnnic.cn/ipstats/detail.php?obj=ipv4&country=CN"
IPV4_HTML=/tmp/cnnic_ipv4.html
IPV4_DATA=~/cnnic_ipv4.data
VPN_IF=`ip tuntap | cut -d ':' -f 1`

if [ -z "$VPN_IF" ] ; then
	VPN_IF="tun"
fi

GATEWAY=`route -n | grep -v "$VPN_IF" | grep UG | tr -s ' ' | cut -d ' ' -f 2`

if [ "`echo "$GATEWAY" | wc -l`" -gt "1" ] ; then
	ROUTE_ADDED=true
	GATEWAY=`echo "$GATEWAY" | head -n 1`
fi

GATEWAY_IF=`route -n | grep $GATEWAY | head -n 1 | tr -s ' ' | cut -d ' ' -f8`

IP_PROG=`which ip`

ROUTE_DIR_DEBIAN=/etc/network/if-up.d
ROUTE_FILE_DEBIAN=$ROUTE_DIR_DEBIAN/vpnroute

ROUTE_DIR_REDHAT=/etc/sysconfig/network-scripts
ROUTE_FILE_REDHAT=$ROUTE_DIR_REDHAT/route-$GATEWAY_IF

function get_cn_data() {
	echo "Downloading CN network data from CNNIC..."
	RE_PATTERN="s/^.*searchtext=\([./0-9]*\).*$/\1/"
	wget -O $IPV4_HTML $URL_CNNIC
	grep "whois.pl?" $IPV4_HTML | sed -e "$RE_PATTERN" > $IPV4_DATA
	chmod a+w $IPV4_HTML
	chmod a+w $IPV4_DATA
	echo "Done."
}

function check_data() {
	if [ ! -f $IPV4_DATA ]
	then
		echo "CN network data file [$IPV4_DATA] is not exist."
		get_cn_data
	fi
}

function add_route_cn() {
	check_data
	PERMANENT="$1"
	# Check whether the routes are already added
	if [ "$ROUTE_ADDED" == "true" ] ; then
		echo "Routes already added, should be removed first."
		remove_route_cn
		GATEWAY=`echo "$GATEWAY" | head -n 1`
	fi
	echo "Adding routes of CN ..."
	echo "Default gateway is $GATEWAY on dev $GATEWAY_IF"
	if [ "$PERMANENT" == "yes" ] ; then
		echo "Generating route config file for next boot. [$ROUTE_FILE_DEBIAN]"
		# Debian/Ubuntu
		if [ -d $ROUTE_DIR_DEBIAN ] ; then
			# Prepare ROUTE_FILE_DEBIAN header
			rm -f $ROUTE_FILE_DEBIAN
			echo "#!/bin/bash" >> $ROUTE_FILE_DEBIAN
			chmod a+x $ROUTE_FILE_DEBIAN
		fi
		# Redhat/Fedora
		if [ -d $ROUTE_DIR_REDHAT ] ; then
			# Use backup overwrite current file, otherwise backup the file
			if [ -x "$ROUTE_FILE_REDHAT.bak" ] ; then
				cp -f "$ROUTE_FILE_REDHAT.bak" "$ROUTE_FILE_REDHAT"
			else
				cp -f "$ROUTE_FILE_REDHAT" "$ROUTE_FILE_REDHAT.bak"
			fi
		fi
	fi

	count=0
	while read line
	do
		net=`echo "$line" | cut -d '/' -f1`
		cidr=`echo "$line" | cut -d '/' -f2`

		# generate ip route cmd
		ip_args="$net/$cidr via $GATEWAY"
		if [ "$PERMANENT" == "yes" ]
		then
			# Debian/Ubuntu
			if [ -d $ROUTE_DIR_DEBIAN ] ; then
				echo "$IP_PROG route add $ip_args" >> $ROUTE_FILE_DEBIAN
			fi
			# Redhat/Fedora
			if [ -d $ROUTE_DIR_REDHAT ] ; then
				echo "$ip_args" >> $ROUTE_FILE_REDHAT
			fi
		fi
		$IP_PROG route add $ip_args
		count=$(( $count + 1 ))
	done < $IPV4_DATA
	echo "Added $count networks."
	echo "Done."
}

function remove_route_cn() {
	check_data
	echo "Removing routes of CN ..."
	# Debian/Ubuntu
	if [ -x $ROUTE_FILE_DEBIAN ] ; then
		rm -f $ROUTE_FILE_DEBIAN
	fi
	# Redhat/Fedora
	if [ -x $ROUTE_FILE_REDHAT ] ; then
		# If the backup exist, then use backup overwrite current file, otherwise filter the file
		if [ -x "$ROUTE_FILE_REDHAT.bak" ] ; then
			mv -f $ROUTE_FILE_REDHAT.bak $ROUTE_FILE_REDHAT
		else
			cp -f $ROUTE_FILE_REDHAT $ROUTE_FILE_REDHAT.old
			sed "/ via $GATEWAY$/d" $ROUTE_FILE_REDHAT > $ROUTE_FILE_REDHAT.tmp
			mv -f $ROUTE_FILE_REDHAT.tmp $ROUTE_FILE_REDHAT
		fi
	fi

	count=0
	while read line
	do
		net=`echo "$line" | cut -d '/' -f1`
		cidr=`echo "$line" | cut -d '/' -f2`
		$IP_PROG route del $net/$cidr via $GATEWAY 2> /dev/null
		count=$(( $count + 1 ))
	done < $IPV4_DATA
	echo "Removed $count networks."
	echo "Done."
}

function usage() {
	echo "Route networks of CN to the default gateway instead of VPN tunnel"
	echo ""
	echo "usage: $PROG {on|off|update}"
	echo ""
	echo "	on	Add routes of CN to default gateway."
	echo "	once	Add routes of CN to default gateway. Only for this time, reboot the configure will be disappeared."
	echo "	off	Remove routes of CN"
	echo "	update	Force download/update CN network data"
	echo ""
}

case "$1" in
	"on")		add_route_cn yes	;;
	"once")		add_route_cn no		;;
	"off")		remove_route_cn		;;
	"update")	get_cn_data		;;
	*)		usage			;;
esac

下载脚本到本地

wget http://files.cnblogs.com/dancefire/vpnroute.zip
unzip vpnroute.zip
sudo bash vpnroute.sh on 

需要删除添加的路由,可以执行:

sudo bash vpnroute.sh off 

需要更新网络路由数据,可以执行:

bash vpnroute.sh update 

Windows

Windows 的批处理文件虽然也能写一些东西,但是总觉得它的功能太受限了。因此,这次我使用 PowerShell 来写这个脚本。PowerShell 是微软 2006 年推出的抗衡 *nix 中的 shell 的产品,通过紧密结合 .Net Framework 大幅提高命令行及脚本的能力。熟悉 linux shell 脚本的朋友会在使用中会发现有很多语法似曾相识。虽然有各种不适应,但是不得不说 PowerShell 还是很强大的。

$prog = $myinvocation.mycommand.name
$url_cnnic = "http://ipwhois.cnnic.cn/ipstats/detail.php?obj=ipv4&country=CN"
$curdir = (get-location).path
$ipv4_html = join-path $curdir "cnnic_ipv4.html"
$ipv4_data = join-path $curdir "cnnic_ipv4.data"
$gateway = route print -4 | where { $_ -match "^\s+0.0.0.0\s+0.0.0.0" } | %{ $_ -replace '\s+', ' ' } | %{ $_.Split(" ")[3] }

function cidr-to-mask($cidr) {
	[string]$mask = ""
	$full_octets = $cidr/8
	$partial_octet = $cidr%8
	
	for ($i = 0; $i -lt 4; $i++) {
		$a = $full_octets - $i
		if (($a -gt 0) -and ($a -ge 1)) {
			$mask += "255"
		} elseif (($a -gt 0) -and ($a -lt 1)) {
			$mask += (256 - [math]::Pow(2,(8-$partial_octet)))
		} else {
			$mask += "0"
		}
		if ($i -lt 3) { $mask += "." }
	}
	return $mask
}

function get-cn-data {
	Write-Host "Downloading CN network data from CNNIC..."
	$webclient = new-object System.Net.WebClient
	$webclient.DownloadFile($url_cnnic, $ipv4_html)
	
	cat $ipv4_html | where { $_ -match "whois.pl?" } | % { $_ -replace "^.*searchtext=([.\d/]+).*$", '$1' } > $ipv4_data
	Write-Host "Done."
}

function check-data {
	if (!(test-path $ipv4_data)) {
		Write-Host "CN network data file [$ipv4_data] is not exist."
		get-cn-data
	}
}

function add-route-cn([bool]$permanent) {
	check-data

	$route_count = route print -4 | where { $_ -match "$gateway" } | measure-object
	if ($route_count > 1) {
		Write-Host "Routes already added, should be removed first."
		remove-route-cn
	}

	Write-Host "Adding routes of CN ..."
	Write-Host "Default gateway is $gateway"
	$count = 0
	foreach ($line in Get-Content $ipv4_data)
	{
		$item = $line.split("/")
		$net = $item[0]
		$cidr = $item[1]
		if (($cidr -ge 0) -and ($cidr -le 32))
		{
			$mask = cidr-to-mask($cidr)
			$opt = ""
			if ($permanent) {
				$opt = "-p"
			}
			route.exe add $opt $net mask $mask $gateway > null
			$count++
		}
	}
	Write-Host "Added $count networks."
	Write-Host "Done."
}

function remove-route-cn {
	check-data
	Write-Host "Removing routes of CN ..."
	$count = 0
	foreach ($line in Get-Content $ipv4_data)
	{
		$item = $line.split("/")
		$net = $item[0]
		$cidr = $item[1]
		if (($cidr -ge 0) -and ($cidr -le 32))
		{
			$mask = cidr-to-mask($cidr)
		}
		route delete $net mask $mask > null
		$count++
	}
	Write-Host "Removed $count networks."
	Write-Host "Done."
}

function usage {
	Write-Host "Route networks of CN to the default gateway instead of VPN tunnel"
	Write-Host ""
	Write-Host "usage: $prog {on|off|update}"
	Write-Host ""
	Write-Host "	on	Add routes of CN to default gateway"
	Write-Host "	once	Add routes of CN to default gateway. Only for this time, reboot the configure will be disappeared."
	Write-Host "	off	Remove routes of CN"
	Write-Host "	update	Download/update CN network data"
	Write-Host ""
}

switch($args[0]) {
	"on"		{	add-route-cn($true)	}
	"once"		{	add-route-cn($false)	}
	"off"		{	remove-route-cn		}
	"update"	{	get-cn-data		}
	default		{	usage			}
}

下载脚本 http://files.cnblogs.com/dancefire/vpnroute.zip 到某个目录,比如 d:\tmp

修改 PowerShell 执行权限,默认只能执行签名脚本。:

开始->附件->Windows PowerShell->右键点击 Windows PowerShell->以管理员身份执行

在命令行里执行:

Set-ExecutionPolicy -ExecutionPolicy Unrestricted

可能会提示如下信息:

执行策略更改
执行策略可以防止您执行不信任的脚本。更改执行策略可能会使您面临
about_Execution_Policies 
帮助主题中所述的安全风险。是否要更改执行策略?
[Y] 是(Y) [N] 否(N) [S] 挂起(S) [?] 帮助 (默认值为“Y”):

选择 Y,或者回车即可。

然后,进入下载文件所在目录,执行脚本

cd d:\tmp
.\vpnroute.ps1

如果脚本可以执行,它会返回如下帮助信息:

PS d:\tmp> .\vpnroute.ps1
Route networks of CN to the default gateway instead of VPN tunnel

usage: vpnroute.ps1 {on|off|update}

	on	Add routes of CN to default gateway
	once	Add routes of CN to default gateway. Only for this time, reboot the configure will be disappeared.
	off	Remove routes of CN
	update	Force download/update CN network data

如要添加路由,希望所有中国范围的IP使用默认的网关访问,就运行:

.\vpnroute.ps1 on

如果想要去掉添加的路由,就运行

.\vpnroute.ps1 off

想更新中国IP路由数据,就运行

.\vpnroute.ps1 update

我注意到 Windows 的 PowerShell 脚本执行速度不是很高,在 Linux 上使用 Bash 添加路由大约只需要10-20秒,在 Windows 上使用 PowerShell 需要 70-80 秒。大家如果看到有段时间没响应,不要着急,给他一些时间就会好的。

连接后,分别访问 http://www.ip.cnhttp://whatismyipaddress.com 以测试路由设置是否正常。

如果路由添加正确,访问国内网站 http://www.ip.cn 应该看到的是你国内的 ip。而访问国外网站 http://whatismyipaddress.com ,应该看到的是你 vpn 的 ip。