本地端口转发

本地转发,说白了,就是把发到本地的某个端口请求,转发到远程的某台机器上面。

下图,假如host3和host1、host2都同互相通信,但是host1和host2之间不能通信,如何从host1连接上host2?

image.png

对于实现ssh连接来说,实现方式很简单,从host1 ssh到host3,再ssh到host2,也就是将host3作为跳板的方式。

但是如果不是ssh,而是http的80端口呢?如何让host1能访问host2的80端口?

ssh支持本地端口转发,语法格式为:

ssh -L [local_bind_addr:]local_port:remote:remote_port middle_host

[root@host1 ~]# ssh -g -L 2222:host2:80 host3

其中"-L"选项表示本地端口转发,其工作方式为:在本地指定一个由ssh监听的转发端口(2222),将远程主机的端口(host2:80)映射为本地端口(2222),当有主机连接本地映射端口(2222)时,本地ssh就将此端口的数据包转发给中间主机(host3),然后host3再与远程主机的端口(host2:80)通信。

现在就可以通过访问host1的2222端口来达到访问host2:80的目的了。例如:
重新开启一个终端:curl http://172.16.10.5:2222

再来解释下"-g"选项,指定该选项表示允许外界主机连接本地转发端口(2222),如果不指定"-g",则host4将无法通过访问host1:2222达到访问host2:80的目的。甚至,host1自身也不能使用172.16.10.5:2222,而只能使用localhost:2222或127.0.0.1:2222这样的方式达到访问host2:80的目的,之所以如此,是因为本地转发端口默认绑定在回环地址上。可以使用bind_addr来改变转发端口的绑定地址,例如:

[root@host1 ~]# ssh -L 172.16.10.5:2222:host2:80 host3

这样,host1自身就能通过访问172.16.10.5:2222的方式达到访问host2:80的目的。

一般来说,使用转发端口,都建议同时使用"-g"选项,否则将只有自身能访问转发端口。

再来分析下转发端口通信的过程。

image.png

当host4 发起172.16.10.5:2222的连接时(即步骤1),数据包的目标地址和端口为"172.16.10.5:2222"。由于host1上ssh已经监听了2222端口,并且知道该端口映射自哪台主机哪个端口,所以将会把该数据包目标地址和端口替换为"172.16.10.3:80",并将此数据包通过转发给host3。当host3收到该数据包时,发现是host1转发过来请求访问host2:80的数据包,所以host3将代为访问host2的80端口。

所以,host1 和 host3 之间的通信方式是SSH协议,这段连接是安全加密的,因此称为"安全隧道",而host3和host2之间通信协议则是HTTP而不是ssh。

现在再来考虑下,通过本地端口转发的方式如何实现ssh跳板的功能呢?仍以上图为例:

[root@host1 ~]# ssh -g -L 22333:host2:22 host3

打开新终端:

ssh root@host1 -p 222333

这样只需使用ssh连上host1的22333端口就等于连接了host2的22端口。

最后,关于端口转发有一个需要注意的问题:ssh命令中带有要执行的命令。考虑了下面的三条在host1上执行的命令的区别。

[root@host1 ~]# ssh -g -L 22333:host2:22 host3
[root@host1 ~]# ssh -g -L 22333:host2:22 host3 "ifconfig"
[root@host1 ~]# ssh -g -L 22333:host2:22 host3 "sleep 10"

第一条命令开启了本地端口转发,且是以登录到host3的方式开启的,所以执行完该命令后,将跳到host3主机上,当退出host3时,端口转发功能将被关闭。

另外,host1上之所以要开启端口转发,目的是为了与host2进行通信,而不是跳到host3上,所以应该在ssh命令行上加上 -f 选项让ssh在本机host1上以后台方式提供端口转发功能,而不是跳到host3上来提供端口转发功能。

第二条命令在开启本地转发的时候还指定了要在host3上执行"ifconfig"命令,但是ssh的工作机制是远程命令执行完毕的那一刻,ssh关闭连接,所以此命令开启的本地端口转发功能有效期只有执行ifconfig命令的一瞬间。

第三条命令和第二条命令类似,只不过指定的是睡眠10秒命令,所以此命令开启的本地转发功能有效期只有10秒。

结合上面的分析,开启端口转发功能时,建议让ssh以后台方式提供端口转发功能,且明确指示不要执行任何ssh命令行上的远程命令。即最佳开启方式为:

[root@host1 ~]# ssh -f -N -g -L 22333:host2:22 host3

远程端口转发

ssh除了支持本地端口转发,还支持远程端口转发。顾名思义,远程端口转发表示的是将远程端口的数据转发到本地。

如下图:假如host3是内网主机,它能和host2互相通信,也能和host1通信,但反过来,host1不能和host3通信。这时要让host1访问host3或host2就没办法通过本地端口转发了,因为要在host1上开启本地端口转发,必须要和host3通信请求建立隧道。

image.png

可以通过在host3上发起远程端口转发来实现,因为host3能和host1通信,host3可以请求在host1和host3之间建立隧道。

语法如下:

ssh -R [bind_addr:]remote1_port:host:port remote1

以上图为例,实现方式是在host3上执行:

[root@host3 ~]# ssh -R 22333:host2:80 host1

这表示host3请求host1上的sshd服务,在host1上建立一个套接字监听22333端口,它是host2端口的映射,当有主机连接host1:22333时,此连接中的数据全部都通过host1和host3之间的安全隧道转发给host3,再由host3向host2的80端口发起访问。由于host3请求开启的转发端口是在远程主机host1上的,所以称为"远程端口转发"。

再考虑下面这条命令所开启的远程转发端口,它是在host3上执行的。

[root@host3 ~]# ssh -R 22333:host3:80 host1

该命令将自身的host3:80映射到host1:22333上,这也能让host1和host2、host3通信,因为隧道是建立在host1:22333<-->host3:80上的。

但是,远程端口转发和本地端口转发最大的一个区别是,远程转发端口是由host1上的sshd服务控制的,默认配置情况下,sshd服务只允许本地开启的远程转发端口(22333)绑定在环回地址(127.0.0.1)上,即使显式指定了bind_addr也无法覆盖。例如:

[root@host3 ~]# ssh -R *:22333:host2:80 host1 

[root@host3 ~]# netstat -tnlp
Active Internet connections (only servers)
tcp   0   0 0.0.0.0:22        0.0.0.0:*   LISTEN  8405/sshd  
tcp   0   0 127.0.0.1:25      0.0.0.0:*   LISTEN  1422/master
tcp   0   0 127.0.0.1:22333   0.0.0.0:*   LISTEN  8407/sshd  
tcp   0   0 :::22             :::*        LISTEN  8405/sshd  
tcp   0   0 ::1:25            :::*        LISTEN  1422/master
tcp   0   0 ::1:22333         :::*        LISTEN  8407/sshd

要允许本地的远程转发端口绑定在非环回地址上,需要在host1的sshd配置文件中启用"GatewayPorts"项,它的默认值为no。启动该选项后,不给定bind_addr或bind_addr设置为"*"都表示绑定在所有地址上。如下:

[root@host3 ~]# ssh -g -R *:22333:host2:80 host1

[root@host3 ~]# netstat -tnlp
Active Internet connections (only servers)
tcp   0  0 0.0.0.0:22      0.0.0.0:*   LISTEN  8466/sshd  
tcp   0  0 127.0.0.1:25    0.0.0.0:*   LISTEN  1422/master
tcp   0  0 0.0.0.0:22333   0.0.0.0:*   LISTEN  8468/sshd  
tcp   0  0 :::22           :::*        LISTEN  8466/sshd  
tcp   0  0 ::1:25          :::*        LISTEN  1422/master
tcp   0  0 :::22333        :::*        LISTEN  8468/sshd

和前面的本地转发端口一样,建议的几个选项是:"-g"、"-f"、"-N"。即推荐的命令写法是:

[root@host3 ~]# ssh -fgN -R 22333:host2:80 host1

现在,就可以通过访问host1:22333达到访问host2:80的目的了。如下图所示。

动态端口转发(SOCKS代理)

无论是本地端口转发还是远程端口转发,都是将某固定主机及其端口映射到本地或远程转发端口上,例如将host2:80映射到host1:2222。也就是说,本地或远程转发端口和目标端口所代表的应用层协议是一对一的关系,2222端口必须对应的是http的80端口,使用浏览器向host1:2222端口发起http请求当然没问题,但是使用ssh工具向host1:2222发起连接将会被拒绝,因为host2上http服务只能解析http请求,不能解析ssh连接请求。

ssh支持动态端口转发,由ssh来判断发起请求的工具使用的是什么应用层协议,然后根据判断出的协议结果决定目标端口。
以下图为例进行说明,host1处在办公内网,能和host3互相通信,但它无法直接和互联网和host2通信,而host3则可以和host2以及互联网通信。

image.png

要让host1访问互联网,又能和host2的22端口即ssh服务通信,显然在host1上仅设置一个本地端口转发是不够的,虽然可以设置多个本地转发端口分别映射不同的端口,但这显然比较笨重和麻烦。使用动态端口转发即可。

语法格式为:

ssh -D [bind_addr:]port remote 

以上图为例,在host1上执行:

[root@host1 ~]# ssh -Nfg -D 2222 host3

执行完上面的命令,host1将在本地开启SOCKS4或SOCKS5服务来监听2222端口。只要客户端程序工具(隐含了使用的应用层协议类型)将其自身的代理设置为host1:2222,则该程序所有产生的数据都将转发到host1:2222,再由host1:2222将数据通过隧道转发给host3,最后由host3和互联网或host2上对应客户端工具的应用层协议的端口进行通信。

其实很简单,假如host4使用IE浏览器作为客户端工具,并将IE浏览器的代理设置为host1:2222,由于IE浏览器发起的请求使用的是http协议(此处不考虑其他可能的协议),那么IE浏览器产生的数据都转发到host1:2222,再由host1:2222通过隧道转发给host3,host3能联网,所以host4就实现了联网功能。如下图设置:

image.png

再比如host4上的QQ客户端也可以设置代理。这样QQ产生的数据都将通过host1:2222转发出去,host1:2222再将QQ的数据转发到host3上,host3知道这些数据使用的协议是oicq,所以host3会去连接腾讯的QQ服务器(oicq服务对应的端口)。

image.png

ssh只支持socks4和socks5两种代理,有些客户端工具中需要明确指明代理类型。

和本地、远程端口转发一样,建议的选项是:"-f"、"-N"和"-g"。

由于ssh动态端口转发是ssh客户端的功能,所以不使用ssh命令,使用SecurtCRT、putty等ssh客户端工具都可以实现代理上网。例如,本地主机不能上网,但能和172.16.10.6的SSH服务通信,而172.16.10.6能上网,则可以在本地主机先使用SecurtCRT连接172.16.10.6,再在对应的会话选项上做如下设置,使得本地主机也能上网。

image.png

然后,在本地主机查看下是否监听了SecurtCRT中指定的8888动态转发端口。

image.png

现在,本机所有数据包都通过SecurtCRT所连接的172.16.10.6流向外界。

通过 SSH 端口转发访问只允许本地连接的 MySQL 数据库

image.png

这个时候,我们就可以使用本地端口转发了,将centos A本地的某个端口,映射到centos B(192.168.13.142)机器上面的,如下:

ssh -L 127.0.0.1:3306:127.0.0.1:3306 root@192.168.13.142
# 因为本地网卡地址是可以省略的,上面的转发,可以简写为
ssh -L 3306:127.0.0.1:3306 root@192.168.13.142
# 当然,ssh连接的时候,若两台机器的用户名相同,也是可以省略的,即命令可以简写为
ssh -L 3306:127.0.0.1:3306 192.168.13.14

上面的代码就是将本地的3306端口,转发到192.168.13.142的3306端口。因为centos B(192.168.13.142)上面的mysql使用的3606端口。当然,我们首先得看看本地的3306端口是否被占用,如被占用,可以使用其他的端口。

数据流向如图:
image.png

  • 首先,centos A(192.168.13.139)上的应用将数据发送到本地的127.0.0.1上面的3306端口。
  • 然后,centos A(192.168.13.139)将3306端口的数据,通过SSH转发到centos B(192.168.13.142)的3306端口。
  • 接着,centos B(192.168.13.142)将处理后的数据,原路返回给centos A(192.168.13.139)。

如果是首次通过ssh连接cetosB该机器,则会提示确认公钥,并让你选择是否确定连接。此时,我们在centos A上面连接centos B上面的mysql,就可以这么写了。

mysql -h127.0.0.1 -uroot -p

我们可以通过下面命令,在centosA查看ssh转发监听的进程。

lsof -i:3306

SSH转发实现与其它机器相同的服务

案例1、在机器B(192.168.13.149)上面访问机器A(192.168.13.148)的服务。现在,我们在A机器上面,启动了Nginx服务

我们希望B机器也能够这样使用A机器上面的服务。需要把B机器上面80端口请求,转发到A机器上面。

image.png

需要在B机器上面,实现本地转发,执行如下代码:

ssh -f -N -L 127.0.01:80:192.168.13.148:80 root@192.168.13.148

我们也可以通过远程转发来实现。即在A机器上面执行如下代码:

sudo ssh -f -N -R 8081:127.0.0.1:80 root@192.168.13.149

我们监听了B机器的8081端口,把该端口的请求,转发到A机器上面。
此时,如果在 B 上执行如下命令,就会被转发到A机器的127.0.0.1的80端口

curl 127.0.0.1:8081

利用远程转发,实现代理功能

image.png

目前B机器,只能在自己127.0.0.1的80端口监听并转发,如何让B机器作为代理,转发其他机器的请求到A机器上面呢?比如,现在有一台机器C(192.168.13.143),C不能访问A,但是能够访问B。如何让C利用B来访问A呢?
此时,需要将B的监听,由127.0.0.1:8081,改为0:0.0.0:8081,修改sshd的配置/etc/ssh/sshd_config。

vim /etc/ssh/sshd_config
GatewayPorts yes

然后重启 sshd 服务。

在 C 上配置如下:

ssh -f -g  -N -R 8081:127.0.0.1:80 root@192.168.13.149

可以看到,此时B机器,已经监听了0:0.0.0:8081
在C机器上面,我们通过curl模拟请求,利用B机器做代理,如下:

curl -x 192.168.13.149:8081 127.0.0.1

当然,如果还有其他机器,也可以使用类似的方式,来请求A机器。

SSH存在的问题

如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户,那么用户很难辨别真伪。因为不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,也就是说,都是自己签发的。
可以设想,如果攻击者插在用户与远程主机之间(比如在公共的wifi区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么SSH的安全机制就荡然无存了。这种风险就是著名的"中间人攻击"(Man-in-the-middle attack)。

SSH 命令的三种代理功能(-L/-R/-D)

ssh 命令除了登陆外还有三种代理功能:

  • 正向代理(-L):相当于 iptable 的 port forwarding
  • 反向代理(-R):相当于 frp 或者 ngrok
  • socks5 代理(-D):相当于 ss/ssr

如要长期高效的服务,应使用对应的专用软件。如没法安装软件,比如当你处在限制环境下想要访问下某个不可达到的目标,或者某个临时需求,那么 ssh 就是你的兜底方案。

正向代理

所谓“正向代理”就是在本地启动端口,把本地端口数据转发到远端。

用法1:远程端口映射到其他机器

HostB 上启动一个 PortB 端口,映射到 HostC:PortC 上,在 HostB 上运行:

$ ssh -L 0.0.0.0:PortB:HostC:PortC user@HostC

这时访问 HostB:PortB 相当于访问 HostC:PortC(和 iptable 的 port-forwarding 类似)。

用法2:本地端口通过跳板映射到其他机器

HostA 上启动一个 PortA 端口,通过 HostB 转发到 HostC:PortC上,在 HostA 上运行:

$ ssh -L 0.0.0.0:PortA:HostC:PortC  user@HostB

这时访问 HostA:PortA 相当于访问 HostC:PortC。

两种用法的区别

两种用法的区别是,第一种用法本地到跳板机 HostB 的数据是明文的,而第二种用法一般本地就是 HostA,访问本地的 PortA,数据被 ssh 加密传输给 HostB 又转发给 HostC:PortC。

反向代理

所谓“反向代理”就是让远端启动端口,把远端端口数据转发到本地。

HostA 将自己可以访问的 HostB:PortB 暴露给外网服务器 HostC:PortC,在 HostA 上运行:

$ ssh -R HostC:PortC:HostB:PortB  user@HostC

那么链接 HostC:PortC 就相当于链接 HostB:PortB。使用时需修改 HostC 的 /etc/ssh/sshd_config,添加:

GatewayPorts yes

相当于内网穿透,比如 HostA 和 HostB 是同一个内网下的两台可以互相访问的机器,HostC是外网跳板机,HostC不能访问 HostA,但是 HostA 可以访问 HostC。

那么通过在内网 HostA 上运行 ssh -R 告诉 HostC,创建 PortC 端口监听,把该端口所有数据转发给我(HostA),我会再转发给同一个内网下的 HostB:PortB。

同内网下的 HostA/HostB 也可以是同一台机器,换句话说就是内网 HostA 把自己可以访问的端口暴露给了外网 HostC。

相当于再 HostA 上启动了 frpc,而再 HostC 上启动了 frps。

本地 socks5 代理

在 HostA 的本地 1080 端口启动一个 socks5 服务,通过本地 socks5 代理的数据会通过 ssh 链接先发送给 HostB,再从 HostB 转发送给远程主机:

ssh -D localhost:1080  HostB

那么在 HostA 上面,浏览器配置 socks5 代理为 127.0.0.1:1080,看网页时就能把数据通过 HostB 代理出去,类似 ss/ssr 版本,只不过用 ssh 来实现。

xshell 配置

新建连接配置隧道
image.png

打开此连接并在不断开的情况下,然后在浏览器上配置 sock5 代理为 127.0.0.1:1080

可以通过 查看 -> 隧道窗格 查看具体的 连接信息。

使用优化

为了更好用一点,ssh 后面还可以加上:-CqTnN 参数,比如:

$ ssh -CqTnN -L 0.0.0.0:PortA:HostC:PortC  user@HostB

其中 -C 为压缩数据,-q 安静模式,-T 禁止远程分配终端,-n 关闭标准输入,-N 不执行远程命令。此外视需要还可以增加 -f 参数,把 ssh 放到后台运行。

这些 ssh 代理没有断线重连功能,链接断了命令就退出了,所以需要些脚本监控重启,或者使用 autossh 之类的工具保持链接。

功能对比

正向代理(-L)的第一种用法可以用 iptable 的 port-forwarding 模拟,iptable 性能更好,但是需要 root 权限,ssh -L 性能不好,但是正向代理花样更多些。反向代理(-R)一般就作为没有安装 frp/ngrok/shootback 时候的一种代替,但是数据传输的性能和稳定性当然 frp 这些专用软件更好。

socks5 代理(-D)其实是可以代替 ss/ssr 的,区别和上面类似。所以要长久使用,推荐安装对应软件,临时用一下 ssh 挺顺手。

补充下 iptable 的 port-forwarding 设置

补充下 iptable 的 port-forwarding 怎么设置,十分管用的功能,两个函数即可:

#! /bin/sh

# create forward rule by source interface
# http://serverfault.com/questions/532569/how-to-do-port-forwarding-redirecting-on-debian
PortForward1() {
    local IN_IF=$1
    local IN_PORT=$2
    local OUT_IP=$3
    local OUT_PORT=$4
    local IPTBL="/sbin/iptables"
    echo "1" > /proc/sys/net/ipv4/ip_forward
    $IPTBL -A PREROUTING -t nat -i $IN_IF -p tcp --dport $IN_PORT -j DNAT --to-destination ${OUT_IP}:${OUT_PORT}
    $IPTBL -A FORWARD -p tcp -d $OUT_IP --dport $OUT_PORT -j ACCEPT
    $IPTBL -A POSTROUTING -t nat -j MASQUERADE
}

# create forward rule by source ip
# http://blog.csdn.net/zzhongcy/article/details/42738285
ForwardPort2() {
    local IN_IP=$1
    local IN_PORT=$2
    local OUT_IP=$3
    local OUT_PORT=$4
    local IPTBL="/sbin/iptables"
    echo "1" > /proc/sys/net/ipv4/ip_forward
    $IPTBL -t nat -A PREROUTING --dst $IN_IP -p tcp --dport $IN_PORT -j DNAT --to-destination ${OUT_IP}:${OUT_PORT}
    $IPTBL -t nat -A POSTROUTING --dst $OUT_IP -p tcp --dport $OUT_PORT -j SNAT --to-source $IN_IP
}

第一个函数是按照网卡名称设置转发:

PortForward1 eth1 8765 202.115.8.2 8765

这时,本地 eth1 网卡的 8765 端口就会被转发给 202.115.8.2 的 8765 端口。

第二个函数是按照本机的 ip 地址,比如本机是 192.168.1.2:

PortForward2 192.168.1.2 8765 202.115.8.2 8765

那么任何访问本机 192.168.1.2 这个地址 8765 端口,都会被转发到 202.115.8.2:8765

这个 iptable 的 port forwarding 是内核层运行的,性能极好,只不过每次重启都需要重新设置下。

参考连接

https://zhuanlan.zhihu.com/p/57630633