TCP穿透(绕过监听套接字)

3
几天来,我一直在研究“洞穴打洞”,希望获得一些可靠的行为,但是现在我已经陷入了死胡同。
UDP洞穴打洞效果很好:首先向远程发送一个数据包,然后让远程发送数据包另一侧,因为它将通过源NAT传输。从我试过的情况来看,这相当可靠。
但是现在来了TCP……我不明白。
目前为止,我只能通过连接套接字与 NAT 建立联系。
A.connect(B) -> Crash agains't B's NAT, but open a hole in A's NAT.
B.connect(A) -> Get in A's NAT hole, reach A's connecting socket.

但是现在,用于连接的两个发送SYN数据包的套接字已经连接成功。
你可能会认为我已经做到了,通过2个NAT获得了连接,太棒了。
但问题在于,这不是正常的行为,根据这篇论文:http://www.brynosaurus.com/pub/net/p2pnat/,我应该能够同时拥有一个监听套接字和一个连接套接字。
因此,我绑定了一个监听套接字,可以接受传入的连接。
但是传入的连接始终被连接套接字捕获,而不是监听套接字... 例如:
#!/usr/bin/env python3
from socket import *
from threading import Thread
Socket = socket

# The used endpoints:
LOCAL = '0.0.0.0', 7000
REMOTE = 'remote', 7000

# Create the listening socket, bind it and make it listen:
Listening = Socket(AF_INET, SOCK_STREAM)
Listening.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
Listening.bind(LOCAL)
Listening.listen(5)

# Just start in another thread some kind of debug:
# Print the addr of any connecting client:
def handle():
    while not Listening._closed:
        client, addr = Listening.accept()
        print('ACCEPTED', addr)
Thread(target=handle).start()

# Now creating the connecting socket:
Connecting = Socket(AF_INET, SOCK_STREAM)
Connecting.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
Connecting.bind(LOCAL)

# Now we can attempt a connection:
try:
    Connecting.connect(REMOTE)
    print('CONNECTED', Connecting.getpeername())
except Exception as e:
    print('TRIED', type(e), e)

现在有了这个脚本,只需与朋友或其他人商定一个端口,并在一端执行它,Connecting.connect(...)应该会运行一段时间(等待超时,因为SYN数据包撞到了远程NAT,但幸运的是打开了自己NAT中的一个洞),同时在另一端执行脚本,现在Connecting.connect(...)将返回,因为它已连接。

最奇怪的部分是:Listening套接字从未被触发

为什么?如何使侦听套接字捕获传入的连接而不是连接套接字?

注意:关闭连接套接字会发送一些东西到网络上,立即关闭洞口,至少在我的网络上是这样的。

第二个注意事项:我使用的是Windows。

编辑:主要问题是,在任何情况下,此脚本输出CONNECTED [...]而不是CLIENT [...],根据某些讲座,这不应该发生。

你尝试过在监听端获取数据包捕获吗?你能在那里看到来自远程端的SYN请求吗? - Dmitry Poroh
是的,正如我所说,每个端口(远程/本地)上的套接字确实建立了连接,但不是在“侦听”套接字上... - WKnight02
2个回答

2
所以,在更多的测试和阅读之后,这就是我得出的结论:
事实上,可以在同一地址(IP、端口)上绑定一个监听套接字和一个进行出站连接的套接字。
但是套接字的行为严重依赖于系统/TCP堆栈的实现,正如在§4.3中提到的:http://www.brynosaurus.com/pub/net/p2pnat/
客户端应用程序在TCP打洞期间观察到套接字发生的情况取决于时间和涉及的TCP实现。假设A向B的公共端点的第一个出站SYN数据包被NAT B丢弃,但是在A的TCP重新传输其SYN之前,B的第一个随后的SYN数据包通过A的公共端点到达了A。根据涉及的操作系统,可能会发生以下两种情况之一:
- A的TCP实现注意到传入SYN的会话端点与A正在尝试启动的出站会话的端点匹配。因此,A的TCP堆栈将此新会话与本地应用程序在A上使用的套接字相关联,以连接()到B的公共端点。应用程序的异步connect()调用成功,并且应用程序的侦听套接字不会发生任何事情。
由于接收到的SYN数据包没有包含对A先前出站SYN的ACK,A的TCP回复B的公共端点以SYN-ACK数据包,其中SYN部分仅是A原始出站SYN的重播,使用相同的序列号。一旦B的TCP接收到A的SYN-ACK,它就会响应自己对A的SYN的ACK,TCP会话在两端进入连接状态。 - 或者,A的TCP实现可能会注意到A在该端口上具有等待传入连接尝试的活动侦听套接字。由于B的SYN看起来像是一个传入的连接尝试,A的TCP创建了一个新的流套接字,以将新的TCP会话关联起来,并通过应用程序在其侦听套接字上的下一个accept()调用将此新套接字交给应用程序。然后,A的TCP像上面一样向B响应SYN-ACK,并且TCP连接设置按照客户端/服务器式连接通常进行。
由于A之前对B的出站connect()尝试使用了另一个套接字刚刚通过accept()返回给应用程序的源和目标端点的组合,因此A的异步connect()尝试必须在某个时候失败,通常会出现“地址正在使用”错误。尽管如此,应用程序仍具有所需的工作点对点流套接字,可以与B通信,因此它忽略了此故障。
上述第一种行为似乎是基于BSD的操作系统通常采用的方式,而第二种行为在Linux和Windows下更为常见。
因此,我实际上处于第一种情况。在我的Windows 10上。
这意味着要创建一个可靠的TCP穿透方法,需要同时绑定监听套接字和连接套接字,但之后需要检测哪个触发了(监听或连接),并将其传递到应用程序的流程中。

唯一让我困扰的是我只看到了其中一种行为,但我想我会把这段话当作已知情况并相应地采取行动。 - WKnight02
你有代码示例吗?你是如何解决这个问题的? - Amar
@Amar 你可以从我在原问题中编写的代码中获得灵感。我的问题是关于我观察到的行为,根据http://www.brynosaurus.com/pub/net/p2pnat/,这似乎是正常的。因此,根据您的平台,可能会发生不同的行为。然后就轮到您的应用程序检测触发了哪种行为。我没有更多的代码可分享。 - WKnight02

1

为什么监听套接字没有触发

我认为答案就在这里。TCP连接由四个元素的元组定义:

  • 本地地址
  • 本地端口
  • 远程地址
  • 远程端口

当您建立TCP连接时,将此元组绑定到本地主机上的连接套接字。

当通过NAT发送SYN时,它会创建绑定: - 本地地址/端口 -> 公共地址/端口

当远程方向公共地址/端口发送其SYN时,这些地址被转换为本地地址/端口并传递到本地计算机。在该计算机上,此连接与初始连接无法区分,并成功建立(使用SYN/ACK)。

这意味着本地端没有收到INITIAL SYN。

如何使监听套接字捕获连接套接字上的入站连接?

使用源NAT是不可能的。要接受NAT后面的新连接,您需要目标NAT,将某些公共IP/端口映射到您的私有IP/端口。


再次感谢您的回答,这个问题不再孤单真是太好了 :)现在,如果我理解正确,您是说我不能接受连接,因为我的NAT没有任何重定向? 因为这正是我试图通过Hole Punching解决的问题。基于此,那么我可以使用REMOTE ='localhost',7000作为示例运行脚本,它应该按预期工作吗? 没有更多的NAT,数据包在本地发送。实际上,它正在做同样的事情:连接套接字捕获连接,监听套接字不会。 - WKnight02
顺便说一句,无论我先绑定哪个套接字,连接套接字始终优先于监听/接受套接字。唉。 - WKnight02
我明白你的意思。但是如果我尝试使用我的远程主机作为“localhost”,那么我的监听套接字应该能够接受连接,而不是我的连接套接字建立到另一个连接的连接(A.connect(B) <==CONN==> B.connect(A))?但是从我所做的事情来看,即使在本地主机上,监听套接字也没有捕获连接。 - WKnight02
由于连接识别原因,在建立第二个连接时,您会向现有(连接)发送SYN,操作系统不会将其视为新连接。如果在第二个客户端上删除Connecting.bind(LOCAL),您将获得新连接。 - Dmitry Poroh
是的,但如果我解除绑定“Connecting”套接字,我将失去对地址的保证,因为我需要确切地知道从/到哪个端点发送数据包。无论如何,我认为我已经找出了问题所在,这似乎是实现相关的。请参见下面的答案,如果我漏掉了什么,请随时评论 :) - WKnight02
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接