V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
myqoo
V2EX  ›  程序员

关于 TCP 打洞的一些问题,有尝试过的请教一下

  •  
  •   myqoo · 2019-10-02 18:14:46 +08:00 · 11602 次点击
    这是一个创建于 1885 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近发现家里的垃圾宽带(华数网通)居然支持 TCP 打洞,并成功将内网的 HTTP 服务暴露到公网上。

    原理很简单。先连接一个公网服务,获得该连接的公网 IP 和公网端口。然后开启一个 HTTP 服务,端口指定为之前连接的本地端口(端口重用)。然后用其他设备测试,例如手机 4G 访问 http://公网 IP:公网端口,成功显示出 HTTP 服务。(实现超简单,结尾附上 nodejs 演示)

    目前遇到几个疑惑,不知是否为正常现象:

    1.每个 http://公网 IP:公网端口 访问几次后就不能再访问了,需要重新打洞更换公网端口才可以。

    并且每个连接时间不能太久,否则会被断开。我测试大文件下载,速度不错有 2MB/s (对于¥ 200/年的宽带也算满意了),但大约 10 分钟左右下了一个多 GB 就被断开了。

    这是因为该公网 IP 有很多人在用,导致端口被其他人占用了吗?假如我同时发起成千上万的 TCP 连接,是不是也会把同个公网 IP 下其他人正在使用的端口给抢占呢。

    2.当其他设备访问 http://公网 IP:公网端口 时,原先的 TCP 连接不再可用(演示中的心跳包无法收到)。

    TCP 打洞成功后,原先的连接就不可用了?这意味着,本地任何一个 TCP 连接都可以被恶意者破坏(假如恶意者知道该连接的公网端口)。事实上我用服务器扫了下自己公网 IP 的所有端口,发现确实可以把本地开着的 TCP 连接都断开!!!感觉这是运营商配置的问题,不是正常现象吧,要不然安全性也太低了。

    3.OSX 和 Windows 上支持端口重用,但 Linux 却不支持,报端口被占用的错。

    我搜了下 nodejs 官网关于 net 模块的介绍 https://nodejs.org/api/net.html 其中提到 All net.Socket are set to SO_REUSEADDR (see socket(7) for details). 不明白为什么 Linux 系统不支持端口重用?

    最后贴上 nodejs 的演示代码。我试了很多运营商都不行,但华数宽带可以实现(最好是直接在电脑上拨号,因为有些路由器也会影响打洞):

    const net = require('net')
    const http = require('http')
    
    function runHttpSvr(port) {
      http.createServer((req, res) => {
        const ip = req.connection.remoteAddress.replace('::ffff:', '')
        const addr = ip + ':' + req.connection.remotePort
        console.log('client:', addr)
        res.end('Hello: ' + addr + '\n')
      }).listen(port, _ => {
        console.log('server running...')
      })
    }
    
    const conn = net.createConnection(50000, '106.75.17.52', () => {
      const localPort = conn.localPort
      console.log('localPort:', localPort)
    
      let pong = 0
    
      conn.on('data', data => {
        if (data == 'PONG') {
          console.log('PONG:', pong++)
          setTimeout(ping, 1000)
          return
        }
        console.log('curl http://' + data)
        runHttpSvr(localPort)
      })
      conn.on('end', _ => {
        console.log('end')
      })
      conn.on('error', err => {
        console.log('error:', err)
      })
      conn.setEncoding('utf8')
    
      function ping() {
        conn.write('PING')
      }
      ping()
    })
    
    
    21 条回复    2019-12-26 14:13:49 +08:00
    des
        1
    des  
       2019-10-02 19:04:04 +08:00 via Android
    frp 了解下?
    KaneLin1217
        2
    KaneLin1217  
       2019-10-02 19:06:49 +08:00 via iPhone
    zerotier+自建 moon 了解一下?
    widewing
        3
    widewing  
       2019-10-02 19:48:15 +08:00 via Android   ❤️ 4
    @des @KaneLin1217 你俩知道楼主在说啥吗?
    wweir
        4
    wweir  
       2019-10-02 19:53:07 +08:00 via Android
    @widewing frp 的 xtcp 倒是和楼主说的是一个东西
    reus
        5
    reus  
       2019-10-02 19:58:30 +08:00   ❤️ 1
    不就是 NAT 嘛……
    ClarkAbe
        6
    ClarkAbe  
       2019-10-02 20:06:06 +08:00 via Android
    github 有个东东叫“N2N”,简化的 tcp 打洞虚拟组网,不过近几年能成功完全 tcp 打洞完全看运气
    xieyudi2
        7
    xieyudi2  
       2019-10-02 20:22:26 +08:00 via Android
    这也可以… NAT 都不带验证 remote IP:port 的吗…
    我妈也用的是广电的网。连接超过一定时间会被释放掉…
    gam2046
        8
    gam2046  
       2019-10-02 20:32:02 +08:00   ❤️ 3
    TCP 打洞....读了两遍原文,基本了解下来就是 NAT 内网穿透,实现方式与原理均一致。你的实现方式大致是这样的
    Client -> Router -> Server
    举例
    C: 10.10.10.1:7890/TCP => R: 10.10.10.1:7890/TCP <--> 61.61.61.1:23456/TCP => S: 61.61.61.1 -> :80/TCP

    你的问题 1:每个连接时间不能太久,会被断开

    不正常。活跃的端口映射不应该被断开,也就是 R 这里面的映射关系即使在不活跃的状态,也应该保持一段时间(如 2 分钟)更何况你下载时,是处于活跃状态。

    你的问题 2:假如我同时发起成千上万的 TCP 连接,是不是也会把同个公网 IP 下其他人正在使用的端口给抢占呢

    原则上是本着先到先得,单 IP 上的端口数量有限的,但这类二级运营商大量的飞针走线,内网穿透。真出现这种情况,也许会给你换个出口 IP。不过.....正常情况下,出口路由记录的映射关系是<内网 IP:端口 /出口端口 /目标服务器>,也就是说出口路由上的端口是可以复用的。多个内网 IP 出口均使用同一个公网端口,大家的目标服务器不同,返回的数据也可以依据服务器 IP 来区分转发给内网的哪个 IP

    你的问题 3:TCP 打洞成功后,原先的连接就不可用了

    讲道理,在出口路上上,只要映射关系还存在,是可以双向通讯的。但这取决于你所处网络环境的 NAT 类型(这个结论是基于 UDP 得到的,TCP 我不确定)。

    https://zh.wikipedia.org/wiki/UDP%E6%89%93%E6%B4%9E

    你的问题 4:OSX 和 Windows 上支持端口重用,但 Linux 却不支持,报端口被占用的错

    超出射程了,不太了解你指的端口复用是什么。
    myqoo
        9
    myqoo  
    OP
       2019-10-02 21:46:56 +08:00
    @gam2046 多谢解答。端口复用是指 http 服务 listen 已存在连接的 conn.localPort (相当于两个 socket 用同个源端口),我在 linux 下用 iptables 倒是勉强实现了,但总觉得不优雅,而且需要 root 权限。
    fvckDaybyte2
        10
    fvckDaybyte2  
       2019-10-02 22:02:13 +08:00
    @widewing 第一段看了几遍都没看懂到底在说啥,访问的到底是谁的公网 IP,http 服务是开在哪里的,“连接一个公务服务”是用什么协议连接,又到底是什么服务,最好还是有个图吧……
    fvckDaybyte2
        11
    fvckDaybyte2  
       2019-10-02 22:27:40 +08:00   ❤️ 4
    看了好几遍终于懂了,很正常的 NAT traversal 现象,具体参考 RFC4787,概括来说就是,如果客户端 A 访问服务器 B 的 20000 端口,且数据经过一层 NAT,而 NAT 将此次访问映射到了端口 10000,则在一段时间内这个映射关系会被保留
    此时 NAT 分为 3 个类型:
    Endpoint-Independent Mapping:在这段时间内任何服务器发往 10000 端口的数据都将被转发给 B (最容易打洞的类型)
    Address-Dependent Mapping:在这段时间内只有来自服务器 B 的数据会被转发给 B (一般家庭宽带常用的 NAT 类型)
    Address and Port-Dependent Mapping:在这段时间内只有来自服务器 B 的 20000 端口的数据才会被转发(一般 4G 网络的 NAT 类型)

    因此:

    “2.当其他设备访问 http://公网 IP:公网端口 时,原先的 TCP 连接不再可用”
    很明显你的宽带 NAT 不可能是第一种类型,因此会拒绝来自其他 IP 的数据包

    1.每个 http://公网 IP:公网端口 访问几次后就不能再访问了,需要重新打洞更换公网端口才可以。
    有可能是宽带 NAT 映射关系过期了,必须有 TCP 或者 UDP 的 keepalive 包来保持这个映射,也有可能是你手机 4G 的 NAT 映射关系过期了,因此每次发出来的包端口不一样,而你的宽带是第三种类型会拒绝不同端口的数据包。两边都是 NAT 会有许多种情况。

    写一个小程序先验证一下 NAT mapping behavior 是哪种类型的,很快就能定位了
    CallMeReznov
        12
    CallMeReznov  
       2019-10-02 22:46:38 +08:00
    动态 NAT?
    wslzy007
        13
    wslzy007  
       2019-10-02 23:43:58 +08:00
    基于 tcp 进行打洞的内网穿透工具,见: https://github.com/lazy-luo/smarGate
    realpg
        14
    realpg  
       2019-10-03 11:49:29 +08:00
    NAT 的类型了解一下
    j4fun
        15
    j4fun  
       2019-10-03 14:34:47 +08:00   ❤️ 1
    1、Linux 要 reuseport 需要端口都设置,所以你要先 bind 获取一个端口,再 reuseport,再 conn 你的 STUN 服务器。最后再 reuseport 你的 http server。
    2、端口变化很正常,这个取决于运营商 /路由器。NAT 1 (就是你现在的 NAT 类型)类型的有部分用户会变化包括 UDP/TCP。你可以把代码拿给其他 NAT1 的朋友试试有的是不会变的,有的是会几小时变一次。
    myqoo
        16
    myqoo  
    OP
       2019-10-03 15:03:28 +08:00
    @j4fun 试了下确实如此,给第一个连接指定 localPort 就没问题了。 感谢!
    feast
        17
    feast  
       2019-10-03 15:10:54 +08:00 via Android
    你这种一般属于 NAT 网关极为特殊才会出现这种情况,首先很显然是 Restricted Cone NAT,不验证来访 IP 是否等于请求 IP,特殊的在于 TCP 是有状态的有一个三次握手,绝大多数网关都会有这样一个表用来储存这个特定 TCP 握手信息,只要有一点地址信息不符就会拒绝数据包,所以你这个网关很特殊,一般只有无状态的 UDP 的才能打洞
    feast
        18
    feast  
       2019-10-03 15:30:00 +08:00 via Android
    你真要研究 NAT 打洞,可以看看目前大部分运营商的 CGN NAT PCP 端口控制协议,github.com/libpcp/pcp
    LBL584520
        19
    LBL584520  
       2019-10-05 16:07:32 +08:00 via iPhone
    @feast

    /t/603512

    有现成编译好的工具 /软件 来测试吗?
    不是专业的,不会编译


    有的话麻烦发一个,
    我测试一下我这移动宽带
    feast
        20
    feast  
       2019-10-06 16:56:06 +08:00 via Android
    @LBL584520 linux 下 autogen.sh && make 即可
    ofblyt
        21
    ofblyt  
       2019-12-26 14:13:49 +08:00   ❤️ 1
    推荐博客
    http://www.cnblogs.com/pannengzhi/p/4800526.html
    这上面的打洞可以说是很清楚了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2865 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 06:41 · PVG 14:41 · LAX 22:41 · JFK 01:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.