use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tracing::debug;
use super::message::{DhtMessage, DhtMessageBuilder};
use super::node::DhtNode;
use super::routing_table::RoutingTable;
use super::socket::DhtSocket;
use super::transaction::TransactionManager;
use crate::bittorrent::bencode::codec::BencodeValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompactNode {
V4(std::net::Ipv4Addr, u16),
V6(std::net::Ipv6Addr, u16),
}
impl CompactNode {
pub fn to_socket_addr(&self) -> SocketAddr {
match self {
CompactNode::V4(ip, port) => SocketAddr::from((*ip, *port)),
CompactNode::V6(ip, port) => {
SocketAddr::V6(std::net::SocketAddrV6::new(*ip, *port, 0, 0))
}
}
}
}
pub fn parse_compact_nodes(data: &[u8], start: usize, count: usize) -> Vec<CompactNode> {
let mut nodes = Vec::new();
let mut offset = start;
for _ in 0..count {
if offset >= data.len() {
break;
}
let node_type = data[offset];
offset += 1;
match node_type {
0x04 => {
if offset + 6 > data.len() {
break;
}
let ip = std::net::Ipv4Addr::new(
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
);
let port = u16::from_be_bytes([data[offset + 4], data[offset + 5]]);
nodes.push(CompactNode::V4(ip, port));
offset += 6;
}
0x06 => {
if offset + 18 > data.len() {
break;
}
let ip = std::net::Ipv6Addr::from([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
data[offset + 8],
data[offset + 9],
data[offset + 10],
data[offset + 11],
data[offset + 12],
data[offset + 13],
data[offset + 14],
data[offset + 15],
]);
let port = u16::from_be_bytes([data[offset + 16], data[offset + 17]]);
nodes.push(CompactNode::V6(ip, port));
offset += 18;
}
_ => {
debug!("Unknown compact node type byte: 0x{:02X}", node_type);
break;
}
}
}
nodes
}
pub struct DhtClientConfig {
pub self_id: [u8; 20],
pub bootstrap_nodes: Vec<SocketAddr>,
pub max_concurrent_queries: usize,
pub query_timeout: Duration,
pub max_rounds: usize,
}
impl Default for DhtClientConfig {
fn default() -> Self {
Self {
self_id: [0u8; 20],
bootstrap_nodes: vec![],
max_concurrent_queries: 8,
query_timeout: Duration::from_secs(5),
max_rounds: 3,
}
}
}
pub struct DiscoveredPeers {
pub addresses: Vec<SocketAddr>,
pub nodes_contacted: usize,
}
#[derive(Debug)]
pub enum GetPeersResult {
Peers(Vec<SocketAddr>),
Nodes(Vec<DhtNode>),
}
struct NodeCacheEntry {
peers: Vec<SocketAddr>,
timestamp: Instant,
nodes_contacted: usize,
}
const NODE_CACHE_TTL_SECS: u64 = 300;
pub struct DhtClient {
config: DhtClientConfig,
routing_table: RoutingTable,
#[allow(dead_code)] tx_manager: TransactionManager,
node_cache: HashMap<[u8; 20], NodeCacheEntry>,
socket: Option<DhtSocket>,
}
impl DhtClient {
pub fn new(config: DhtClientConfig) -> Self {
let mut routing_table = RoutingTable::new(config.self_id);
for addr in &config.bootstrap_nodes {
let node = DhtNode::new([0u8; 20], *addr);
routing_table.insert(node);
}
Self {
config,
routing_table,
tx_manager: TransactionManager::new(),
node_cache: HashMap::new(),
socket: None,
}
}
pub async fn initialize(&mut self) -> Result<(), String> {
if self.socket.is_none() {
self.socket = Some(DhtSocket::bind(0).await?);
}
Ok(())
}
pub async fn send_ping(&mut self, node: &DhtNode) -> Result<bool, String> {
let socket = self.get_socket().await?;
let tx_id = rand::random::<u32>();
let msg = DhtMessageBuilder::ping(tx_id, &self.config.self_id);
let encoded = msg.encode()?;
socket.send_to(node.addr, &encoded).await?;
let mut buf = [0u8; 4096];
match socket
.recv_with_timeout(&mut buf, self.config.query_timeout)
.await
{
Ok((len, _)) => {
if len == 0 {
return Ok(false);
}
match DhtMessage::decode(&buf[..len]) {
Ok(response) => {
if response.is_response() {
self.routing_table.mark_good(&node.id);
Ok(true)
} else {
self.routing_table.mark_bad(&node.id);
Ok(false)
}
}
Err(_) => Ok(false),
}
}
Err(_) => {
self.routing_table.mark_bad(&node.id);
Ok(false)
}
}
}
pub async fn send_find_node(&mut self, target: &[u8; 20]) -> Result<Vec<DhtNode>, String> {
let socket = self.get_socket().await?;
let mut all_nodes = Vec::new();
let nodes_to_query: Vec<DhtNode> = self
.routing_table
.find_closest(target, self.config.max_concurrent_queries)
.into_iter()
.cloned()
.collect();
for node in nodes_to_query {
let tx_id = rand::random::<u32>();
let msg = DhtMessageBuilder::find_node(tx_id, &self.config.self_id, target);
let encoded = msg.encode()?;
socket.send_to(node.addr, &encoded).await?;
let mut buf = [0u8; 4096];
if let Ok((len, _)) = socket
.recv_with_timeout(&mut buf, self.config.query_timeout)
.await
&& len > 0
&& let Ok(response) = DhtMessage::decode(&buf[..len])
{
let new_nodes = extract_compact_nodes_from_response(&response);
for (addr, nid) in new_nodes {
let new_node = DhtNode::new(nid, addr);
self.routing_table.insert(new_node.clone());
all_nodes.push(new_node);
}
}
}
Ok(all_nodes)
}
pub async fn send_get_peers(&mut self, info_hash: &[u8; 20]) -> Result<GetPeersResult, String> {
let socket = self.get_socket().await?;
let mut all_peers = Vec::new();
let mut all_nodes = Vec::new();
let nodes_to_query: Vec<DhtNode> = self
.routing_table
.find_closest(info_hash, self.config.max_concurrent_queries)
.into_iter()
.cloned()
.collect();
for node in nodes_to_query {
let tx_id = rand::random::<u32>();
let msg = DhtMessageBuilder::get_peers(tx_id, &self.config.self_id, info_hash);
let encoded = msg.encode()?;
socket.send_to(node.addr, &encoded).await?;
let mut buf = [0u8; 4096];
if let Ok((len, _)) = socket
.recv_with_timeout(&mut buf, self.config.query_timeout)
.await
&& len > 0
&& let Ok(response) = DhtMessage::decode(&buf[..len])
{
let peers = extract_compact_peers_from_response(&response);
all_peers.extend(peers);
let nodes = extract_compact_nodes_from_response(&response);
for (addr, nid) in nodes {
let new_node = DhtNode::new(nid, addr);
self.routing_table.insert(new_node.clone());
all_nodes.push(new_node);
}
}
}
if !all_peers.is_empty() {
Ok(GetPeersResult::Peers(all_peers))
} else {
Ok(GetPeersResult::Nodes(all_nodes))
}
}
pub async fn send_announce_peer(
&mut self,
info_hash: &[u8; 20],
port: u16,
) -> Result<(), String> {
let socket = self.get_socket().await?;
let nodes_to_query: Vec<DhtNode> = self
.routing_table
.find_closest(info_hash, 8)
.into_iter()
.cloned()
.collect();
for node in nodes_to_query {
let token = format!("token_{}", rand::random::<u32>());
let tx_id = rand::random::<u32>();
let msg = DhtMessageBuilder::announce_peer(
tx_id,
&self.config.self_id,
info_hash,
port,
&token,
);
let encoded = msg.encode()?;
socket.send_to(node.addr, &encoded).await?;
}
Ok(())
}
async fn get_socket(&mut self) -> Result<DhtSocket, String> {
if self.socket.is_none() {
self.socket = Some(DhtSocket::bind(0).await?);
}
Ok(self.socket.as_ref().unwrap().clone())
}
pub async fn discover_peers(
&mut self,
target_info_hash: &[u8; 20],
) -> Result<DiscoveredPeers, String> {
if let Some(entry) = self.node_cache.get(target_info_hash) {
if entry.timestamp.elapsed().as_secs() < NODE_CACHE_TTL_SECS {
debug!(
"DHT: returning cached peers for info_hash ({} peers, age={}s)",
entry.peers.len(),
entry.timestamp.elapsed().as_secs()
);
return Ok(DiscoveredPeers {
addresses: entry.peers.clone(),
nodes_contacted: entry.nodes_contacted,
});
} else {
self.node_cache.remove(target_info_hash);
}
}
let socket = DhtSocket::bind(0).await?;
let mut all_peers: Vec<SocketAddr> = Vec::new();
let mut nodes_contacted = 0usize;
for round in 0..self.config.max_rounds {
debug!(
"DHT discovery round {}/{}",
round + 1,
self.config.max_rounds
);
if !all_peers.is_empty() {
break;
}
let closest_nodes = self
.routing_table
.find_closest(target_info_hash, self.config.max_concurrent_queries);
let nodes_to_query: Vec<SocketAddr> = if !closest_nodes.is_empty() {
closest_nodes.iter().map(|n| n.addr).collect()
} else if round == 0 {
self.config.bootstrap_nodes.clone()
} else {
vec![]
};
if nodes_to_query.is_empty() {
break;
}
let mut handles = Vec::new();
for (i, node_addr) in nodes_to_query.iter().enumerate() {
if i >= self.config.max_concurrent_queries {
break;
}
if *node_addr == SocketAddr::from(([0, 0, 0, 0], 0)) {
continue;
}
let tx_id = (round * 1000 + i) as u32;
let query_msg =
DhtMessageBuilder::get_peers(tx_id, &self.config.self_id, target_info_hash);
let addr = *node_addr;
let query_timeout = self.config.query_timeout;
let sock = socket.clone();
handles.push(tokio::spawn(async move {
Self::query_node_with_socket(&sock, addr, &query_msg, query_timeout).await
}));
}
for handle in handles {
match handle.await {
Ok(Ok(response)) => {
nodes_contacted += 1;
let peers = extract_compact_peers_from_response(&response);
all_peers.extend(peers);
let new_nodes = extract_compact_nodes_from_response(&response);
for (naddr, nid) in new_nodes {
let node = DhtNode::new(nid, naddr);
self.routing_table.insert(node);
}
}
Ok(Err(e)) => {
debug!("DHT query error: {}", e);
}
Err(join_err) => {
debug!("DHT query task panicked: {}", join_err);
}
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
all_peers.sort();
all_peers.dedup();
self.node_cache.insert(
*target_info_hash,
NodeCacheEntry {
peers: all_peers.clone(),
timestamp: Instant::now(),
nodes_contacted,
},
);
Ok(DiscoveredPeers {
addresses: all_peers,
nodes_contacted,
})
}
async fn query_node_with_socket(
socket: &DhtSocket,
node_addr: SocketAddr,
message: &DhtMessage,
query_timeout: Duration,
) -> Result<DhtMessage, String> {
let encoded = message.encode()?;
socket.send_to(node_addr, &encoded).await?;
let mut buf = [0u8; 4096];
match socket.recv_with_timeout(&mut buf, query_timeout).await {
Ok((len, _from)) => {
if len == 0 {
return Err("Empty response".to_string());
}
DhtMessage::decode(&buf[..len])
}
Err(e) => Err(e),
}
}
pub fn routing_table(&self) -> &RoutingTable {
&self.routing_table
}
}
pub fn extract_compact_peers_from_response(response: &DhtMessage) -> Vec<SocketAddr> {
let r = match &response.r {
Some(r) => r,
None => return vec![],
};
let values = match r.dict_get(b"values") {
Some(BencodeValue::List(list)) => list,
_ => return vec![],
};
let mut peers = Vec::new();
for item in values {
if let BencodeValue::Bytes(bytes) = item {
if bytes.len() == 6 {
let ip_bytes: [u8; 4] = [bytes[0], bytes[1], bytes[2], bytes[3]];
let port = u16::from_be_bytes([bytes[4], bytes[5]]);
peers.push(SocketAddr::from((std::net::Ipv4Addr::from(ip_bytes), port)));
} else if bytes.len() == 18 {
let octets: [u8; 16] = match bytes[..16].try_into() {
Ok(o) => o,
Err(_) => continue,
};
let port = u16::from_be_bytes([bytes[16], bytes[17]]);
peers.push(SocketAddr::V6(std::net::SocketAddrV6::new(
std::net::Ipv6Addr::from(octets),
port,
0,
0,
)));
}
}
}
peers
}
pub fn extract_compact_nodes_from_response(response: &DhtMessage) -> Vec<(SocketAddr, [u8; 20])> {
let r = match &response.r {
Some(r) => r,
None => return vec![],
};
let nodes_data = match r.dict_get(b"nodes") {
Some(BencodeValue::Bytes(data)) => data,
_ => return vec![],
};
let mut nodes = Vec::new();
if nodes_data.is_empty() {
return nodes;
}
if nodes_data.len() % 38 == 0 && nodes_data.len() >= 38 {
for chunk in nodes_data.chunks(38) {
if chunk.len() < 38 {
continue;
}
let mut node_id = [0u8; 20];
node_id.copy_from_slice(&chunk[0..20]);
let octets: [u8; 16] = match chunk[20..36].try_into() {
Ok(o) => o,
Err(_) => continue,
};
let port = u16::from_be_bytes([chunk[36], chunk[37]]);
nodes.push((
SocketAddr::V6(std::net::SocketAddrV6::new(
std::net::Ipv6Addr::from(octets),
port,
0,
0,
)),
node_id,
));
}
} else {
let chunk_size = 26;
for chunk in nodes_data.chunks(chunk_size) {
if chunk.len() < chunk_size {
continue;
}
let mut node_id = [0u8; 20];
node_id.copy_from_slice(&chunk[0..20]);
let ip_bytes: [u8; 4] = [chunk[20], chunk[21], chunk[22], chunk[23]];
let port = u16::from_be_bytes([chunk[24], chunk[25]]);
nodes.push((
SocketAddr::from((std::net::Ipv4Addr::from(ip_bytes), port)),
node_id,
));
}
}
nodes
}
pub fn generate_random_node_id() -> [u8; 20] {
use rand::RngCore;
let mut id = [0u8; 20];
rand::thread_rng().fill_bytes(&mut id);
id
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_compact_peers_empty() {
let msg = DhtMessage::new_response(
vec![1, 2],
crate::bittorrent::bencode::codec::BencodeValue::Dict(
std::collections::BTreeMap::new()
.into_iter()
.map(|(k, v): (&Vec<u8>, &Vec<u8>)| {
(
k.to_vec(),
crate::bittorrent::bencode::codec::BencodeValue::Bytes(v.clone()),
)
})
.collect::<std::collections::BTreeMap<Vec<u8>, _>>(),
),
);
let result = extract_compact_peers_from_response(&msg);
assert!(result.is_empty());
}
#[test]
fn test_extract_compact_peers_single() {
use crate::bittorrent::bencode::codec::BencodeValue;
use std::collections::BTreeMap;
let peer_bytes: Vec<u8> = vec![
192, 168, 1, 1, 0x1F, 0x90, ];
let mut r_dict = BTreeMap::new();
r_dict.insert(
b"values".to_vec(),
BencodeValue::List(vec![BencodeValue::Bytes(peer_bytes)]),
);
let msg = DhtMessage::new_response(vec![1, 2], BencodeValue::Dict(r_dict));
let peers = extract_compact_peers_from_response(&msg);
assert_eq!(peers.len(), 1);
assert_eq!(
peers[0].ip(),
std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 1))
);
assert_eq!(peers[0].port(), 8080);
}
#[test]
fn test_extract_compact_peers_multiple() {
use crate::bittorrent::bencode::codec::BencodeValue;
use std::collections::BTreeMap;
let peer1: Vec<u8> = vec![10, 0, 0, 1, 0x00, 0x50];
let peer2: Vec<u8> = vec![172, 16, 5, 1, 0x17, 0x70];
let mut r_dict = BTreeMap::new();
r_dict.insert(
b"values".to_vec(),
BencodeValue::List(vec![BencodeValue::Bytes(peer1), BencodeValue::Bytes(peer2)]),
);
let msg = DhtMessage::new_response(vec![1, 2], BencodeValue::Dict(r_dict));
let peers = extract_compact_peers_from_response(&msg);
assert_eq!(peers.len(), 2);
assert_eq!(peers[0].port(), 80);
assert_eq!(peers[1].port(), 6000);
}
#[test]
fn test_extract_compact_nodes_single() {
use crate::bittorrent::bencode::codec::BencodeValue;
use std::collections::BTreeMap;
let mut node_id = [0u8; 20];
node_id[0] = 0xAB;
let mut node_bytes = Vec::with_capacity(26);
node_bytes.extend_from_slice(&node_id);
node_bytes.extend_from_slice(&[10, 0, 0, 2]);
node_bytes.extend_from_slice(&[0x13, 0x88]);
let mut r_dict = BTreeMap::new();
r_dict.insert(b"nodes".to_vec(), BencodeValue::Bytes(node_bytes));
let msg = DhtMessage::new_response(vec![1, 2], BencodeValue::Dict(r_dict));
let nodes = extract_compact_nodes_from_response(&msg);
assert_eq!(nodes.len(), 1);
let (addr, nid) = &nodes[0];
assert_eq!(
addr.ip(),
std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 2))
);
assert_eq!(addr.port(), 5000);
assert_eq!(nid[0], 0xAB);
}
#[test]
fn test_extract_compact_nodes_multiple() {
use crate::bittorrent::bencode::codec::BencodeValue;
use std::collections::BTreeMap;
let mut n1 = vec![0xAA; 20];
n1.extend_from_slice(&[192, 168, 1, 100, 0x1F, 0x40]);
let mut n2 = vec![0xBB; 20];
n2.extend_from_slice(&[172, 16, 0, 1, 0x07, 0xD0]);
let combined: Vec<u8> = [n1.as_slice(), n2.as_slice()].concat();
let mut r_dict = BTreeMap::new();
r_dict.insert(b"nodes".to_vec(), BencodeValue::Bytes(combined));
let msg = DhtMessage::new_response(vec![1, 2], BencodeValue::Dict(r_dict));
let nodes = extract_compact_nodes_from_response(&msg);
assert_eq!(nodes.len(), 2);
assert_eq!(nodes[0].0.port(), 8000);
assert_eq!(nodes[1].0.port(), 2000);
}
#[test]
fn test_generate_random_node_id_nonzero() {
let id = generate_random_node_id();
assert!(
!id.iter().all(|&b| b == 0),
"Random ID should not be all zeros"
);
}
#[test]
fn test_dht_client_new_with_bootstrap() {
let config = DhtClientConfig {
self_id: generate_random_node_id(),
bootstrap_nodes: vec![
"127.0.0.1:6881".parse().unwrap(),
"127.0.0.1:6882".parse().unwrap(),
],
..Default::default()
};
let client = DhtClient::new(config);
let rt = client.routing_table();
assert!(
rt.total_node_count() >= 1,
"bootstrap nodes share zero ID so only one is stored"
);
}
#[test]
fn test_discover_peers_no_bootstrap_returns_empty() {
let config = DhtClientConfig {
self_id: generate_random_node_id(),
bootstrap_nodes: vec![],
max_concurrent_queries: 1,
query_timeout: Duration::from_millis(50),
max_rounds: 1,
};
let mut client = DhtClient::new(config);
let target = [0u8; 20];
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(client.discover_peers(&target)).unwrap();
assert!(result.addresses.is_empty());
}
#[test]
fn test_parse_compact_nodes_ipv4() {
let mut data = Vec::new();
data.push(0x04); data.extend_from_slice(&[192, 168, 1, 1]); data.extend_from_slice(&[0x1F, 0x90]);
data.push(0x04); data.extend_from_slice(&[10, 0, 0, 2]); data.extend_from_slice(&[0x1A, 0xE1]);
let nodes = parse_compact_nodes(&data, 0, 10);
assert_eq!(nodes.len(), 2);
match &nodes[0] {
CompactNode::V4(ip, port) => {
assert_eq!(*ip, std::net::Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(*port, 8080);
}
_ => panic!("Expected IPv4 node"),
}
match &nodes[1] {
CompactNode::V4(ip, port) => {
assert_eq!(*ip, std::net::Ipv4Addr::new(10, 0, 0, 2));
assert_eq!(*port, 6881);
}
_ => panic!("Expected IPv4 node"),
}
}
#[test]
fn test_parse_compact_nodes_ipv6() {
let mut data = Vec::new();
data.push(0x06); data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); data.extend_from_slice(&[0x1F, 0x90]);
data.push(0x06); data.extend_from_slice(&[0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); data.extend_from_slice(&[0x1A, 0xE1]);
let nodes = parse_compact_nodes(&data, 0, 10);
assert_eq!(nodes.len(), 2);
match &nodes[0] {
CompactNode::V6(ip, port) => {
assert_eq!(*ip, std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
assert_eq!(*port, 8080);
}
_ => panic!("Expected IPv6 node"),
}
match &nodes[1] {
CompactNode::V6(ip, port) => {
assert_eq!(*ip, std::net::Ipv6Addr::new(0xFE80, 0, 0, 0, 0, 0, 0, 1));
assert_eq!(*port, 6881);
}
_ => panic!("Expected IPv6 node"),
}
}
#[test]
fn test_parse_mixed_nodes() {
let mut data = Vec::new();
data.push(0x04);
data.extend_from_slice(&[172, 16, 0, 1]);
data.extend_from_slice(&[0x1F, 0x90]);
data.push(0x06);
data.extend_from_slice(&[0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
data.extend_from_slice(&[0x1A, 0xE1]);
data.push(0x04);
data.extend_from_slice(&[10, 0, 0, 5]);
data.extend_from_slice(&[0x00, 0x50]);
let nodes = parse_compact_nodes(&data, 0, 10);
assert_eq!(nodes.len(), 3);
assert!(matches!(nodes[0], CompactNode::V4(..)));
assert!(matches!(nodes[1], CompactNode::V6(..)));
assert!(matches!(nodes[2], CompactNode::V4(..)));
}
#[test]
fn test_parse_compact_nodes_unknown_type_stops() {
let mut data = Vec::new();
data.push(0x04);
data.extend_from_slice(&[192, 168, 1, 1]);
data.extend_from_slice(&[0x1F, 0x90]);
data.push(0xFF);
data.push(0x04);
data.extend_from_slice(&[10, 0, 0, 1]);
data.extend_from_slice(&[0x00, 0x50]);
let nodes = parse_compact_nodes(&data, 0, 10);
assert_eq!(nodes.len(), 1); }
#[test]
fn test_parse_compact_nodes_truncated_data() {
let mut data = Vec::new();
data.push(0x06);
data.extend_from_slice(&[0, 0, 0, 0]);
let nodes = parse_compact_nodes(&data, 0, 10);
assert!(nodes.is_empty()); }
#[test]
fn test_compact_node_to_socket_addr_v4() {
let node = CompactNode::V4(std::net::Ipv4Addr::new(192, 168, 1, 100), 8080);
let addr = node.to_socket_addr();
assert_eq!(
addr.ip(),
std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 100))
);
assert_eq!(addr.port(), 8080);
}
#[test]
fn test_compact_node_to_socket_addr_v6() {
let node = CompactNode::V6(
std::net::Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 1),
443,
);
let addr = node.to_socket_addr();
assert!(matches!(addr.ip(), std::net::IpAddr::V6(_)));
assert_eq!(addr.port(), 443);
}
}