SSH翻墙集群

SSH动态代理是国内较为常见的翻墙方法。正如SSH隧道简介所说,它存在不少有优点。

然而在实际使用中,它存在如下缺点:

  • 与PPTP等VPN协议相比,它的连接不稳定。前者应该具备协议级断线重传机制。
  • 基于廉价VPS,导致它的连接不稳定。而且廉价VPS容易掉线,有时需要用户自己找在线客户修复,进一步延长了掉线时间。
  • 由于上述缺点,不适合小型公司多人使用。

在大概2年前,我摸索出SSH动态代理集群的办法。并将之部署于我所服务的公司,成功负载了20-30人日常翻墙学习与工作的需求。

原理

SSH动态代理,即为SOCK5代理,所以我们需要的是SOCK5集群。

若搜索socks 5 load balance不难发现一些有用信息:

What is the best way to load balance multiple sock5 proxys on seperate VM’s in the same datacenter?

我将分别介绍3种方法搭建SOCK5集群:

  1. 利用第三方模块nginx_tcp_proxy_module
  2. Nginx 1.9开始支持TCP Load Balancing
  3. HAProxy

关于SSH动态代理的配置方法,请参看AutoSSH简介

nginx_tcp_proxy_module的配置方法

Ubuntu的Nginx并没有将nginx_tcp_proxy_module编译进去。为了简化安装,我基于Ubuntu的Nginx包,做了Nginx的PPA:

  • 升级Nginx版本
  • 加入nginx_tcp_proxy_module

添加我的PPA

sudo add-apt-repository ppa:likemartinma/net
sudo apt-get -y update

若未安装nginx,则

sudo apt-get install -y nginx

若已安装nginx,则

sudo apt-get -y upgrade

在/etc/nginx/nginx.conf中,增加如下内容:

AutoSSH简介

autossh (Automatically restart SSH sessions and tunnels) 在运行的时候启动一个 ssh 进程,并监控该进程的健康状况;当 ssh 进程崩溃或停止通信时,它将重启动 ssh 进程。

命令选项

autossh [-V] [-M port[:echo_port]] [-f] [SSH_OPTIONS]
  • -M port[:echo_port] 指定监控端口(和echo端口,默认为前者加1)。

    • 若希望使远程标准 inetd 的 echo 服务(默认端口为7),则指定 echo_port ,仅需服务监听地址为 localhost
    • port 设置为 0 ,则将禁用监控功能。仅在 ssh 退出后重启它。
  • -f 使 autossh 在后台运行。

另外,autossh 还提供了一组环境变量来控制其行为, 这里仅介绍几个有代表性的,其可以 man autossh

  • AUTOSSH_FIRST_POLL : 首次论询测试时间。
  • AUTOSSH_POLL : 连接论询时间,默认 600 。若该值小于两次网络超时(默认 15 秒),则网络超将被调整为该值的 1/2
  • AUTOSSH_GATETIME : 等待 ssh 连接成功建立的时间,默认 30 秒,超时表示首次运行失败,将退出 autossh 。若设为 0 ,则禁用该功能,通常用于启动时运行 autossh 。
  • AUTOSSH_MAXLIFETIME : autossh 最长运行时间,达到该时间,autossh 将退出,并杀死 ssh 进程。
  • AUTOSSH_MAXSTART : ssh 最大启动次数。默认-1,表示无限制。

Ubuntu 配置方法

以 SSH 动态代理(即 SSH 翻墙)为例:

SSH隧道简介

SSH的功能非常强大,日常除了用于命令行远程登录服务器。它还具有神奇的隧道(tunnel)功能(也被称为SSH代理),可用于加密访问本地或远程主机的服务。

通常,SSH代理具有3种方式:

SSH(正向)代理

通过参数-L [bind_address:]port:remote_host:remote_port,将指定本地(客户端)端口转发至远程端口上。

/images/ssh-tunnel-proxy.png

如上图, hosta无法直接访问hostb,但它能直接SSH登录gateway;如此通过gateway,将hosta的端口X转发至hostb的端口Y上。相当于端口X和端口Y之间建立了加密隧道。

一般来说,端口Y为hostb上某服务的监听端口。当建立隧道后,hosta将监听端口X。应用程序访问hosta的端口X,等同于访问hostb的端口Y。对于应用程序,hostb端口Y对应的服务就如同运行在hosta上。

日常工作中,客户的网络常由于信息安全而被网关(或防火墙)隔离。当我们的软件在客户网络中某服务器发生问题时,我们常需奔赴客户现场进行调试。若客户存在某机器安装了SSH服务器,且能被外部访问。就可以利用SSH正向代理的方法,快速简便的登录被隔离的服务器并进行应用调试。

SSH反向代理

通过参数-R [bind_address:]port:remote_host:remote_port,将指定远程端口转发至本地(客户端)端口上。

/images/ssh-tunnel-reverse-proxy.png

如上图,hosta在防火墙内,无法被hostb直接访问。但它能直接SSH登录hostb;如此通过hostb,将hostb的端口X转发至hosta的端口Y上。该方法与SSH正向代理类似,所不同的是该隧道的访问方向是从服务端(hostb)至客户端(hosta),故被称为反向代理。

其应用场景也与SSH正向代理类似,所不同的是若客户不存在可供外部访问的SSH服务器时,我们可以在外网建设一个SSH服务器给客户的被隔离服务器来建立隧道。如此,我们可以访问自己的SSH服务器对应端口来调试客户服务器的应用。

更进一步,客户内网甚至不能访问外网,此时可利用客户内网一台笔记本(或台式机,它可以访问目标服务器)USB接上3G/4G手机来达到访问外部SSH服务器的目的。

/images/ssh-tunnel-reverse-proxy-mobile.png

SSH动态代理

通过参数-D [bind_address:]port,利用远程服务器为访问出口,在本地建立SOCKS 4/5代理服务器。

可以形象描绘为将本地应用的端口(SOCKS客户端端口),动态转发至远程。

/images/ssh-tunnel-dynamic-proxy.png

该功能广为人知的应用场景为翻墙。如上图,在国外租用VPS(hostb),客户端(hosta)通过SSH动态代理端口X(SOCKS 4/5的端口)便可以访问被GFW封锁的网络。

这种翻墙最大的优势在于

  • 低成本:国外廉价低配置VPS基本满足个人翻墙需求。
  • 服务端0配置:服务端只需要安装SSH服务端。
  • 客户端配置简单:客户端需要安装SSH客户端,以及一条命令。
  • 加密隧道:保证网络访问的数据安全。

创建通过SSH访问的chroot

我的工作环境是Ubuntu,然而经常需要CentOS来编译或测试。一般存在3种解决办法:

  • 创建VirtualBox或KVM虚拟机:
    • 优点:部署容易,且可以运行各种应用(如Oracle)。
    • 缺点:运行速度相对LXC和chroot慢,特别是I/O.
  • 创建CentOS的LXC:
    • 优点:运行速度快,且具有独立IP,可以通过SSH访问。
    • 缺点: 需要修改启动脚本。
  • 创建CentOS的chroot:
    • 优点:不需要修改任何配置。
    • 缺点:无法直接通过SSH访问,且需要root权限才能运行chroot。

创建CentOS的chroot

具体参看Rinse简介

创建SSH用户(或组)

在当前OS环境中,创建用户centos6.

sudo useradd -m -s /bin/bash centos6

获取centos6的uid和gid

id -u centos6
id -g centos6

在chroot环境中创建同名用户,且保持uid和gid相同。

sudo chroot /var/lib/centos-6 /bin/bash
groupadd -g <gid> centos6
useradd -s /bin/bash -m -u <uid> -g centos6 centos6

配置sshd

将下述内容追加至/etc/ssh/sshd_config

Match group centos6
	ChrootDirectory /var/lib/centos-6

确保/var/lib/centos-6的每一级目录的属主为root,且其他用户或组没有写权限。

然后,重启ssh

service ssh restart

这样保证centos6组的用户登录ssh时,chroot至/var/lib/centos-6目录中。

配置dev, proc和sysfs

chroot环境中rpm安装和卸载的前/后置脚本依赖dev, proc和sysfs,否则可能将造成安装和卸载错误。

在/etc/fstab中,增加proc和sysfs的挂载选项

proc /var/lib/centos-6/proc proc  defaults      0 0
none /var/lib/centos-6/sys  sysfs defaults      0 0
/dev /var/lib/centos-6/dev  none  defaults,bind 0 0

配置ssh密钥登录

chroot的ssh密钥登录与常规情况的唯一区别在于公钥应存放在当前OS环境(而非chroot环境)的~/.ssh/authorized_keys,因为sshd在执行chroot之前需要检查公钥是否正确。

Rinse简介——Debian/Ubuntu中创建RPM安装环境

Rinse 是 Debian 创建 RPM 发行版本(如 CentOS ,Scientific Linux 和 openSUSE)的工具。它可以被用于创建各种 RPM 发行版本的 chroot 环境。

以下基于Ubuntu 12.04 amd64,主要以创建CentOS 6 x86_64为例。

安装Rinse

sudo apt-get install -y rinse

创建CentOS 6

sudo rinse --distribution centos-6 --arch amd64 --directory centos-6

运行该命令将创建 CentOS 6 amd64 于当前工作目录的 centos-6 目录中。其中,

  • --distribution 指定发行版本,类似还可以centos-{4,5}, fedora-core-{4,5,6,7,8,9}和opensuse-{10.1,10.2,10.3,11.0,11.1,12.1}等。可以下述命令获取:
rinse --list-distributions

具体对应于 /etc/rinse/*.packages 的模板名,它们主要包含 RPM 包列表。换一句话说,你根据需要定制自己的模板。另一方面,你也可以通过 --pkgs-dir 指定不同于 /etc/rinse 的模板目录。

  • --arch : 指定架构,amd64 表示 64 位架构, i386 表示 32 位架构。缺省为 i386 .
  • --directory : 指定为安装目录,安装结束后便可以 chroot 该目录了。

另外,需要额外安装某些包,可以通过指定 --add-pkg-list 来完成。

利用SSH限制rsync的访问目录

我经常使用VPS来共享数据给朋友同事。由于VPS通常内存较小,不太适合安装ftp服务器;而http服务器常常只有下载的功能;更重要是某些数据较大,每天可能都在改变,需要增量同步上传或下载。

rsync是这类场景的良好选择。一方面,通过SSH服务器来工作,它不常驻内存,节约了VPS内存使用;另一方面,它优秀的二进制增量同步功能,不仅减少了同步时间,也节约了VPS有限的带宽。

在实际使用过程中,我遇到了下述问题:

  • 针对某些目录,某些用户仅能读(下载);
  • 针对某些目录,某些用户仅能写(上传);
  • 具有读或写权限的用户不能登录SSH,也不能执行任何程序;
  • 具有读或写权限的用户不能访问其他目录。

诚然,这些问题可以通过操作系统用户相对复杂的权限控制(包括目录基本权限和ACL以及SELinux等)。然而,通过搜索和学习,我发现强大的SSH早已具备这样的接口:

  • 通过密钥对,简化用户使用,让用户彻底摆脱密码的记忆(当然用户加密私钥,还是要自己记密码的);
  • 通过~/.ssh/authorized_keys的command选项(通过man authorized_keys阅读详细内容),设置脚本过滤掉业务上无效的指令;
  • 还是通过command选项,限制不同密钥有不同权限,如密钥A只能读,密钥B只能写。

下面,我将通过具体示例来解释command选项:

限制用户仅能读

#!/bin/sh

DATA_DIR=/home/share/data

case "$SSH_ORIGINAL_COMMAND" in
rsync\ --server*)
        TARGET=`echo "$SSH_ORIGINAL_COMMAND" | awk '{ print $NF }'`
        if echo "$SSH_ORIGINAL_COMMAND" | grep -q " --sender " && [ "$TARGET" = $DATA_DIR ]; then
                $SSH_ORIGINAL_COMMAND
        else
                echo "Rejected"
        fi
        ;;
*)
        echo "Rejected"
        ;;
esac

该脚本(存储在文件/usr/local/bin/validate_pull_share)限制用户share仅能下载$DATA_DIR的文件, 它需要跟公钥配置在服务器的/home/share/.ssh/authorized_keys,如:

command="/usr/local/bin/validate_pull_share" ssh-rsa <用户公钥A>
  • command选项用于指定脚本,该脚本可以过滤一些远程命令(禁止执行),它相当于远程命令的前置脚本。
  • $SSH_ORIGINAL_COMMAND是sshd传递给command脚本环境变量,表示ssh客户端需要远程执行的命令。
  • 第6行限制仅能执行rsync服务端指令。
  • 第7行获取$SSH_ORIGINAL_COMMAND的最后一个参数(这里没有考虑有空格的目录),这个参数在这里是rsync需要访问的目录$TARGET.
  • 第8行为命令合法性判断, 由两个条件组成:
    • 判断--sender参数是否存在于$SSH_ORIGINAL_COMMAND中,--sender表示该命令为下载命令;
    • 判断下载目录$TARGET是否与规定的目录一致。

限制用户仅能写

#!/bin/sh

DATA_DIR=/home/share/data

case "$SSH_ORIGINAL_COMMAND" in
rsync\ --server*)
        TARGET=`echo "$SSH_ORIGINAL_COMMAND" | awk '{ print $NF }'`
        if ! echo "$SSH_ORIGINAL_COMMAND" | grep -q " --sender " && [ "$TARGET" = $DATA_DIR ]; then
                $SSH_ORIGINAL_COMMAND
        else
                echo "Rejected"
        fi
        ;;
*)
        echo "Rejected"
        ;;
esac

实际仅与读限制脚本相差一行(第8行),仅允许没有--sender参数的命令(即上传命令)。