漫谈打洞

September 2, 2007 1:39 am UTC | In Tech

网络总是有缺陷的,一种常见的情形是,Alice 想向 Bob 建立 TCP 连接,但是因为种种原因不能直接建立连接。如果有一台机器和两边都没有连接障碍,那么就有可能通过这台机器帮助从 A 向 B 建立连接。一般把这个工作叫做建隧道,或者搭桥,不过好像也有人叫打洞,我觉得打洞这个名字听起来好玩一些,所以标题里就叫打洞吧。

A. 直接端口映射

先从最土鳖的说起。最简单的洞是在某台机器 C 上开一个 daemon 监听一个特定端口,然后把 A 发过来的数据都原封不动的转发给 B,这里假设 C 到 A 和 B 的通信都是毫无阻碍的。网上流传了两个版本的 datapipe.c 1 2。后者是前者的徒弟,当然也比新。不过后者带了 idle timeout 5 分钟,很容易改掉。另外一个简洁的程序是 rinetd。总之这些程序大同小异,对于最简单的应用足够了。究其本质,其实它就是把来去网络包的 src 和 dst 改了。所以上面这类 daemon 的工作可以用两条 iptables 规则搞定(nat 表):

-A PREROUTING -p tcp -m tcp --dport 9998 -j DNAT --to-destination Bob:9999
-A POSTROUTING -d Bob -p tcp -m tcp --dport 9999 -j SNAT --to-source C

这样就把发给 C:9998 的数据转发给 B:9999 了。当然了,iptables 需要 root 权限,所以比 daemon 权限要求高一些。另外一点是,如果 C 是 B 的网关,那么第二条 iptables 规则是不需要的,这时候第一条规则就相当于路由器配置常见的端口映射。

B. ssh 端口映射

稍微高级一点,可以用 ssh 端口映射。最简单的例子来说,比如 B 在内网所以 A 没办法向 B 建立连接,但是 B 的内网中有一台 D 可以操作并向外网的 A 建立连接。如果 A 这里有 sshd server,就可以从 D 用 ssh -R 来反向建立通道:

ssh -R 8888:Bob:8889 Alice

这样在 A 上连 localhost:8888 的时候,ssh 通道会自动把网络包发回 B 机器的 8889 端口。这里如果能操作 B,那么从 B 直接 ssh A 也可以,就不需要 D 了。在没有网关权限的情况下,这个方法是从外网连内网机器的最基本的方法。

ssh 的 -L 和 -R 两个参数神通广大,灵活运用可以打各种洞。比如假设 A B C 都是互联网上的机器,但是 A B 通讯不畅,想通过 C 中转。可能的方法有:从 C 往 A ssh -R,从 C 往 B ssh -L,从 A 往 C ssh -L,从 B 往 C ssh -R。有一个要注意的问题是 -L -R 参数开的端口往往只监听 localhost,绑定到外网 IP 可能被禁止,这时候可以再建一个本地的洞从 0.0.0.0:某个端口打通到那个本地洞。这些不同的方法本质一样,但是受各类网关和权限限制,或者 QoS 的影响,或者安全性的考虑,往往某个方法优于别的方法,所以可以根据实际情况选择。

比较新的 ssh 开始带 -D 参数,可以将 ssh 通道当作 socks4 代理使用。这个尤其适合连 B 上的 ftp。从 A 通过另一台机器 C 连 B 的 ftp 碰到的问题是数据连接端口是动态的,无论 PORT 还是 PASV 模式都需要特殊配置。比较简单的方法是:A 往 C ssh -D 8899 端口,然后配置 A 本地 ftp 软件使用 socks4 代理:localhost:8899,这样 ftp 软件就可以正常工作了。这个方法也可以用于浏览有 IP 限制的网段。比如从国外无法浏览中国教育网里面的网页,如果可以用 ssh -D 挂上一台能直连教育网的机器,那么配置浏览器使用这个 ssh 通道的 socks4 代理就可以了。另外比如你想下 bt 但是又想避开米国的耳目,或者你对某段网路的安全不放心,也可以用这个办法来方便的加密传输数据,因为 ssh 通道内传送的数据是加密的。

C. openvpn 大法

最后出场的就是 openvpn 了。 ssh 端口映射一般不需要 root 权限,但是 openvpn 需要,当然 openvpn 也会灵活得多。针对 A 无法直接连 B 的问题,最简单的 openvpn 解法是从 B 连上 A 的 openvpn server,这样就可以从 A 直接向 B 的 openvpn ip 连接了。这个方法和 ssh 端口映射比似乎没有什么优势,不过我碰到过土鳖破网关,openvpn 自动重连可以保证可靠的连接,虽然要折腾出可靠的 ssh 也不是不可能的。

openvpn 虚拟出一个网卡, 在网络配置上要灵活得多。一个常见的应用是将外网机器接入内网子网。假设 C 和 B 在一个子网 192.168.1.0/24,C 有外网 IP(不一定要是 B 的网关)。这时候在 C 上架 openvpn server,配置 openvpn.conf:

push "route 192.168.1.0 255.255.255.0"

如果 C 不是这个子网的网关,还需要在 iptables 配置 SNAT(nat 表):

-A POSTROUTING -s 10.38.0.0/255.255.255.0 -d 192.168.1.0/255.255.255.0 -j SNAT --to-source C

这里 10.38.0.0/24 是 openvpn 的网段。这样配置之后,A 连上 C 的 openvpn 之后 A 去 192.168.1.0/24 的网络包就自动走 openvpn,几乎所有需要使用这个内网的程序都不需要任何配置就可以直接使用,连接 B 自然也没有问题。

现在绝一点,还是上面的情况,假设 B 这个子网里面没有机器有外网 IP,B 的网关没有权限,而且 A 在另一个子网,A 的网关也动不了手脚,那么怎么让 A 能够进入 B 这里的内网呢?这时候可以找外网一台自由的机器 D,让 A B 都连上 D 的 openvpn server,这样就可以利用 openvpn 客户端之间的通讯了。具体配置在 D 的 openvpn server 上有,配置 openvpn.conf:

client-config-dir ccd
route 192.168.1.0 255.255.255.0
client-to-client

配置 ccd/B:

iroute 192.168.1.0 255.255.255.0

配置 ccd/A:

push "route 192.168.1.0 255.255.255.0"

B 上面需要配置 SNAT:

-A POSTROUTING -s 10.38.0.0/255.255.255.0 -d 192.168.1.0/255.255.255.0 -j SNAT --to-source B

这样就可以了,A 连上 openvpn 以后应该会显示 192.168.1.0/24 路由走 openvpn 网卡。

可以看到,openvpn 配置了底层的路由表,所以大多数应用程序都不需要设置就可以直接使用这个洞了,这对于一些没有设置 socks 代理功能的软件尤其有用。更高级的应用还可以做 taprd,直接在外网去拿内网的 DHCP 地址等等。这些配置都是高科技武器,不一一叙述,不过我个人的体会是只有你想不到,没有你做不到。

D. 杂问题

  • openvpn 用 tcp 还是 udp,参见这个链接。我实测在高速可靠网路上,两者似乎没有区别。不过还是推荐 udp。
  • 性能。大多数打洞的方法是会损失性能的,包括网络速度和 CPU 的负荷。破机器或者嵌入式机器需要注意一下 CPU 负荷是不是性能瓶颈。网络速度一般会达不到满带宽,就 openvpn 来说,一般至少会损失 20% 的网络速度,去掉加密,加大压缩可能会有帮助。tunnel over ssh 属于 tcp over tcp,在不可靠网络上速度尤其会受到影响。不过带加密的洞在 tcp checksum 的基础上还有一层验证,数据可靠性很高。我曾经在极不稳定的网路上用 ftp over ssh 传过快 1 TB 的数据,一个 bit 也没有传错。
  • 不修改 src 的端口映射。打洞一般碰到的一个问题是,在目标机器 B 上看,连接是从中间机器 C 来的了,无法知道最初的连接来自哪里,这个问题在某些情况下是很不爽的。那么为什么 C 需要修改网络包的 src 呢? 这是由于一般来说路由表是根据网络包 dst 地址来配置的,所以如果 C 不修改网络包 src 地址,B 回应 A 的网络包就未必会经过 C 了。解决办法很自然:要么把 C 设置成 B 的网关,要么在 B 以及所有 B 和 C 之间的路由器上设置策略路由(policy-routing,根据 src 而不是 dst 选择路由)。在只有 B 有配置权限的情况下,可以用 openvpn 把 B 和 C 打通,保证 B 可以不通过路由器直接把网络包发送给 C。

其实很多打洞原理和方法说明白了也没什么奥妙,不过本文部分内容鄙人还是从康神czz 这里学到的,致敬!

Tags: , , , , ,

6 Comments »

RSS feed for comments on this post.

  1. 来拜一下acore。嗯。
    技术男。哈哈。

    Comment by fancy — September 4, 2007 2:37 am UTC #

  2. 似乎还有一种打洞的办法 就是某些vpn软件用的
    就是两台内网A B机器在服务器C的帮助下 约好一个时间 同时向对方发udp数据包 由于TCP的性质 会认为对方的数据包正好是己方数据包的reply 所以防火墙不会阻拦 然后就不需要C了 A B可以直连了
    不知道上面的openvpn是不是这种?

    Comment by hayate — November 14, 2007 1:22 am UTC #

  3. 再补充一个
    有种叫teredo的技术可以用来打洞,原理是ipv6 over udp
    所以NAT必须允许udp,并且NAT必须不能是对称NAT
    不过貌似适用范围还是很广的。好处是连接建立后,是点对点的不需中转,速度很快。缺点是需要软件支持ipv6

    Comment by hayate — March 2, 2008 9:59 am UTC #

  4. Hi atppp.
    ssh的打洞是单向的,A->C->B ,需要一个中间的C,到A和B都是通的。能否找到双向的打洞方式? 假如C仅能到B,A仅能到C。有没有办法让B可以访问A上的资源?

    Comment by rmrf — June 9, 2008 7:49 pm UTC #

  5. 嗯。。给一个土鳖办法:第一步先在 A 上:

    ssh -L 4444:B:22 C

    之后在 A 另一个终端:

    ssh -R 5555:127.0.0.1:80 localhost -p 4444

    之后 C 上可以访问 localhost:5555 来访问 A 的 web。

    不知道是否有错。。

    Comment by atppp — June 9, 2008 8:50 pm UTC #

  6. 我有机会试试吧,貌似可行

    Comment by rmrf — June 10, 2008 10:10 pm UTC #

Leave a comment

XHTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

This weblog is licensed under a Creative Commons License.
Powered by WordPress. Theme based on Pool by Borja Fernandez.