進階
Getaddr & Addr Messages
了解 P2P 協議中的地址發現機制,節點如何請求和分享對等節點地址。
10 分鐘
Getaddr 和 Addr 消息是比特幣 P2P 網路中的地址發現機制。 節點通過這些消息交換已知的對等節點地址,維護去中心化的網路拓撲。
地址發現概述
地址發現流程:
1. 新節點啟動
- 從 DNS seeds 獲取初始地址
- 連接到已知節點
2. 請求更多地址
┌─────────┐ getaddr ┌─────────┐
│ Node A │ ────────────→ │ Node B │
│ │ addr │ │
│ │ ←──────────── │ │
└─────────┘ └─────────┘
3. 存儲地址
- 保存到 AddrMan
- 用於未來連接
4. 地址傳播
- 節點分享新發現的地址
- 網路自我維護
目的:
- 發現新節點
- 維護網路連通性
- 去中心化的對等發現 Getaddr 消息
Getaddr 消息格式:
┌─────────────────────────────────────────┐
│ (empty payload - 0 bytes) │
└─────────────────────────────────────────┘
完整 P2P 消息:
┌──────────────────────────────────────────────┐
│ magic (4 bytes) │
│ command: "getaddr" (12 bytes, padded) │
│ length: 0 (4 bytes) │
│ checksum: 0x5df6e0e2 (4 bytes) │
│ payload: (empty) │
└──────────────────────────────────────────────┘
發送時機:
1. 連接建立後(版本握手完成)
2. 需要更多對等節點時
3. AddrMan 地址不足時
限制:
- 每個連接只回應一次
- 防止地址抓取攻擊
- 隨機選擇返回的地址 Addr 消息(v1)
Addr 消息格式:
┌─────────────────────────────────────────┐
│ count (varint) │
│ addr_list: │
│ ┌─────────────────────────────────┐ │
│ │ time (4 bytes, uint32) │ │
│ │ services (8 bytes, uint64) │ │
│ │ IP address (16 bytes) │ │
│ │ port (2 bytes, big-endian) │ │
│ └─────────────────────────────────┘ │
│ × count │
└─────────────────────────────────────────┘
欄位說明:
time:
- 最後一次看到該節點的時間戳
- Unix epoch seconds
services:
- 節點提供的服務
- NODE_NETWORK (1), NODE_BLOOM (4), etc.
IP address:
- IPv6 格式(16 bytes)
- IPv4 映射為 ::ffff:x.x.x.x
port:
- 網路字節序(big-endian)
- 主網默認 8333
限制:
- 最多 1000 個地址
- 超過則分多個消息 Addrv2 消息
Addrv2 (BIP-155) 改進:
支持更多網路類型:
- IPv4 (4 bytes)
- IPv6 (16 bytes)
- Tor v2 (10 bytes) - 已棄用
- Tor v3 (32 bytes)
- I2P (32 bytes)
- CJDNS (16 bytes)
Addrv2 格式:
┌─────────────────────────────────────────┐
│ count (varint) │
│ addr_list: │
│ ┌─────────────────────────────────┐ │
│ │ time (4 bytes, uint32) │ │
│ │ services (compact size) │ │
│ │ network_id (1 byte) │ │
│ │ addr_len (varint) │ │
│ │ addr (variable) │ │
│ │ port (2 bytes, big-endian) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
network_id:
0x01 = IPv4
0x02 = IPv6
0x03 = Tor v2 (deprecated)
0x04 = Tor v3
0x05 = I2P
0x06 = CJDNS
協商:
- 發送 sendaddrv2 消息表示支持
- 必須在 verack 之前發送 地址中繼
地址自動中繼:
接收新地址後:
1. 驗證地址格式
2. 更新 AddrMan
3. 選擇性轉發給其他節點
轉發規則:
- 不轉發太舊的地址(>10分鐘)
- 隨機選擇 1-2 個節點轉發
- 限制轉發頻率
自我廣播:
- 節點定期廣播自己的地址
- 通過 addr 消息發送
- 幫助網路發現自己
限制:
- 每個節點每 24 小時只接受一個 getaddr
- 防止地址枚舉攻擊
- 隨機延遲轉發
Python 偽代碼:
def on_addr(peer, addresses):
for addr in addresses:
if addr.time > now() - 10 * 60: # 10 分鐘內
addrman.add(addr)
# 隨機選擇 1-2 個節點轉發
for relay_peer in random_peers(1, 2):
relay_peer.send_addr([addr]) AddrMan 整合
地址如何存儲到 AddrMan:
AddrMan 結構:
- new 表: 新發現的地址
- tried 表: 已嘗試連接的地址
接收 addr 後:
1. 驗證地址
- 格式正確
- 不是私有地址
- 時間戳合理
2. 計算桶位置
- 基於來源和地址
- 確保分散存儲
3. 插入到 new 表
- 如果桶已滿,可能替換舊條目
- 隨機選擇替換
4. 後續使用
- 需要連接時從 tried 表選擇
- 或從 new 表嘗試新地址
代碼流程:
addr_received → validate() → addrman.Add() → bucket_insert() 隱私考量
地址消息的隱私問題:
1. 拓撲洩露
問題: 通過地址可以推斷網路結構
緩解:
- 隨機選擇返回的地址
- 限制 getaddr 回應次數
- 添加隨機延遲
2. 時間戳攻擊
問題: 時間戳可能洩露節點活動
緩解:
- 更新時間戳有最小間隔
- 不精確的時間戳
3. 地址枚舉
問題: 攻擊者可能枚舉所有節點
緩解:
- 每個連接只回應一次 getaddr
- 隨機子集
4. Sybil 攻擊
問題: 注入假地址佔據 AddrMan
緩解:
- 桶分散算法
- 來源多樣性檢查
// 地址中繼設計平衡了發現性和隱私 調試與監控
# 監控地址相關活動
# 查看已知地址數量
bitcoin-cli getnodeaddresses 0 | jq 'length'
# 獲取隨機地址
bitcoin-cli getnodeaddresses 10
[
{
"time": 1704067200,
"services": 1033,
"address": "1.2.3.4",
"port": 8333,
"network": "ipv4"
},
...
]
# 查看節點連接
bitcoin-cli getpeerinfo | jq '.[] | {
addr: .addr,
services: .services,
addr_relay: .addr_relay_enabled
}'
# 日誌設置
# bitcoin.conf
debug=addrman
debug=net
# 日誌輸出:
# "Added 23 addresses from peer=5"
# "Sending getaddr to peer=3"
# "Received addr: 15 addresses" 實現細節
Bitcoin Core 中的實現:
// 處理 getaddr
void PeerManager::ProcessGetAddr(CNode& node) {
// 每個連接只回應一次
if (node.fSentAddr) {
return;
}
node.fSentAddr = true;
// 從 AddrMan 獲取隨機地址
std::vector<CAddress> vAddr = addrman.GetAddr(
MAX_ADDR_TO_SEND, // 1000
MAX_PCT_ADDR_TO_SEND, // 23%
node.GetNetwork()
);
// 發送 addr 或 addrv2
if (node.m_wants_addrv2) {
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::ADDRV2, vAddr));
} else {
connman.PushMessage(&node,
CNetMsgMaker(node.GetCommonVersion())
.Make(NetMsgType::ADDR, vAddr));
}
}
// 處理收到的 addr
void PeerManager::ProcessAddr(CNode& node, std::vector<CAddress>& vAddr) {
// 驗證數量
if (vAddr.size() > MAX_ADDR_TO_SEND) {
Misbehaving(node.GetId(), 20);
return;
}
// 添加到 AddrMan
std::vector<CAddress> vAddrOk;
for (auto& addr : vAddr) {
if (addr.IsValid() && !addr.IsLocal()) {
vAddrOk.push_back(addr);
}
}
addrman.Add(vAddrOk, node.addr);
} 相關概念
- Node Discovery:節點發現機制
- AddrMan:地址管理器
- P2P Protocol:點對點協議
- Tor Integration:Tor 網路整合
- I2P Integration:I2P 網路整合
已複製連結