You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Python3如何从STUN服务器获取NAT发送UDP数据包的外部端口

解决STUN响应中提取NAT外部UDP端口的问题

我来帮你搞定这个问题!首先,你的STUN请求没有遵循RFC5389的规范,这可能是导致你拿到错误响应数据的原因,另外我们需要正确解析STUN响应中的XOR-MAPPED-ADDRESS属性(这是RFC5389推荐使用的属性,比旧的MAPPED-ADDRESS更可靠)。

问题分析

你的代码生成的STUN事务ID是完全随机的16字节,但RFC5389要求事务ID的前4字节必须是magic cookie(固定值0x2112A442),后面12字节才是随机数。这会导致STUN服务器可能返回不符合预期的响应,比如你拿到的IP是目标IP而不是NAT的外部IP。

另外,你需要从响应的XOR-MAPPED-ADDRESS属性中解析端口和IP,这个属性的值是经过XOR加密的,需要用magic cookie来解密。

修正后的代码及解析逻辑

下面是修正后的代码,包含正确的STUN请求生成和响应解析:

import socket
import secrets

# STUN RFC5389 constants
STUN_MAGIC_COOKIE = 0x2112A442
STUN_BIND_REQUEST = 0x0001
STUN_BIND_RESPONSE = 0x0101
ATTR_XOR_MAPPED_ADDRESS = 0x0020

def int_to_bytes(n, length, byteorder='big'):
    return n.to_bytes(length, byteorder=byteorder)

def bytes_to_int(b, byteorder='big'):
    return int.from_bytes(b, byteorder=byteorder)

def generate_stun_transaction_id():
    # RFC5389: 4 bytes magic cookie + 12 bytes random
    magic_bytes = int_to_bytes(STUN_MAGIC_COOKIE, 4)
    random_bytes = secrets.token_bytes(12)
    return magic_bytes + random_bytes

def parse_xor_mapped_address(attr_value):
    # 解析XOR-MAPPED-ADDRESS属性
    # 格式: [保留位(1字节)] [地址家族(1字节)] [端口(2字节)] [IP(4字节, IPv4)]
    family = attr_value[1]
    if family != 1:
        raise ValueError("只支持IPv4地址")
    
    # 解密端口: 端口字节 XOR magic cookie的前2字节
    port_bytes = attr_value[2:4]
    port_xor = bytes_to_int(port_bytes)
    port = port_xor ^ (STUN_MAGIC_COOKIE >> 16)
    
    # 解密IP: IP字节 XOR magic cookie
    ip_bytes = attr_value[4:8]
    ip_int = bytes_to_int(ip_bytes)
    ip_int_decrypted = ip_int ^ STUN_MAGIC_COOKIE
    ip = socket.inet_ntoa(int_to_bytes(ip_int_decrypted, 4))
    
    return ip, port

def get_nat_external_port():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定到你示例中的9000端口,也可以换成其他端口
    sock.bind(('0.0.0.0', 9000))
    
    # 构建STUN BIND请求
    msg_type = int_to_bytes(STUN_BIND_REQUEST, 2)
    msg_length = int_to_bytes(0, 2)  # 没有额外属性,长度为0
    trans_id = generate_stun_transaction_id()
    stun_request = msg_type + msg_length + trans_id
    
    # 发送到Google的STUN服务器
    stun_server = ('stun.l.google.com', 19302)
    sock.sendto(stun_request, stun_server)
    
    # 接收响应
    recv_data, _ = sock.recvfrom(2048)
    
    # 解析STUN响应头
    msg_type = bytes_to_int(recv_data[:2])
    if msg_type != STUN_BIND_RESPONSE:
        raise ValueError("收到的不是STUN绑定响应")
    
    msg_length = bytes_to_int(recv_data[2:4])
    trans_id_response = recv_data[4:20]
    if trans_id_response != trans_id:
        raise ValueError("事务ID不匹配")
    
    # 遍历属性,找到XOR-MAPPED-ADDRESS
    offset = 20
    while offset < 20 + msg_length:
        attr_type = bytes_to_int(recv_data[offset:offset+2])
        attr_length = bytes_to_int(recv_data[offset+2:offset+4])
        attr_value = recv_data[offset+4:offset+4+attr_length]
        
        if attr_type == ATTR_XOR_MAPPED_ADDRESS:
            external_ip, external_port = parse_xor_mapped_address(attr_value)
            print(f"NAT外部IP: {external_ip}, 外部端口: {external_port}")
            return external_port
        
        # 属性长度是4字节对齐的,需要处理填充字节
        offset += 4 + ((attr_length + 3) // 4) * 4 - attr_length
    
    raise ValueError("未找到XOR-MAPPED-ADDRESS属性")

if __name__ == "__main__":
    try:
        external_port = get_nat_external_port()
    except Exception as e:
        print(f"错误: {e}")

关键解析步骤说明

  1. 修正STUN请求格式:事务ID必须包含0x2112A442的magic cookie,这样服务器才会返回符合RFC5389标准的有效响应。
  2. 解析XOR-MAPPED-ADDRESS
    • 端口解密:将响应中的端口字节转成整数后,与magic cookie的高16位(0x2112)进行XOR运算,得到真实的外部端口。
    • IP解密:将响应中的IP字节转成整数后,与完整的magic cookie进行XOR运算,再转成IPv4字符串格式。
  3. 属性遍历与对齐:STUN属性的长度是4字节对齐的,遍历属性时要考虑到自动填充的字节,避免解析错位。

为什么你的原代码无法正确解析?

你的原请求没有使用magic cookie,服务器返回的MAPPED-ADDRESS属性可能并不是你的NAT外部地址(比如返回的是目标服务器的IP),而且你也没有处理属性值的XOR加密逻辑,导致无法得到正确的端口信息。

内容的提问来源于stack exchange,提问作者ibrahem

火山引擎 最新活动