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}")
关键解析步骤说明
- 修正STUN请求格式:事务ID必须包含
0x2112A442的magic cookie,这样服务器才会返回符合RFC5389标准的有效响应。 - 解析XOR-MAPPED-ADDRESS:
- 端口解密:将响应中的端口字节转成整数后,与magic cookie的高16位(
0x2112)进行XOR运算,得到真实的外部端口。 - IP解密:将响应中的IP字节转成整数后,与完整的magic cookie进行XOR运算,再转成IPv4字符串格式。
- 端口解密:将响应中的端口字节转成整数后,与magic cookie的高16位(
- 属性遍历与对齐:STUN属性的长度是4字节对齐的,遍历属性时要考虑到自动填充的字节,避免解析错位。
为什么你的原代码无法正确解析?
你的原请求没有使用magic cookie,服务器返回的MAPPED-ADDRESS属性可能并不是你的NAT外部地址(比如返回的是目标服务器的IP),而且你也没有处理属性值的XOR加密逻辑,导致无法得到正确的端口信息。
内容的提问来源于stack exchange,提问作者ibrahem




