SSH反向代理内网穿透

SSH可以用来做正向Socks代理外,还可以用来做反向代理。典型的应用场景是:让内网的机器对外网可见,常见手段是在路由器上做端口映射,但是由于路由器的IP地址会变化(除非你有固定不变的IP),所以还需要在内网或路由器上运行一个动态DNS的工具(这种工具实际就是往外网发送IP地址),然后通过域名获取到最新的IP,而SSH做反向代理就不需要这些步骤,当然,需要外网有一台机器作为接入点。

简单来说,就是内网机器和外网的机器建立一条TCP隧道,外网预期分配的端口通过SSHD服务转发到内网来,内网的数据出去也一样。TCP隧道,头尾都对应端口号,比如把外网的2222端口,通过TCP隧道跟内网的22端口接起来。

ssh -CNR 2222:127.0.0.1:22 -b 0.0.0.0 www@外网IP

然后输入外网IP的www用户的密码就可以建立一条TCP隧道。可以看到,这个时候外网IP的2222端口开放了,它是由SSHD服务开启的。这里的-b参数说明希望外网IP的监听地址是0.0.0.0,就是0.0.0.0:2222,不过可能看到的是127.0.0.1:2222,这个不是我们所期望的。为了让反向代理真正可靠的使用,还需要解决如下几个问题:

0 自动登录问题
这个问题最常见的是放置公钥,内网ssh通过-i来指定私钥文件。如果希望使用密码来登录,那么需要借助sshpass这个包装器,之所以需要它,那是因为ssh不支持通过参数指定密码:

yum install sshpass

#格式
sshpass -p "密码" ssh -CNR 3080:127.0.0.1:80 www@xx.xx.xx.xx

1 在外网监听任意地址
首先,内网ssh命令需要使用-b指定监听地址为0.0.0.0,然后外网的sshd服务的配置,需要修改GatewayPorts为yes:

vi /etc/ssh/sshd_config
GatewayPorts yes

可能还需要重启一下sshd服务。

2 TCP隧道长时保持连接
在内网上运行ssh命令时,加上-o TCPKeepAlive=yes来让TCP保持长时连接。

3 防止TCP被中断
发送大量的数据或长时没有数据发送时,都可能导致TCP连接被卡住或被中间的路由器杀掉。所以需要建立一个心跳检查,在内网运行ssh时添加-o ServerAliveInterval=10 -o ServerAliveCountMax=6来维持心跳,这里的意思是每10秒检测一次,如果连续6次都无法获取响应,那么进程将退出。

4 防止进程异常退出
通过设置TCP长链接和定时发送心跳数据来保存通道畅通,但是从实际使用来看,这些还是远远不够的。进程异常退出监控起来很简单,但是实际遇到过这种情况:通道实际已经无法通信,但是本地进程并未退出,这个情况可能是远程进程异常退出,但是本地进程并没有收到通知,或者中间路由的问题,所以为了解决这个问题,我们可以定时发送数据,根据响应来判断是否要重启本地进程,于是有了以下脚本:

#!/bin/bash

cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

#不等于空说明通道通畅
ping=`curl -s http://x.x.x.x:2222`
if [ "$ping" = "" ]; then
	ps aux | grep 'x.x.x.x' | grep -v grep | awk '{print $2"|"$6}' | cat | while read line
	do
    		pid=`echo $line | cut -d "|" -f 1`
    		rss=`echo $line | cut -d "|" -f 2`

        	kill=`$cmm -9 $pid`
	        date=`date "+%Y-%m-%d %H:%M:%S"`

	        echo $date' -- 'PID:$pid' - Was Killed.'
	done
fi

#反代
live=`ps -efH | grep 'ssh -CN -R 2222:127.0.0.1:22' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    ssh -CN -R 2222:127.0.0.1:22 -b 0.0.0.0 -o TCPKeepAlive=yes -o ServerAliveInterval=10 -o ServerAliveCountMax=6 sshproxy@x.x.x.x -p 22 > /dev/null 2>&1 &
fi

live=`ps -efH | grep 'ssh -CN -R 8000:127.0.0.1:8000' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    ssh -CN -R 8000:127.0.0.1:8000 -b 0.0.0.0 -o TCPKeepAlive=yes -o ServerAliveInterval=10 -o ServerAliveCountMax=6 sshproxy@x.x.x.x -p 22 > /dev/null 2>&1 &
fi

使用curl来发送数据,如果通道畅通就会有响应,否则就无响应,无响应则强制杀掉本地进程。紧接着重启进程。

这个方法虽然很粗暴,但是很实用。从运行来看,还是相当稳定的(实际我仅仅是把内网的一个服务映射出去,偶尔用一用,但是用的时候要保证能用,比如内网的Gitlab)。