一个适用于 CN 网络的 VPN 解决方案

部署图

说明

  • VPN Clients 是位于 CN 网络的 VPN 客户端。
  • ns.example.net 和 vpn.example.net 可以是同一台机器,应位于防火墙(即 the Great Firewall,下同)内。
  • radius.example.net 和 pgsql.example.net 位置无所谓,但是最好和 vpn.example.net 很近或者位于同一台机器,以便获得更短的延时。
  • ss-server.example.net 应位于防火墙外。

安装

ss-server.example.net
安装 shadowsocks-libev 服务器端
pgsql.example.net
在 FreeBSD 上安装 PostgreSQL
radius.example.net
在 FreeBSD 上安装 FreeRADIUS
ns.example.net
使用 dnsmasq 和 shadowsocks-libev 构建一个干净的域名服务器
vpn.example.net
strongSwan 的安装和配置

#cisco-ipsec, #dnsmasq, #freebsd, #freeradius, #gfw, #gfwlist, #gfwlist2dnsmasq-awk, #ikev2, #ipsec, #iptables, #letsencrypt, #postgresql, #shadowsocks-libev, #ss-redir, #ss-server, #ss-tunnel, #strongswan, #ubuntu, #vpn

strongSwan 的安装和配置

本文目标

在 Ubuntu 上安装配置 strongSwan,使用 letsencrypt 的 SSL 证书,并采用 FreeRADIUS 作为用户认证审计等。

使用 ss-redir(包含在 shadowsocks-libev) 转发非 CN 的 IP 的请求到 ss-server(包含在)。

本文测试环境

本文假设 strongSwan 的 hostname 为 vpn.example.net,IP 地址为 1.2.3.4,FreeRADIUS 的 hostname 为 radius.example.net。
所以在下面的代码中如果看到这些假设的值,请自行修改为合适自己的值。

安装

$ apt install strongswan strongswan-plugin-eap-radius strongswan-plugin-xauth-eap
$ apt install letsencrypt
$ letsencrypt certonly --standalone -d vpn.example.net -m john.doe@example.net --agree-tos

$ cp /etc/letsencrypt/live/vpn.example.net/cert.pem /etc/ipsec.d/certs/
$ cp /etc/letsencrypt/live/vpn.example.net/privkey.pem /etc/ipsec.d/private/
$ cp /etc/letsencrypt/live/vpn.example.net/chain.pem /etc/ipsec.d/cacerts/

/etc/crontab 增加:

30 0    1 * *   root    letsencrypt renew
30 1    1 * *   root    /root/bin/renew-cert.sh

其中 /root/bin/renew-cert.sh (chmod +x) 内容如下:

#!/bin/sh
from="/etc/letsencrypt/live/vpn.example.net"
to="/etc/ipsec.d"

cp "${from}/chain.pem"   "${to}/cacerts/chain.pem"
cp "${from}/cert.pem"    "${to}/certs/cert.pem"
cp "${from}/privkey.pem" "${to}/private/privkey.pem"

配置文件

/etc/sysctl.conf

开启 IPv4 包转发:

...
# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1
...

/etc/ipsec.conf

# ipsec.conf - strongSwan IPsec configuration file

# basic configuration

config setup
        # strictcrlpolicy=yes
        uniqueids = no

conn %default
        dpdaction=clear
        forceencaps=yes
        leftsubnet=0.0.0.0/0
        right=%any 
        rightsourceip=192.168.100.2/24
        rightdns=1.2.3.4

conn Cisco-IPSec
        keyexchange=ikev1
        auto=add
        left=%defaultroute
        leftauth=psk
        rightauth=psk
        rightauth2=xauth
  
conn IKEv2
        keyexchange=ikev2
        auto=add
        fragmentation=yes
        compress=yes    
        leftcert=cert.pem
        leftsendcert=always
        rightauth=eap-radius
        eap_identity=%identity
  
# Windows and BlackBerry clients usually goes here
conn IKEv2-mschapv2
        also=IKEv2
  
# Apple clients usually goes here
conn IKEv2-mschapv2-apple
        also=IKEv2
        leftid=vpn.example.net

说明:

/etc/ipsec.secrets

# This file holds shared secrets or RSA private keys for authentication.

# RSA private key for this host, authenticating it to any other host
# which knows the public part.

: RSA privkey.pem
: PSK "the Great Wall"

/etc/strongswan.d/charon/eap-radius.conf

eap-radius {

    # Send RADIUS accounting information to RADIUS servers.
    accounting = yes

    # Close the IKE_SA if there is a timeout during interim RADIUS accounting
    # updates.
    # accounting_close_on_timeout = yes

    # Interval in seconds for interim RADIUS accounting updates, if not
    # specified by the RADIUS server in the Access-Accept message.
    # accounting_interval = 0

    # If enabled, accounting is disabled unless an IKE_SA has at least one
    # virtual IP. Only for IKEv2, for IKEv1 a virtual IP is strictly necessary.
    # accounting_requires_vip = no

    # Use class attributes in Access-Accept messages as group membership
    # information.
    # class_group = no

    # Closes all IKE_SAs if communication with the RADIUS server times out. If
    # it is not set only the current IKE_SA is closed.
    # close_all_on_timeout = no

    # Send EAP-Start instead of EAP-Identity to start RADIUS conversation.
    # eap_start = no

    # Use filter_id attribute as group membership information.
    # filter_id = no

    # Prefix to EAP-Identity, some AAA servers use a IMSI prefix to select the
    # EAP method.
    # id_prefix =

    # Whether to load the plugin. Can also be an integer to increase the
    # priority of this plugin.
    load = yes

    # NAS-Identifier to include in RADIUS messages.
    # nas_identifier = strongSwan

    # Port of RADIUS server (authentication).
    # port = 1812

    # Base to use for calculating exponential back off.
    # retransmit_base = 1.4

    # Timeout in seconds before sending first retransmit.
    # retransmit_timeout = 2.0

    # Number of times to retransmit a packet before giving up.
    # retransmit_tries = 4

    # Shared secret between RADIUS and NAS. If set, make sure to adjust the
    # permissions of the config file accordingly.
    secret = yourPassword

    # IP/Hostname of RADIUS server.
    server = radius.example.net

    # Number of sockets (ports) to use, increase for high load.
    # sockets = 1

    dae {

        # Enables support for the Dynamic Authorization Extension (RFC 5176).
        # enable = no

        # Address to listen for DAE messages from the RADIUS server.
        # listen = 0.0.0.0

        # Port to listen for DAE requests.
        # port = 3799

        # Shared secret used to verify/sign DAE messages. If set, make sure to
        # adjust the permissions of the config file accordingly.
        # secret =

    }

    forward {

        # RADIUS attributes to be forwarded from IKEv2 to RADIUS.
        # ike_to_radius =

        # Same as ike_to_radius but from RADIUS to IKEv2.
        # radius_to_ike =

    }

    # Section to specify multiple RADIUS servers.
    servers {

    }

    # Section to configure multiple XAuth authentication rounds via RADIUS.
    xauth {

    }

}

说明:

注意修改 secret 和 server 参数:

    # Shared secret between RADIUS and NAS. If set, make sure to adjust the
    # permissions of the config file accordingly.
    secret = yourPassword

    # IP/Hostname of RADIUS server.
    server = radius.example.net

/etc/strongswan.d/charon/xauth-eap.conf

xauth-eap {

    # EAP plugin to be used as backend for XAuth credential verification.
    backend = radius

    # Whether to load the plugin. Can also be an integer to increase the
    # priority of this plugin.
    load = yes

}

ss-redir

在 /etc/rc.local 的 exit 0 前添加如下代码,便于开机时自动建立监听于 1080 端口的转发,它将请求转发到 ss-server:

ss-redir -c /usr/local/etc/shadowsocks-libev/config.json -u -A -l 1080 -b 0.0.0.0 > /dev/null 2>&1 &

iptables

将下面的内容编写文件脚本文件 /root/bin/iptable.sh,需要 ipset (apt install ipset):

#!/bin/sh

# 转发客户端请求。注意网段和 ipsec.conf 里要一致。网卡使用外网网卡。
out_iface="eth1"
server_IP="1.2.3.4"
subnet="192.168.100.0/24"

# strongswan
iptables -A INPUT -p udp --dport 500 -j ACCEPT
iptables -A INPUT -p udp --dport 4500 -j ACCEPT

# Moved to /etc/sysctl.conf: net.ipv4.ip_forward=1
# 打开网卡转发功能。要把客户端的请求转发到公网网卡
# echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s ${subnet} -o ${out_iface} -j MASQUERADE
# 允许转发
iptables -A FORWARD -s ${subnet} -j ACCEPT

[ -r chnroute.txt ] || curl 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep ipv4 | grep CN | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute.txt

iptables -t nat -N SHADOWSOCKS

iptables -t nat -A SHADOWSOCKS -d $server_IP -j RETURN

# 内网网段
iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN

# 创建ipset列表
#ipset -exist create gfwlist hash:net
ipset -exist create chnroute hash:net
cat chnroute.txt | sudo xargs -I ip ipset -exist add chnroute ip

# gfwlist 走ss转发
#iptables -t nat -A SHADOWSOCKS -m set --match-set gfwlist dst -p tcp  -j REDIRECT --to-ports 1080

# 国内ip表直连
iptables -t nat -A SHADOWSOCKS -m set --match-set chnroute dst -j RETURN
# 其他连接走ss转发
iptables -t nat -A SHADOWSOCKS -p tcp  -j REDIRECT --to-ports 1080

iptables -t nat -A PREROUTING -s ${subnet} -p tcp -j SHADOWSOCKS

# ss-redir
iptables -A INPUT -p tcp -s ${subnet}--dport 1080 -j ACCEPT
iptables -A INPUT -p tcp --dport 1080 -j DROP

注意这段代码只是示意,请根据实际情况改进,比如定期更新 ipset chnroute 的 IP 列表,使用 iptables-persistent(apt install iptables-persistent) 来持久化 iptables 规则等。

在使用 iptables-persistent 时可能会用到的命令:

$ iptables-save > /etc/iptables/rules.v4
$ ip6tables-save > /etc/iptables/rules.v6

测试

$ ipsec start --nofork

在 PostgreSQL 数据库 radius 中执行如下 SQL 语句:

/* 用户名为 yourVPNUsername 的密码为 yourVPNPassword */
insert into radcheck(username, attribute, op, value) values('yourVPNUsername', 'Password', '==', 'yourVPNPassword');
/* 可以用来配置月流量为 1GiB */
/* insert into radcheck(username, attribute, op, value) values('yourVPNUsername', 'Max-Monthly-Traffic', ':=', '1024'); */
/* 控制组 plan-monthly-1GiB 的用户的并发会话为 2 */
insert into radgroupcheck(groupname, attribute, op, value) values('plan-monthly-1GiB', 'Simultaneous-Use', ':=', '2');
/* 限制组 plan-monthly-1GiB 的用户的月流量为 1024 MiB */
insert into radgroupcheck(groupname, attribute, op, value) values('plan-monthly-1GiB', 'Max-Monthly-Traffic', ':=', '1024');
/* 将用户 yourVPNUsername 添加到组 plan-monthly-1GiB */
insert into radusergroup(username, groupname) values('yourVPNUsername', 'plan-monthly-1GiB');

注意这里用到的 Max-Monthly-Traffic 是在在 FreeBSD 上安装 FreeRADIUS中定义的。

在 VPN 客户端可以使用 IKEv2 或者 Cisco IPSec 来连接。

IKEv2:
Server Address(服务器地址): vpn.example.net
Remote ID(远程 ID): vpn.example.net
Authentication Settings(鉴定设置): Username(用户名)
Username(用户名): yourVPNUsername
Password(密码): yourVPNPassword

Cisco IPSec:
Server Address(服务器地址): vpn.example.net
Account Name(帐户名称): yourVPNUsername
Password(密码): yourVPNPassword
Authentication Settings(鉴定设置):
Machine Authentication(机器鉴定):
Shared Secret(共享的密钥): the Great Wall
(Shared Secret 是配置在 /etc/ipsec.secrets 中的 PSK)

启动

service strongswan start

#cisco-ipsec, #freeradius, #ikev2, #ipset, #iptables, #iptables-persistent, #letsencrypt, #postgresql, #shadowsocks-libev, #ss-redir, #strongswan, #ubuntu

使用 dnsmasq 和 shadowsocks-libev 构建一个干净的域名服务器

需要两台服务器。

一台位于离我们近的机房,这个机房可以是位于有域名服务器缓存污染的防火墙内。(当然如果不是有污染的环境,也没有必要写这篇文章。)

一台位于没有域名服务器缓存污染的防火墙外。

我们把这两台服务器分别取名为 ns.example.net 和 ss-server.example.net。

不能直接使用防火墙外的域名服务器作为客户端的域名服务器,是因为当域名解析数据包经过防火墙时会被投毒。

使用 dnsmasq 来分流不同的域名解析请求,是因为如果全部定向到防火墙外的域名服务器,对某些使用 CDN 的网站,特别是防火墙内的网站将会被解析得到距离我们较远的服务器的 IP。

在 ns.example.net 上安装和运行 dnsmasq 和 ss-tunnel(包含在 shadowsocks-libev),在 ss-server.example.net 上安装和运行 ss-server(包含在 shadowsocks-libev)。

部署图如下:

版本信息

本文测试环境:

  • Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-72-generic x86_64)
  • shadowsocks-libev 2.6.3

ss-server.example.net 的安装

见:安装 shadowsocks-libev 服务器端

ns.example.net 的安装

dnsmasq

轻量级 DNS 转发器、DHCP 和 TFTP 服务器。我们利用它的 DNS 转发功能。

安装

apt install dnsmasq

配置

确保文件 /etc/dnsmasq.conf 包含了如下代码,它通常位于最后一行:

# Include all files in a directory which end in .conf
conf-dir=/etc/dnsmasq.d/,*.conf

gfwlist2dnsmasq.sh

创建一个脚本文件 /usr/local/bin/gfwlist2dnsmasq.sh (chmod +x),内容如下:

#!/bin/sh
curl -s -o /tmp/gfwlist.txt \
	https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt \
	&& base64 -d /tmp/gfwlist.txt \
	| awk -f $(cd "$(dirname "$0")"; pwd)/gfwlist2domainlist.awk \
		- /usr/local/etc/user_rule.txt \
	| grep -F -v -f "/usr/local/etc/skip_domain.txt" \
	| awk -f $(cd "$(dirname "$0")"; pwd)/domainlist2dnsmasq.awk \
		-v "noipset=noipset" \
		- \
	> /etc/dnsmasq.d/gfwlist.conf 2>/dev/null \
	&& service dnsmasq restart

该脚本会生成 dnsmasq 配置文件 /etc/dnsmasq.d/gfwlist.conf 让 dnsmasq 将 gfwlist 中的域名的解析转发到 127.0.0.1:5353 端口。

gfwlist2domainlist.awk 和 domainlist2dnsmasq.awk 可以从 gfwlist2dnsmasq.awk 获得。

/usr/local/etc/user_rule.txt 可以用来存放额外的想交由 127.0.0.1:5353 端口解析的域名列表,
/usr/local/etc/skip_domain.txt 可以用来存放在 gfwlist 列表中,但是不想交由 127.0.0.1:5353 端口解析的域名。

在 /etc/rc.local 的 exit 0 之前添加,使其在开机时运行一次:

/usr/local/bin/gfwlist2dnsmasq.sh

由于 gfwlist 是不断更新的,所以可以在 /etc/crontab 中添加,以便每天早上4点20分更新一次:

20 4	* * *	root	/usr/local/bin/gfwlist2dnsmasq.sh

ss-tunnel

安装

同:安装 shadowsocks-libev 服务器端

启动

在 /etc/rc.local 的 exit 0 之前添加如下代码,以便开机时自动运行一个监听在 127.0.0.1:5353 端口,请求会通过隧道转到 8.8.8.8:53 上。

/usr/local/bin/ss-tunnel -c /usr/local/etc/shadowsocks-libev/config.json -l 5353 -L 8.8.8.8:53 -u -A > /dev/null 2>&1 &

测试

在 ns.example.net 上可以通过如下命令来测试 ss-tunnel 创建的隧道是否正常工作:

root@ns:~# nslookup 
> server 127.0.0.1
Default server: 127.0.0.1
Address: 127.0.0.1#53
> set port=5353
> www.google.com
Server:		127.0.0.1
Address:	127.0.0.1#5353

Non-authoritative answer:
Name:	www.google.com
Address: 172.217.25.4
> exit

#dnsmasq, #gfwlist, #gfwlist2dnsmasq-awk, #shadowsocks-libev, #ss-server, #ss-tunnel, #ubuntu

安装 shadowsocks-libev 服务器端

版本信息

本文测试环境:

  • Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-72-generic x86_64)
  • shadowsocks-libev 2.6.3

安装 shadowsocks-libev 并设置 ss-server 开机自动启动

取出 shadowsocks-libev 源码,并切换到到 v2.6.3 分支:

$ git clone https://github.com/shadowsocks/shadowsocks-libev.git
$ cd shadowsocks-libev
$ git checkout v2.6.3
$ git submodule update --init --recursive

安装依赖:

$ sudo apt install autoconf automake libtool pkg-config libmbedtls-dev libsodium-dev libpcre3-dev libev-dev libudns-dev asciidoc xmlto make

编译并安装:

$ ./autogen.sh && ./configure && make
$ sudo make install

添加配置文件 /usr/local/etc/shadowsocks-libev/config.json:

{
    "server":"a.b.c.d",
    "server_port":8388,
    "local_port":1080,
    "password":"barfoo!",
    "timeout":60,
    "method":"aes-256-cfb"
}

注意修改 “server”:”a.b.c.d” 中的 a.b.c.d. 为你的服务器的外部 IP 地址,修改 password 和 method,其它参数按照需要修改。

设置开机时自动启动 ss-server,在 /etc/rc.local 的 exit 0 之前添加如下代码:

/usr/local/bin/ss-server -u -A -c /usr/local/etc/shadowsocks-libev/config.json > /dev/null 2>&1

配置 iptables 保护服务器 (可选)

由于实际使用中,shadowsocks-libev 客户端的 IP 地址可能是动态变化的,比如某次 PPPoE 拨号后 IP 地址可能就发生了变化。因此我们使用了 DDNS 来跟踪客户端 IP 地址,客户端通过 DDNS 来主动向 name server 上报 IP 地址,shadowsocks-libev 服务器需要在客户端 IP 地址发生变化后及时地修改 iptables 防火墙规则。

下面的防火墙规则中使用了 ipset (可以通过命令 apt install ipset 来安装)来存储客户端的 IP。

添加文件 /usr/local/bin/iptables.sh(注意 chmod +x):

#!/bin/sh
iptables -F

ext_ip="a.b.c.d"

ipset -exist create clients hash:net

# ping: allow ping from IPs in clients only
iptables -A INPUT -p icmp --icmp-type 8 -m set --match-set clients src -d ${ext_ip} -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type 0 -s ${ext_ip} -d 0/0 -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp -j DROP
iptables -A OUTPUT -p icmp -j DROP

# ss-server: allow from IPs in clients only
iptables -A INPUT -p tcp -m set --match-set clients src --dport 8388 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp -m set --match-set clients dst --sport 8388 -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp --dport 8388 -j DROP
iptables -A OUTPUT -p tcp --sport 8388 -j DROP

注意修改 ext_ip=”a.b.c.d” 中的 a.b.c.d 修改为你的服务器的外部 IP 地址。
如果你有其它 iptables 规则请自行解决。

添加文件 /usr/local/bin/update-clients-ip.sh(注意 chmod +x),这段脚本通过向 name server 查询文件 /usr/local/etc/clients.txt 中的域名列表的 IP,在 IP 发生变化时更新 ipset:

#!/bin/sh
PATH=$PATH:/bin:/usr/bin

for domain in $(cat "/usr/local/etc/clients.txt"); do
	ip_file="/tmp/update-clients-ip.${domain}"
	ip_file_log="${ip_file}.log"
	ip_file_old="${ip_file}.old"
	ip_file_new="${ip_file}.new"

	mv -f "${ip_file_new}" "${ip_file_old}" 2>/dev/null
	dig +short @ns1.example.net "${domain}" > "${ip_file_new}"
	diff -N "${ip_file_old}" "${ip_file_new}" > /dev/null

	if [ $? -eq 1 ]; then
		echo -n "`date -R` " >> "${ip_file_log}"
		cat "${ip_file_new}" >> "${ip_file_log}"

		[ -r "${ip_file_old}" ] && cat "${ip_file_old}" | xargs -I ip ipset -exist del clients ip
		[ -r "${ip_file_new}" ] && cat "${ip_file_new}" | xargs -I ip ipset -exist add clients ip
	fi
done

注意修改其中 ns1.example.net 为你自己的 name server。

添加文件 /usr/local/etc/clients.txt,文件内容为你的客户端的域名列表:

yourclient1.example.net
yourclient2.example.net

设置开机时自动运行 iptables 相关脚本,在 /etc/rc.local 的 exit 0 之前添加如下代码:

/usr/local/bin/iptables.sh
/usr/local/bin/update-clients-ip.sh

定时运行 update-clients-ip.sh,在 /etc/crontab 里增加:

* *	* * *	root	/usr/local/bin/update-clients-ip.sh

可以使用命令 ipset list clients 来查看 clients 里有哪些 IP 地址。

#crontab, #ddns, #ipset, #iptables, #rc-local, #shadowsocks-libev, #ubuntu

Starting unfs3: Cannot register service: RPC: Unable to receive; errno = Connection refused

# apt-get install unfs3
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following NEW packages will be installed:
  unfs3
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 0B/64.1kB of archives.
After this operation, 225kB of additional disk space will be used.
Selecting previously deselected package unfs3.
(Reading database ... 34099 files and directories currently installed.)
Unpacking unfs3 (from .../unfs3_0.9.22+dfsg-2_amd64.deb) ...
Setting up unfs3 (0.9.22+dfsg-2) ...
Starting unfs3: Cannot register service: RPC: Unable to receive; errno = Connection refused
unable to register (NFS3_PROGRAM, NFS_V3, udp).
invoke-rc.d: initscript unfs3, action "start" failed.
dpkg: error processing unfs3 (--configure):
 subprocess installed post-installation script returned error exit status 1
Errors were encountered while processing:
 unfs3
E: Sub-process /usr/bin/dpkg returned an error code (1)
# apt-get install portmap
Reading package lists... Done
Building dependency tree       
Reading state information... Done
portmap is already the newest version.
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
1 not fully installed or removed.
After this operation, 0B of additional disk space will be used.
Do you want to continue [Y/n]? 
Setting up unfs3 (0.9.22+dfsg-2) ...
Starting unfs3: unfs3.

#ubuntu