TCP穿透的方法

TCP打洞比UDP打洞复杂一些,主要原因是:

  1. TCP需要三次握手
  2. TCP的NAT映射更严格
  3. 需要同时发起连接(Simultaneous Open)

TCP打洞流程

  1. 服务器记录客户端的公网地址和端口
  2. 客户端需要先绑定本地端口,使用 SO_REUSEADDR 重用端口
  3. 双方同时向对方发起连接,利用TCP的同时打开(Simultaneous Open)机制

工作原理

复制代码
Client A                    Server                    Client B
   |                          |                          |
   |--REGISTER--------------->|                          |
   |                          |<-------------REGISTER----|
   |                          |                          |
   |--QUERY B---------------->|                          |
   |<--ADDR B (IP:Port)-------|                          |
   |                          |                          |
   |========== Both clients try to connect ==========    |
   |---SYN (to B)------------>NAT-A                      |
   |                    (opens hole in NAT-A)            |
   |                          |        NAT-B<---SYN (to A)
   |                          |  (opens hole in NAT-B)   |
   |                                                      |
   |<===== TCP connection established through NAT ======>|

Simultaneous Open

参考

  1. https://ttcplinux.sourceforge.net/documents/one/tcpstate/tcpstate.html
  2. https://github.com/kota-yata/tcp-simultaneous-open
  3. TCP simultaneous open and self connect prevention

自测

  1. TCP没有打洞成功(也可能是我打开的方式不对)
  2. 在家庭网络有大概率成功的udp打洞,在tcp的环境下也不太好使了。

代码

python 复制代码
# Server (Public IP) - TCP Version
import socket
import threading
import json

clients = {}  # {client_id: (ip, port)}

def handle_client(conn, addr):
    try:
        while True:
            data = conn.recv(1024)
            if not data:
                break
                
            msg = data.decode()
            
            if msg.startswith("REGISTER:"):
                # Register client
                client_id = msg.split(":")[1]
                clients[client_id] = addr
                print(f"Registered: {client_id} -> {addr}")
                conn.sendall(b"OK")
                
            elif msg.startswith("QUERY:"):
                # Query peer address
                target_id = msg.split(":")[1]
                if target_id in clients:
                    target_addr = clients[target_id]
                    response = f"ADDR:{target_addr[0]}:{target_addr[1]}"
                    conn.sendall(response.encode())
                    print(f"Query: {addr} wants to connect {target_addr}")
                else:
                    conn.sendall(b"NOT_FOUND")
    except Exception as e:
        print(f"Error: {e}")
    finally:
        conn.close()

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(("0.0.0.0", 9999))
    sock.listen(5)
    print("TCP Server started on port 9999")
    
    while True:
        conn, addr = sock.accept()
        print(f"New connection from {addr}")
        threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()

if __name__ == "__main__":
    main()
python 复制代码
# Client A/B - TCP Version
import socket
import time
import threading

class TCPClient:
    def __init__(self, client_id, server_addr):
        self.client_id = client_id
        self.server_addr = server_addr
        self.local_port = None
        self.peer_addr = None
        self.connected = False
        
    def register(self):
        # Register to server and get local port
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.connect(self.server_addr)
        
        # Get local port after connection
        self.local_port = sock.getsockname()[1]
        print(f"Local port: {self.local_port}")
        
        # Register
        sock.sendall(f"REGISTER:{self.client_id}".encode())
        response = sock.recv(1024)
        print(f"Register response: {response.decode()}")
        
        sock.close()
        return self.local_port
    
    def query_peer(self, peer_id):
        # Query peer address from server
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(self.server_addr)
        
        sock.sendall(f"QUERY:{peer_id}".encode())
        response = sock.recv(1024).decode()
        
        if response.startswith("ADDR:"):
            parts = response.split(":")
            peer_ip = parts[1]
            peer_port = int(parts[2])
            self.peer_addr = (peer_ip, peer_port)
            print(f"Peer address: {self.peer_addr}")
        else:
            print("Peer not found")
            
        sock.close()
        return self.peer_addr
    
    def tcp_punch_hole(self):
        # TCP hole punching using simultaneous connect
        if not self.peer_addr:
            print("No peer address")
            return None
        
        # Create socket with same local port
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        # Bind to the same port used during registration
        sock.bind(("0.0.0.0", self.local_port))
        
        # Set non-blocking for simultaneous connect
        sock.setblocking(False)
        
        print(f"Attempting to connect to {self.peer_addr} from port {self.local_port}")
        
        # Try to connect (will likely fail first time, but opens NAT hole)
        max_attempts = 30
        for i in range(max_attempts):
            try:
                sock.connect(self.peer_addr)
                print("Connected immediately!")
                sock.setblocking(True)
                self.connected = True
                return sock
            except BlockingIOError:
                # Connection in progress
                pass
            except ConnectionRefusedError:
                # Expected during hole punching
                pass
            except OSError as e:
                if e.errno == 106:  # Already connected
                    print("Connection established!")
                    sock.setblocking(True)
                    self.connected = True
                    return sock
            
            # Wait and retry
            time.sleep(1)
            print(f"Retry attempt {i+1}/{max_attempts}")
            
            # Recreate socket for new attempt
            if i < max_attempts - 1:
                sock.close()
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                sock.bind(("0.0.0.0", self.local_port))
                sock.setblocking(False)
        
        print("Failed to establish connection")
        sock.close()
        return None
    
    def send_messages(self, sock):
        # Send messages continuously
        counter = 0
        while self.connected:
            try:
                message = f"MSG_{counter}".encode()
                sock.sendall(message)
                print(f"Sent: {message.decode()}")
                counter += 1
                time.sleep(2)
            except Exception as e:
                print(f"Send error: {e}")
                self.connected = False
                break
    
    def receive_messages(self, sock):
        # Receive messages continuously
        while self.connected:
            try:
                data = sock.recv(1024)
                if not data:
                    print("Connection closed by peer")
                    self.connected = False
                    break
                print(f"Received: {data.decode()}")
            except Exception as e:
                print(f"Receive error: {e}")
                self.connected = False
                break
    
    def start(self, peer_id):
        # Main flow
        print(f"=== Client {self.client_id} starting ===")
        
        # Step 1: Register
        print("Step 1: Registering...")
        self.register()
        
        # Wait a bit
        time.sleep(2)
        
        # Step 2: Query peer
        print("Step 2: Querying peer...")
        if not self.query_peer(peer_id):
            print("Cannot find peer")
            return
        
        # Wait for both clients to be ready
        print("Step 3: Waiting for both clients to be ready...")
        time.sleep(3)
        
        # Step 4: Punch hole and connect
        print("Step 4: TCP hole punching...")
        sock = self.tcp_punch_hole()
        
        if sock:
            print("=== Connection established! ===")
            # Start sending and receiving
            send_thread = threading.Thread(target=self.send_messages, args=(sock,), daemon=True)
            recv_thread = threading.Thread(target=self.receive_messages, args=(sock,), daemon=True)
            
            send_thread.start()
            recv_thread.start()
            
            # Keep alive
            try:
                while self.connected:
                    time.sleep(1)
            except KeyboardInterrupt:
                print("Interrupted")
            finally:
                self.connected = False
                sock.close()
        else:
            print("=== Connection failed ===")

# Example usage
if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 3:
        print("Usage: python client.py <client_id> <peer_id>")
        print("Example: python client.py A B")
        sys.exit(1)
    
    client_id = sys.argv[1]
    peer_id = sys.argv[2]
    server_addr = ("your.server.ip", 9999)  # Replace with your server IP
    
    client = TCPClient(client_id, server_addr)
    client.start(peer_id)
bash 复制代码
# Terminal 1 - Run server
python server.py

# Terminal 2 - Client A
python client.py A B

# Terminal 3 - Client B (almost same time)
python client.py B A

本系列的其他文章

  1. udp穿透的方法:https://blog.csdn.net/yeshennet/article/details/135165159
  2. udp穿透的方法V2:https://blog.csdn.net/yeshennet/article/details/157170723
  3. TCP穿透的方法:https://blog.csdn.net/yeshennet/article/details/157170822

AI总结

https://stackoverflow.com/a/6865020

复制代码
while true
do
    telnet 127.0.0.1 50000 
done

这是一个**TCP 自连接(Self-Connection)**的经典案例,完美展示了 TCP 同时打开的实际效果!


🎯 现象解释

发生了什么?

复制代码
正常情况:
Client (随机端口) → Server (50000端口) ❌ 连接被拒绝

巧合情况:
Client (恰好分配到50000) → Destination (50000端口) ✅ 自己连上自己!

关键条件

telnet 127.0.0.1 50000 时:

  1. 源端口:系统从临时端口池随机分配
  2. 目标端口:50000
  3. 巧合:当源端口恰好也是 50000 时

结果:

复制代码
127.0.0.1:50000 (source) → 127.0.0.1:50000 (dest)

这是同一个套接字四元组


🔄 握手过程

正常思维的困惑

复制代码
问:没有人在 listen(),谁发的 ACK?

实际发生的事

复制代码
1. 内核分配源端口 50000
2. 向 127.0.0.1:50000 发送 SYN
3. SYN 包到达本地,发现目标端口 50000
4. 内核发现:源和目标完全一样!
5. 触发 TCP 同时打开机制
6. 自己给自己回 SYN+ACK
7. 自己给自己发 ACK
8. 连接建立!

关键 :不需要 listen(),因为是同时打开,不是传统的客户端-服务器模式!


💬 "自己聊天"的原因

python 复制代码
# 发送数据
telnet> hello?

# 数据流向
127.0.0.1:50000 → 127.0.0.1:50000

# 接收数据(就是自己发的!)
hello?

🛡️ 不同系统的处理

系统 行为 原因
Linux 允许自连接 严格按照 RFC 793 实现
FreeBSD 允许自连接 同上
Darwin (macOS) 禁止源端口=目标端口 特意规避这个问题
c 复制代码
// Darwin 内核可能的检查
if (src_port == dst_port && src_ip == dst_ip) {
    return EADDRINUSE; // 禁止分配相同端口
}

📊 概率分析

为什么要"等一分钟"?

Linux 临时端口范围(默认):

bash 复制代码
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768   60999
  • 可用端口数:28232
  • 命中目标端口的概率:1/28232 ≈ 0.0035%

所以需要多次尝试才能碰到!


🎓 面试价值

考察点

  1. TCP 协议细节

    • 三次握手 vs 四次握手(同时打开)
    • RFC 793 的完整实现
  2. 操作系统差异

    • Linux/BSD 的标准实现
    • macOS 的特殊处理
  3. 调试思维

    • 为什么"没有服务器也能连接"?
    • 如何从现象推导本质?
  4. 实际问题

    • 高端口服务器可能遇到的问题
    • 如何避免(固定源端口 / 使用低端口)

🔧 复现方法

bash 复制代码
# 方法1:暴力尝试
while true; do telnet 127.0.0.1 50000; done

# 方法2:提高概率(缩小端口范围)
sudo sysctl -w net.ipv4.ip_local_port_range="50000 50010"
telnet 127.0.0.1 50000  # 很快就会成功

💡 实际意义

1. NAT 打洞

这正是你之前问的 TCP 打洞的原理!通过同时打开建立连接。

2. 防火墙规则

bash 复制代码
# 可能需要特殊规则防止自连接
iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 --sport 50000 --dport 50000 -j DROP

3. 服务器设计

避免使用高端口号(建议 < 10000),降低碰撞概率。


这个例子完美展示了 TCP 同时打开不是理论,而是可能实际发生的边缘情况! 🎯

相关推荐
AOwhisky2 小时前
iSCSI 网络存储服务从入门到精通
linux·运维·网络
橘颂TA2 小时前
【Linux 网络】拒绝传输卡顿!滑动窗口如何让数据 “跑赢” 等待?
运维·服务器·网络
maosheng11462 小时前
HCIP的多进程练习和路由策略
网络
“αβ”2 小时前
传输层协议--TCP协议
linux·服务器·网络·网络协议·tcp/ip·http·https
Elieal2 小时前
零基础入门 WebSocket:从原理到 Java 实战
java·websocket·网络协议
芯有所享2 小时前
【芯片设计中的ARM CoreSight IP:全方位调试与追踪解决方案】
arm开发·经验分享·网络协议·tcp/ip
终端域名2 小时前
如何保障网络架构变革下物联网设备的安全?
网络·物联网·架构·区块链
灰鲸广告联盟2 小时前
APP广告变现数据分析:关键指标与优化策略
大数据·网络·数据分析
多多*3 小时前
程序设计工作室1月21日内部训练赛
java·开发语言·网络·jvm·tcp/ip