You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

已知infohash与节点时BitTorrent握手空响应问题排查求助

BitTorrent搜索引擎构建问题排查

我正在构建BitTorrent搜索引擎,监听DHT网络的get_peers消息时成功抓取到infohash,接着向对应DHT节点请求peer列表。为验证infohash有效性,我尝试向peer发送BitTorrent握手并对比响应,但除了连接拒绝错误外,多数peer返回空响应,想排查操作是否有误,以下是相关代码:

握手函数

import socket 

def handshake(infohash, peer):
    peer_id = b"-TR2940-k8hj0wgej6ch"

    handshake = b'\x13'
    handshake += b'BitTorrent protocol'
    handshake += b'\x00\x00\x00\x00\x00\x10\x00\x00'
    handshake += infohash
    handshake += peer_id

    try:
        ClientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        ClientSocket.settimeout(3)
        ClientSocket.connect(peer)
        print("Connected to peer.")
        ClientSocket.sendall(handshake)
        response = ClientSocket.recv(68)

        if not response:
            print("Empty response.")
            return
        print(f"Handshake completed, resp: {response}")

        ClientSocket.close()        
    except Exception as e:
        print(e)

从指定infohash和DHT节点获取peer的工具函数

import random
import uuid
import bencode
import socket
from struct import unpack
import handshake

def newTID(tidlen):
    tid = ""
    for i in range(0, tidlen):
        tid += chr(random.randint(97, 122))
    return tid

def newID():
  return uuid.uuid4().hex[0:20]

def split_nodes(nodes):
    length = len(nodes)
    if (length % 26) != 0:
        return
    for i in range(0, length, 26):
        nid = nodes[i:i+20]
        ip = socket.inet_ntoa(nodes[i+20:i+24])
        port = unpack("!H", nodes[i+24:i+26])[0]
        yield nid, ip, port

UDPClientSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
UDPClientSocket.settimeout(4)

def get_peers_from_infohash(infohash, node):
    get_peers_query = {"t":"aa", "y":"q", "q":"get_peers", "a": {"id":newID(), "info_hash":infohash}}
    get_peers_query = bencode.encode(get_peers_query)
    
    UDPClientSocket.sendto(get_peers_query, node)

    received = UDPClientSocket.recvfrom(65536)
    msg = received[0]
    decoded = bencode.decode(msg)

    peers = split_nodes(decoded["r"]["nodes"])
    for nid, ip, port in peers:
        print(infohash, infohash.hex(),  ip, port)
        handshake.handshake(infohash, (ip, port))

DHT爬虫代码

import bencode
import socket
import uuid
from struct import unpack
import threading
import random
import dhtutils

def newTID(tidlen):
    tid = ""
    for i in range(0, tidlen):
        tid += chr(random.randint(97, 122))
    return tid

def newID():
  return uuid.uuid4().hex[0:20]

def handle_message(msg, node):
    if msg.get("e"):
        # print(msg.get("e"))
        pass
    elif msg.get("y") == "r":
       handle_response(msg, node)
    elif msg.get("y") == "q":
        handle_query(msg, node)

def handle_query(msg, node):
    try:
        if msg["q"] == "get_peers":
            infohash = msg["a"]["info_hash"]
            # print(infohash.hex(), msg, node)
            print(infohash.hex())
            dhtutils.get_peers_from_infohash(infohash, node)
    except:
        pass

def handle_response(msg, node):
    global all_nodes
    if msg.get("r").get("nodes"):
        # response from find_nodes
        nodes = msg.get("r").get("nodes")
        if nodes:
            nodes = split_nodes(nodes)
            for id, ip, port in nodes:
                find_nodes(id, (ip, port))
                all_nodes.append((id, (ip, port)))
    elif msg.get("t") == "pg":
        # response from ping
        id = msg["r"]["id"]
        all_nodes.append((id, node))

def split_nodes(nodes):
    length = len(nodes)
    if (length % 26) != 0:
        return
    for i in range(0, length, 26):
        nid = nodes[i:i+20]
        ip = socket.inet_ntoa(nodes[i+20:i+24])
        port = unpack("!H", nodes[i+24:i+26])[0]
        yield nid, ip, port

def find_nodes(id, node):
    global UDPClientSocket
    find_node_query = {"t":newTID(2), "y":"q", "q":"find_node", "a": {"id":newID(), "target":id}}
    find_node_query = bencode.encode(find_node_query)
    UDPClientSocket.sendto(find_node_query, node)

def ping(node):
    global UDPClientSocket
    ping_query = {"t":"pg", "y":"q", "q":"ping", "a":{"id":newID()}}
    ping_query = bencode.encode(ping_query)
    UDPClientSocket.sendto(ping_query, node)

def listen():
    while True:
        try:
            received = UDPClientSocket.recvfrom(65536)
            msg = received[0]
            src = received[1]
            decoded = bencode.decode(msg)
            handle_message(decoded, src)
        except Exception as e:
            pass

UDPClientSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

T = threading.Thread(target=listen)
T.start()

nodes = [
    ("router.bittorrent.com", 6881),
    ("dht.transmissionbt.com", 6881),
    ("router.utorrent.com", 6881)
]

for node in nodes:
    ping(node)

all_nodes = []

while True:
    if len(all_nodes) > 0:
       for node in all_nodes:
           find_nodes(node[0], node[1])

问题排查与修复建议

1. 错误处理get_peers响应的核心字段

当前代码只处理了get_peers响应中的nodes字段,但该字段返回的是DHT路由节点,并非实际拥有种子的下载peer。真正的下载peer列表在values字段中,每个peer以6字节格式存储(4字节IP + 2字节端口)。修改get_peers_from_infohash函数:

def get_peers_from_infohash(infohash, node):
    get_peers_query = {"t":"aa", "y":"q", "q":"get_peers", "a": {"id":newID(), "info_hash":infohash}}
    get_peers_query = bencode.encode(get_peers_query)
    
    UDPClientSocket.sendto(get_peers_query, node)

    received = UDPClientSocket.recvfrom(65536)
    msg = received[0]
    decoded = bencode.decode(msg)

    # 优先处理实际下载peer的values字段
    if "values" in decoded["r"]:
        for peer_bytes in decoded["r"]["values"]:
            if len(peer_bytes) != 6:
                continue
            ip = socket.inet_ntoa(peer_bytes[:4])
            port = unpack("!H", peer_bytes[4:])[0]
            print(f"Found download peer: {ip}:{port}")
            handshake.handshake(infohash, (ip, port))
    # 再处理路由节点(可选,用于扩展DHT网络)
    elif "nodes" in decoded["r"]:
        peers = split_nodes(decoded["r"]["nodes"])
        for nid, ip, port in peers:
            print(f"Found DHT node: {ip}:{port}")
            # 路由节点不需要握手,除非要继续查询
            # handshake.handshake(infohash, (ip, port))

2. 完善握手响应的接收逻辑

recv(68)可能因网络分片导致未一次性获取完整握手响应,改为循环接收直到拿到足够字节:

# 替换原handshake函数中的response接收部分
response = b""
while len(response) < 68:
    chunk = ClientSocket.recv(68 - len(response))
    if not chunk:
        print("Connection closed before full handshake.")
        return
    response += chunk

if len(response) != 68:
    print(f"Incomplete handshake response, length: {len(response)}")
    return

print(f"Handshake completed, resp: {response}")

3. 其他潜在问题

  • peer协议支持:部分peer仅支持uTP协议(基于UDP),不响应TCP握手,可尝试实现uTP握手逻辑。
  • peer_id合法性:确保peer_id符合BitTorrent规范(如开头的客户端标识-TR2940-是Transmission的标准标识,格式无误)。
  • 连接频率限制:频繁向同一IP段发起连接可能被屏蔽,添加随机延迟或限制并发连接数。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 03:50:41