From 98a939121052fc9207f0c39d49a41788210df476 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Wed, 7 Sep 2022 08:51:28 -0600 Subject: [PATCH] bittorrent-dht: decode node data structures Instead of decoding the nodes field into a blog of bytes, decode it into an array of node info objects, each with a node id, IP address and port. --- rust/src/bittorrent_dht/logger.rs | 39 ++++++++++++++++++- rust/src/bittorrent_dht/parser.rs | 62 +++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/rust/src/bittorrent_dht/logger.rs b/rust/src/bittorrent_dht/logger.rs index 51c0418af0..50a1a26087 100644 --- a/rust/src/bittorrent_dht/logger.rs +++ b/rust/src/bittorrent_dht/logger.rs @@ -18,6 +18,33 @@ use super::bittorrent_dht::BitTorrentDHTTransaction; use crate::jsonbuilder::{JsonBuilder, JsonError}; +/// Format bytes as an IP address string. +fn print_ip_addr(addr: &[u8]) -> std::string::String { + if addr.len() == 4 { + return format!("{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3]); + } else if addr.len() == 16 { + return format!("{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}", + addr[0], + addr[1], + addr[2], + addr[3], + addr[4], + addr[5], + addr[6], + addr[7], + addr[8], + addr[9], + addr[10], + addr[11], + addr[12], + addr[13], + addr[14], + addr[15]); + } else { + return "".to_string(); + } +} + fn log_bittorrent_dht( tx: &BitTorrentDHTTransaction, js: &mut JsonBuilder, ) -> Result<(), JsonError> { @@ -58,7 +85,17 @@ fn log_bittorrent_dht( js.open_object("response")?; js.set_hex("id", &response.id)?; if let Some(nodes) = &response.nodes { - js.set_hex("nodes", nodes)?; + if !nodes.is_empty() { + js.open_array("nodes")?; + for node in nodes { + js.start_object()?; + js.set_hex("id", &node.id)?; + js.set_string("ip", &print_ip_addr(&node.ip))?; + js.set_uint("port", node.port.into())?; + js.close()?; + } + js.close()?; + } } if let Some(values) = &response.values { js.open_array("values")?; diff --git a/rust/src/bittorrent_dht/parser.rs b/rust/src/bittorrent_dht/parser.rs index ec74b8a186..b72c5aee9a 100644 --- a/rust/src/bittorrent_dht/parser.rs +++ b/rust/src/bittorrent_dht/parser.rs @@ -18,8 +18,14 @@ /*! Parses BitTorrent DHT specification BEP_0005 * !*/ +// TODO: Custom error type, as we have bencode and nom errors, and may have an our application +// specific errors as we finish off this parser. + use crate::bittorrent_dht::bittorrent_dht::BitTorrentDHTTransaction; use bendy::decoding::{Decoder, Error, FromBencode, Object, ResultExt}; +use nom7::IResult; +use nom7::bytes::complete::take; +use nom7::number::complete::be_u16; #[derive(Debug, Eq, PartialEq)] pub struct BitTorrentDHTRequest { @@ -44,7 +50,7 @@ pub struct BitTorrentDHTResponse { pub id: Vec, /// q = find_node/get_peers - compact node info for target node or /// K(8) closest good nodes in routing table - pub nodes: Option>, + pub nodes: Option>, /// q = get_peers - list of compact peer infos pub values: Option>, /// q = get_peers - token key required for sender's future @@ -60,6 +66,28 @@ pub struct BitTorrentDHTError { pub msg: String, } +#[derive(Debug, Eq, PartialEq)] +pub struct Node { + pub id: Vec, + pub ip: Vec, + pub port: u16, +} + +/// Parse IPv4 node structures. +pub fn parse_node(i: &[u8]) -> IResult<&[u8], Node> { + let (i, id) = take(20usize)(i)?; + let (i, ip) = take(4usize)(i)?; + let (i, port) = be_u16(i)?; + Ok(( + i, + Node { + id: id.to_vec(), + ip: ip.to_vec(), + port, + }, + )) +} + impl FromBencode for BitTorrentDHTRequest { // Try to parse with a `max_depth` of one. // @@ -172,10 +200,12 @@ impl FromBencode for BitTorrentDHTResponse { id = value.try_into_bytes().context("id").map(Some)?; } (b"nodes", value) => { - nodes = value - .try_into_bytes() - .context("nodes") - .map(|v| Some(v.to_vec()))?; + let (_, decoded_nodes) = + nom7::multi::many0(parse_node)(value.try_into_bytes().context("nodes")?) + .map_err(|_| Error::malformed_content("nodes.node"))?; + if !decoded_nodes.is_empty() { + nodes = Some(decoded_nodes); + } } (b"values", value) => { values = Vec::decode_bencode_object(value) @@ -427,11 +457,11 @@ mod tests { "test response from bencode 4")] #[test_case( b"d2:id20:0123456789abcdefghij5:nodes9:def456...e", - BitTorrentDHTResponse { id: b"0123456789abcdefghij".to_vec(), token: None, values: None, nodes: Some(b"def456...".to_vec()) } ; + BitTorrentDHTResponse { id: b"0123456789abcdefghij".to_vec(), token: None, values: None, nodes: None } ; "test response from bencode 5")] #[test_case( b"d2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe", - BitTorrentDHTResponse { id: b"abcdefghij0123456789".to_vec(), token: Some(b"aoeusnth".to_vec()), values: None, nodes: Some(b"def456...".to_vec()) } ; + BitTorrentDHTResponse { id: b"abcdefghij0123456789".to_vec(), token: Some(b"aoeusnth".to_vec()), values: None, nodes: None } ; "test response from bencode 6")] fn test_response_from_bencode(encoded: &[u8], expected: BitTorrentDHTResponse) { let decoded = BitTorrentDHTResponse::from_bencode(encoded).unwrap(); @@ -599,4 +629,22 @@ mod tests { let err = parse_bittorrent_dht_packet(encoded, &mut tx).unwrap_err(); assert_eq!(expected_error, err.to_string()); } + + #[test] + fn test_parse_node() { + let bytes = b"aaaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01"; + let (_rem, node) = parse_node(bytes).unwrap(); + assert_eq!(node.id, b"aaaaaaaaaaaaaaaaaaaa"); + assert_eq!(node.ip, b"\x00\x00\x00\x00"); + assert_eq!(node.port, 1); + + // Short one byte. + let bytes = b"aaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01"; + assert!(parse_node(bytes).is_err()); + + // Has remaining bytes. + let bytes = b"aaaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01bb"; + let (rem, _node) = parse_node(bytes).unwrap(); + assert_eq!(rem, b"bb"); + } } -- 2.47.2