没购买外网IP流量的阿里云ECS服务器通过SSH隧道上外网

网上看到一文《Nginx正向代理让无法直接上网的机器通过代理上网》,说是在阿里云平台买了几台ECS,但是只要其中一台开通了公网,由于要初始化系统环境,需要网络安装相关依赖,于是使用Nginx搭建代理服务器。

看完觉得Nginx功能还是挺多的,只不过这么个做法太low了,https代理没法搞,配置完代理服务器还得在无外网IP的服务器上设置shell环境变量或者apt/yum的配置,遇到不支持代理的软件还更麻烦,这种情况怎么也得来个“全局代理”吧,还有什么做法比更改默认网关更全局呢?看完下文,你只需一台开通公网IP的ECS,就可以让同区域只有内网IP的其他ECS服务器访问公网了。


年初刚学习使用ssh登录的时候,了解到ssh隧道可以当vpn使用,练手写了个bash脚本:
https://github.com/oicu/vpn-over-ssh

这里补充一博文,解释一下这个脚本的作用,首次设置步骤较为繁琐,耐心点。

svpn.sh脚本功能:
1、在 ssh客户端 运行,建立ssh隧道并在 服务端和客户端 各添加tun网卡。
2、在 ssh服务端 添加/删除路由或更改默认网关。
3、在 ssh客户端 添加/删除路由或更改默认网关。
4、在 ssh服务端 配置iptables做snat,tun网卡ip源地址转换,并更改mtu。
5、在 ssh客户端 配置iptables做snat,tun网卡ip源地址转换,并更改mtu。
6、在 ssh服务端 配置iptables做snat,局域网ip源地址转换。
7、在 ssh客户端 配置iptables做snat,局域网ip源地址转换。

运行过程需要输入2次ssh密码,如果使用公钥登录则没那么麻烦。

我列了3种网络拓扑,因为脚本写得粗糙也不方便再优化,不同网络需要对脚本里添加路由、更改默认网关、配置iptables的语句进行修改。步骤4-7全部设置也没有影响,主要是第2、3步选用改路由还是改网关。


脚本开头定义了几个变量:

1
2
3
4
5
6
7
8
9
10
11
12
SERVER_SSH_PORT="22"		#服务端ssh端口
SERVER_SSH_IP="1.2.3.4" #服务端ip,如果是路由器,需要做端口映射
CLIENT_ETHERNET="eth0" #客户端物理网卡名
SERVER_ETHERNET="eth0" #服务端物理网卡名
CLIENT_TUNNEL="tun2" #自定义的客户端tun网卡名,tun+数字
SERVER_TUNNEL="tun1" #自定义的服务端tun网卡名,不能和客户端相同
CLIENT_TUN_IP="10.0.0.2" #自定义的客户端tun网卡IP
SERVER_TUN_IP="10.0.0.1" #自定义的服务端tun网卡IP,不能和客户端相同
CLIENT_NET="192.168.2.0/24" #客户端所在局域网网段
CLIENT_GATEWAY="192.168.2.1" #客户端现在的网关
SERVER_NET="192.168.1.0/24" #服务端所在局域网网段,有可能和客户端相同
SERVER_GATEWAY="192.168.1.1" #服务端现在的网关,有可能和客户端相同

如果有多张物理网卡,要根据情况选用。


脚本默认适用的网络:

1
C --> A --> 路由器1 <-- 互联网 --> 路由器2 <-- B <-- D

A和C在一个局域网,B和D在一个局域网,如何像使用VPN一样,让2个子网互通呢?
即让C和D相互访问呢?

1、首先A和B需要是Linux系统,都需要有root权限。
2、两个局域网的网段不能一样,网络规划好。
3、至少有一个路由器有外网IP,有外网IP的一方作为服务端。假设路由器2有外网IP,那么B当作ssh服务器,在路由器2上做端口映射到B。A作为ssh客户端,在A上放 svpn.sh 脚本,根据两边的网络,更改那几个变量。
4、A和B都需安装的软件:
CentOS: yum install tunctl iptables
Debian/Ubuntu: sudo apt-get install uml-utilities iptables
5、脚本里所有iptables命令都启用(默认是全启用)。
6、脚本里都使用添加/删除路由的命令,而不用更改默认网关的命令,否则访问本地局域
网数据得到对端绕一圈(默认是使用路由)。
7、ssh服务端还需要更改配置/etc/ssh/sshd_config,前2行是必须的:

1
2
3
4
PermitRootLogin yes
PermitTunnel yes
ClientAliveInterval 30
ClientAliveCountMax 6

8、脚本外的操作:C或路由器1上添加对端局域网的访问路由,网关设置为A的局域网IP,如果局域网还有E、F其他电脑,在路由器上设置就不用在每台电脑上都设定。
9、脚本外的操作:D或路由器2上添加对端局域网的访问路由,网关设置为B的局域网IP。

有网友建议通过 bonding 增加可用性。通过 runit/ daemontools 跑起来。一下跑16个进程,然后把16个接口做802.1ad或者xor bonding成一个接口用。但因为我对这个脚本的定位是即用即开,不用即关,所以没研究。


另一种网络拓扑:

1
C --> A --> 路由器 <-- 互联网 -->  B

A、C都限制不能上外网,但路由器的22端口映射到A,B在外网可以ssh登录A,只需在B执行svpn.sh脚本,A就能上外网了,C配置默认网关为A的局域网IP,也能上外网了。只要留一个ssh端口,就能够突破网络封锁!


再来一种网络拓扑:

1
A <-- 互联网 --> B --> GFW

A在国内,B是国外的VPS,在A配置执行svpn.sh脚本,A就能科学上网了。


回到主题,无外网IP的阿里云ECS服务器怎么访问外网呢?网络拓扑简化为:

1
A <--> B <-- 互联网 --> D

A服务器没有外网IP,当作ssh服务端。
B有外网IP,当作ssh客户端,svpn脚本放在B执行。
D是外网的电脑/服务器。

使用以下配置:

1
2
3
4
5
6
7
8
SERVER_SSH_PORT="22"		#A的ssh端口
SERVER_SSH_IP="10.111.1.222" #A的内网ip
CLIENT_ETHERNET="eth1" #B的外网物理网卡
CLIENT_TUNNEL="tun2" #自定义的客户端tun网卡名,tun+数字
SERVER_TUNNEL="tun1" #自定义的服务端tun网卡名,不能和客户端相同
CLIENT_TUN_IP="192.168.169.2" #因为阿里云内网使用A类,所以这里用C类
SERVER_TUN_IP="192.168.169.1"
SERVER_GATEWAY="10.111.1.254" #A的默认网关

有几个变量没有使用到,脚本内含有这几个没使用变量的行注释或删除。

然后在B服务器上运行svpn.sh脚本,脚本会修改A的默认网关,在B用iptables做snat,A就可以访问外网了!

如果有2台无外网流量的ECS服务器要同时访问外网呢?一台用一个脚本,只是tun网卡用另一组:

1
2
CLIENT_TUNNEL="tun4"
SERVER_TUNNEL="tun3"

又多了第3台呢?tun网卡名称都递增+1即可。


这里贴个可用于阿里云的简化版脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/bin/bash
#########################################
#FileName: svpn.sh
#Author: oicu
#Blog: http://oicu.cc.blog.163.com/
#########################################
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin
export PATH

[ "$(whoami)" != 'root' ] && echo "Run it as root." && exit 1

SERVER_SSH_PORT="22" #A的ssh端口
SERVER_SSH_IP="10.111.1.222" #A的内网ip
CLIENT_ETHERNET="eth1" #B的外网物理网卡
CLIENT_TUNNEL="tun2" #自定义的客户端tun网卡名,tun+数字
SERVER_TUNNEL="tun1" #自定义的服务端tun网卡名,不能和客户端相同
CLIENT_TUN_IP="192.168.169.2" #因为阿里云内网使用A类,所以这里用C类
SERVER_TUN_IP="192.168.169.1"
SERVER_GATEWAY="10.111.1.254" #A的默认网关

start()
{
ssh -NTCf -o ServerAliveInterval=30 \
-o ServerAliveCountMax=6 \
-o ExitOnForwardFailure=yes \
-o Tunnel=point-to-point \
-w "${CLIENT_TUNNEL#tun}:${SERVER_TUNNEL#tun}" \
root@${SERVER_SSH_IP} -p ${SERVER_SSH_PORT}
if [ $? -ne 0 ]; then exit 1; fi
echo "ssh tunnel is working."
ssh -T root@${SERVER_SSH_IP} -p ${SERVER_SSH_PORT} > /dev/null 2>&1 << eeooff
ip route replace default via ${SERVER_GATEWAY}
ip link set ${SERVER_TUNNEL} down
ifconfig ${SERVER_TUNNEL} > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo 1 > /proc/sys/net/ipv4/ip_forward
ip link set ${SERVER_TUNNEL} up
ip addr add ${SERVER_TUN_IP}/32 peer ${CLIENT_TUN_IP} dev ${SERVER_TUNNEL}
ip route replace default via ${SERVER_TUN_IP}
fi
exit
eeooff
if [ $? -ne 0 ]; then exit 1; fi
echo "remote start."
ifconfig ${CLIENT_TUNNEL} > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo 1 > /proc/sys/net/ipv4/ip_forward
ip link set ${CLIENT_TUNNEL} up
ip addr add ${CLIENT_TUN_IP}/32 peer ${SERVER_TUN_IP} dev ${CLIENT_TUNNEL}
iptables -t nat -A POSTROUTING -s ${SERVER_TUN_IP}/32 -o ${CLIENT_ETHERNET} -j MASQUERADE
iptables -A FORWARD -p tcp --syn -s ${SERVER_TUN_IP}/32 -j TCPMSS --set-mss 1356
ping ${SERVER_TUN_IP} -i 60 > /dev/null 2>&1 &
echo "local start."
else
exit 1
fi
}

stop-srv()
{
ssh -T root@${SERVER_SSH_IP} -p ${SERVER_SSH_PORT} > /dev/null 2>&1 << eeooff
ip route replace default via ${SERVER_GATEWAY}
ip link set ${SERVER_TUNNEL} down
exit
eeooff
if [ $? -eq 0 ]; then echo "remote stop."; fi
}

stop()
{
ip link set ${CLIENT_TUNNEL} down
iptables -t nat -D POSTROUTING -s ${SERVER_TUN_IP}/32 -o ${CLIENT_ETHERNET} -j MASQUERADE
iptables -D FORWARD -p tcp --syn -s ${SERVER_TUN_IP}/32 -j TCPMSS --set-mss 1356
CLIENT_SSH_PID=`ps -ef | grep 'ssh -NTCf -o' | grep -v grep | head -n1 | awk '{print $2}'`
if [ -n "${CLIENT_SSH_PID}" ]; then kill -9 ${CLIENT_SSH_PID}; fi
if [ -n "`pidof ping`" ]; then pidof ping | xargs kill -9; fi
} > /dev/null 2>&1

usage()
{
echo "usage:"
echo " $0 -start"
echo " $0 -stop"
echo ""
echo "for ssh:"
echo " nohup $0 -start > /dev/null 2>&1"
}

case $1 in
"--start" | "-start")
stop
start
;;
"--stop" | "-stop")
stop-srv
stop
echo "local stop."
;;
*)
usage
;;
esac