# TCP/IP、《关于单隧道实现多服务访问的端口转发状态管理的研究》

摘要

本文提出的是一种在单隧道内实现多连接的技术,通过简单四元组映射实现多连接状态管理,支持TCP+UDP。


1、引言

在虚拟专用网络(VPN)、端口映射等引用中,如P2P打洞成功的单隧道上,或家庭网络路由器中使用端口映射的单端口隧道上,实现多服务访问。

以一对一举例说明,访问端和服务端一对一,和访问端单监听和访问端服务一对一

  1. A端监听11111端口
  2. A端访问127.0.0.1:11111,产生一个连接127.0.0.1:12345
  3. B端使用socket去连接3389,产生一个连接127.0.0.1:22345
  4. A端从12345接收数据,通过隧道发送到B端,写入22345
  5. B端从22345接收数据,通过隧道发送到A端,写入12345。
A访问A监听B连接B服务
127.0.0.1:12345127.0.0.1:22345
127.0.0.1:12346127.0.0.1:11111隧道127.0.0.1:22346127.0.0.1:3389
127.0.0.1:12347127.0.0.1:22347

2、实现原理

2.1、几个问题

实现多连接状态管理需要重点关注的几个问题。

  1. 多客户端:在一对一时,我们可以使用简单四元组(SrcAddr,SrcPort,DstAddr,DstPort)即可实现映射,当多对一时,如A、C、D 都去访问B,那SrcAddr和SrcPort就有可能冲突,我们需要任意多数量访问端对单一服务端
  2. 多监听:也有可能访问端和服务端一对一,但是访问端监听多个服务,比如A端11111对应B端3389、22222对应B端3306,可以任意多数量,我们需要任意多访问端监听对服务端任意多服务
  3. 握手流程:为确保在访问段发送数据到B端时,B端已成功连接,我们模仿TCP握手流程

2.2、数据包头结构

我们定义一下数据包头结构,使用一个简单的头结构支持TCP+UDP。

包头
ForwardFlags Flag //操作标志
ProtocolType PType //协议 TCP/UDP
ushort Port //TCP表示A端连接端口 12345,UDP为监听端口 11111
uint SrcAddr //TCP表示B端连接ip 127.0.0.1,UDP为A连接 127.0.0.1
ushort SrcPort //TCP表示B端连接端口 22345,UDP为A连接 12345
uint DstAddr //B端服务ip 127.0.0.1
ushort DstPort //B端服务端口 3389

ForwardFlags 列表
Fin = 0b00000001 //--
Syn = 0b00000010 //A新建连接
Rst = 0b00000100 //A断开连接
Psh = 0b00001000 //A发送数据
Ack = 0b00010000 //反向标志
SynAck = Syn | Ack //B连接,就是B端对A的回复
PshAck = Psh | Ack //B发送数据
RstAck = Rst | Ack //B关闭连接

2.3、通信流程

虽然包头结构相同,但TCP和UDP的流程会稍有不同,我们分开梳理。

2.3.1、TCP

此TCP方式映射方式最关键的点在于,A端使用12345+127.0.0.1+3389的组合,这是唯一的,B端使用自己本地的127.0.0.1+22345+127.0.0.1+3389的组合也是唯一的,这能在多对多中依然能保持唯一映射。

  1. A:监听11111
  2. A.Syn:A访问 127.0.0.1:11111,收到连接 127.0.0.1:12345,先不接收SocketA的数据
    1. 构建包头:PacketA(Syn,TCP,12345,0,0,127.0.0.1,3389)
    2. 添加映射:(0,12345,127.0.0.1,3389),SocketA
    3. 发送:发送包到B端
  3. B.Syn->SynAck->PshAck:B端收到Syn,去连接 127.0.0.1:3389,产生连接 127.0.0.1:22345
    1. 构建包头:PacketB(SynAck,TCP,12345,127.0.0.1,22345,127.0.0.1,3389)
    2. 添加映射:(127.0.0.1,22345,127.0.0.1,3389),SocketB
    3. 发送:发送SynAck包到A端
    4. 更改标志:发送SynAck之后,可以把标志改为PshAck,后续就是发送数据了
    5. 接收数据:后续可以从SocketB接收数据然后发往A端
  4. A.SynAck->Psh:A收到PacketB
    1. 获取映射:根据PacketB,可以构建出key (0,12345,127.0.0.1,3389) 获取到PacketASocketA
    2. 更新PacketA:在PacketB中传回了srcAddr 和 SrcPort,填进PacketA中,并修改标志为Psh,最后变为PacketA(Psh,TCP,12345,127.0.0.1,22345,127.0.0.1,3389)
    3. 接收数据:接下来可以继续从SocketA接收数据,然后发往B端
  5. A.Rst:A主动断开,就用Rst,B端可以key (127.0.0.1,22345,127.0.0.1,3389)移除对应信息
  6. B.RstAck:B端主动断开,就用RstAck,A端构建key (0,12345,127.0.0.1,3389)移除对应信息
2.3.2、UDP

在UDP,方式有所不同,B端往A发回数据时,我们需要使用监听11111的SocketUdp往源地址发送数据,所以A端不需要映射,只需要包头的Port 11111获取到SocketUdp(如果是多个监听),往包头的srcAddr:srcPort发送数据即可。而B端,为了保证映射唯一,只需要在映射key上加一个隧道id即可,隧道是一对一的,比如隧道id为1,则key(1,127.0.0.1,12345,127.0.0.1,3389)。

  1. A:监听 11111 SocketUdp
  2. A.Psh:SocketUdp接收数据
    1. 构建包头:PacketA(Psh,Udp,11111,127.0.0.1,12345,127.0.0.1,3389)
    2. 发送:直接发往B端
  3. B.Psh:B端收到数据包
    1. 构建包头:PacketB(PshAck,Udp,11111,127.0.0.1,12345,127.0.0.1,3389)
    2. 添加映射:构建key(1,127.0.0.1,12345,127.0.0.1,3389) 添加映射 (1,127.0.0.1,12345,127.0.0.1,3389),SocketB
    3. 接收数据:在添加映射后,先把A端来的数据写入SocketB,然后从SocketB接收数据,发往A端
    4. 后续Psh:在后续Psh中,已经存在映射,使用SocketB写入数据即可
  4. A.Psh:A端收到B端的PshAck,可以使用其中的port 11111 获取到SocketUdp,发送数据到srcAddr:srcPort
  5. Rst/Psh:对于UDP,Rst不一定需要,只需要使用一个定时器检查最后通信时间,超过一定时间则删除映射即可

3、未来

在单隧道上,队头阻塞问题像是一个毒瘤,在未来,这将是一个研究重点。