From 4f45cdcad633374f185f1144320855c4edd492be Mon Sep 17 00:00:00 2001 From: jbesraa Date: Fri, 25 Aug 2023 05:57:56 +0300 Subject: [PATCH] Implement `from_str` trait for `NetAddress` - Add fuzz test for `NetAddress` `from_str` function --- fuzz/src/bin/fromstr_to_netaddress_target.rs | 113 +++++++++++++ fuzz/src/bin/gen_target.sh | 1 + fuzz/src/fromstr_to_netaddress.rs | 31 ++++ fuzz/src/lib.rs | 1 + fuzz/targets.h | 1 + lightning/src/ln/msgs.rs | 168 +++++++++++++++++-- 6 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 fuzz/src/bin/fromstr_to_netaddress_target.rs create mode 100644 fuzz/src/fromstr_to_netaddress.rs diff --git a/fuzz/src/bin/fromstr_to_netaddress_target.rs b/fuzz/src/bin/fromstr_to_netaddress_target.rs new file mode 100644 index 000000000..29c984e60 --- /dev/null +++ b/fuzz/src/bin/fromstr_to_netaddress_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::fromstr_to_netaddress::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + fromstr_to_netaddress_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/fromstr_to_netaddress") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + fromstr_to_netaddress_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index 676bfb821..2fa7debdf 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -22,6 +22,7 @@ GEN_TEST zbase32 GEN_TEST indexedmap GEN_TEST onion_hop_data GEN_TEST base32 +GEN_TEST fromstr_to_netaddress GEN_TEST msg_accept_channel msg_targets:: GEN_TEST msg_announcement_signatures msg_targets:: diff --git a/fuzz/src/fromstr_to_netaddress.rs b/fuzz/src/fromstr_to_netaddress.rs new file mode 100644 index 000000000..199841123 --- /dev/null +++ b/fuzz/src/fromstr_to_netaddress.rs @@ -0,0 +1,31 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use lightning::ln::msgs::NetAddress; +use core::str::FromStr; + +use crate::utils::test_logger; + +#[inline] +pub fn do_test(data: &[u8]) { + if let Ok(s) = std::str::from_utf8(data) { + let _ = NetAddress::from_str(s); + } + +} + +pub fn fromstr_to_netaddress_test(data: &[u8], _out: Out) { + do_test(data); +} + +#[no_mangle] +pub extern "C" fn fromstr_to_netaddress_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }); +} + diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 607924eff..5b5cd69cf 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -30,5 +30,6 @@ pub mod router; pub mod zbase32; pub mod onion_hop_data; pub mod base32; +pub mod fromstr_to_netaddress; pub mod msg_targets; diff --git a/fuzz/targets.h b/fuzz/targets.h index a17231c6d..cad0ac4d8 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -15,6 +15,7 @@ void zbase32_run(const unsigned char* data, size_t data_len); void indexedmap_run(const unsigned char* data, size_t data_len); void onion_hop_data_run(const unsigned char* data, size_t data_len); void base32_run(const unsigned char* data, size_t data_len); +void fromstr_to_netaddress_run(const unsigned char* data, size_t data_len); void msg_accept_channel_run(const unsigned char* data, size_t data_len); void msg_announcement_signatures_run(const unsigned char* data, size_t data_len); void msg_channel_reestablish_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index e8a226932..d12dafb65 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -37,14 +37,17 @@ use crate::ln::onion_utils; use crate::onion_message; use crate::prelude::*; +use core::convert::TryFrom; use core::fmt; use core::fmt::Debug; +use core::str::FromStr; use crate::io::{self, Read}; use crate::io_extras::read_to_end; use crate::events::{MessageSendEventsProvider, OnionMessageProvider}; use crate::util::logger; use crate::util::ser::{LengthReadable, Readable, ReadableArgs, Writeable, Writer, WithoutLength, FixedLengthReader, HighZeroBytesDroppedBigSize, Hostname, TransactionU16LenLimited, BigSize}; +use crate::util::base32; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -899,6 +902,104 @@ impl Readable for NetAddress { } } +/// [`NetAddress`] error variants +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum NetAddressParseError { + /// Socket address (IPv4/IPv6) parsing error + SocketAddrParse, + /// Invalid input format + InvalidInput, + /// Invalid port + InvalidPort, + /// Invalid onion v3 address + InvalidOnionV3, +} + +impl fmt::Display for NetAddressParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NetAddressParseError::SocketAddrParse => write!(f, "Socket address (IPv4/IPv6) parsing error"), + NetAddressParseError::InvalidInput => write!(f, "Invalid input format. \ + Expected: \":\", \"[]:\", \".onion:\" or \":\""), + NetAddressParseError::InvalidPort => write!(f, "Invalid port"), + NetAddressParseError::InvalidOnionV3 => write!(f, "Invalid onion v3 address"), + } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddrV4) -> Self { + NetAddress::IPv4 { addr: addr.ip().octets(), port: addr.port() } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddrV6) -> Self { + NetAddress::IPv6 { addr: addr.ip().octets(), port: addr.port() } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddr) -> Self { + match addr { + std::net::SocketAddr::V4(addr) => addr.into(), + std::net::SocketAddr::V6(addr) => addr.into(), + } + } +} + +fn parse_onion_address(host: &str, port: u16) -> Result { + if host.ends_with(".onion") { + let domain = &host[..host.len() - ".onion".len()]; + if domain.len() != 56 { + return Err(NetAddressParseError::InvalidOnionV3); + } + let onion = base32::Alphabet::RFC4648 { padding: false }.decode(&domain).map_err(|_| NetAddressParseError::InvalidOnionV3)?; + if onion.len() != 35 { + return Err(NetAddressParseError::InvalidOnionV3); + } + let version = onion[0]; + let first_checksum_flag = onion[1]; + let second_checksum_flag = onion[2]; + let mut ed25519_pubkey = [0; 32]; + ed25519_pubkey.copy_from_slice(&onion[3..35]); + let checksum = u16::from_be_bytes([first_checksum_flag, second_checksum_flag]); + return Ok(NetAddress::OnionV3 { ed25519_pubkey, checksum, version, port }); + + } else { + return Err(NetAddressParseError::InvalidInput); + } +} + +#[cfg(feature = "std")] +impl FromStr for NetAddress { + type Err = NetAddressParseError; + + fn from_str(s: &str) -> Result { + match std::net::SocketAddr::from_str(s) { + Ok(addr) => Ok(addr.into()), + Err(_) => { + let trimmed_input = match s.rfind(":") { + Some(pos) => pos, + None => return Err(NetAddressParseError::InvalidInput), + }; + let host = &s[..trimmed_input]; + let port: u16 = s[trimmed_input + 1..].parse().map_err(|_| NetAddressParseError::InvalidPort)?; + if host.ends_with(".onion") { + return parse_onion_address(host, port); + }; + if let Ok(hostname) = Hostname::try_from(s[..trimmed_input].to_string()) { + return Ok(NetAddress::Hostname { hostname, port }); + }; + return Err(NetAddressParseError::SocketAddrParse) + }, + } + } +} + /// Represents the set of gossip messages that require a signature from a node's identity key. pub enum UnsignedGossipMessage<'a> { /// An unsigned channel announcement. @@ -2471,6 +2572,7 @@ impl_writeable_msg!(GossipTimestampFilter, { #[cfg(test)] mod tests { + use std::convert::TryFrom; use bitcoin::blockdata::constants::ChainHash; use bitcoin::{Transaction, PackedLockTime, TxIn, Script, Sequence, Witness, TxOut}; use hex; @@ -2478,6 +2580,7 @@ mod tests { use crate::ln::ChannelId; use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; use crate::ln::msgs::{self, FinalOnionHopData, OnionErrorPacket}; + use crate::ln::msgs::NetAddress; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::util::ser::{Writeable, Readable, Hostname, TransactionU16LenLimited}; @@ -2493,11 +2596,13 @@ mod tests { use crate::io::{self, Cursor}; use crate::prelude::*; - use core::convert::TryFrom; use core::str::FromStr; - use crate::chain::transaction::OutPoint; + #[cfg(feature = "std")] + use std::net::{Ipv4Addr, Ipv6Addr}; + use crate::ln::msgs::NetAddressParseError; + #[test] fn encoding_channel_reestablish() { let public_key = { @@ -2663,24 +2768,24 @@ mod tests { }; let mut addresses = Vec::new(); if ipv4 { - addresses.push(msgs::NetAddress::IPv4 { + addresses.push(NetAddress::IPv4 { addr: [255, 254, 253, 252], port: 9735 }); } if ipv6 { - addresses.push(msgs::NetAddress::IPv6 { + addresses.push(NetAddress::IPv6 { addr: [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240], port: 9735 }); } if onionv2 { - addresses.push(msgs::NetAddress::OnionV2( + addresses.push(NetAddress::OnionV2( [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7] )); } if onionv3 { - addresses.push(msgs::NetAddress::OnionV3 { + addresses.push(NetAddress::OnionV3 { ed25519_pubkey: [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224], checksum: 32, version: 16, @@ -2688,7 +2793,7 @@ mod tests { }); } if hostname { - addresses.push(msgs::NetAddress::Hostname { + addresses.push(NetAddress::Hostname { hostname: Hostname::try_from(String::from("host")).unwrap(), port: 9735, }); @@ -3296,10 +3401,10 @@ mod tests { let shutdown = msgs::Shutdown { channel_id: ChannelId::from_bytes([2; 32]), scriptpubkey: - if script_type == 1 { Address::p2pkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).script_pubkey() } + if script_type == 1 { Address::p2pkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).script_pubkey() } else if script_type == 2 { Address::p2sh(&script, Network::Testnet).unwrap().script_pubkey() } else if script_type == 3 { Address::p2wpkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).unwrap().script_pubkey() } - else { Address::p2wsh(&script, Network::Testnet).script_pubkey() }, + else { Address::p2wsh(&script, Network::Testnet).script_pubkey() }, }; let encoded_value = shutdown.encode(); let mut target_value = hex::decode("0202020202020202020202020202020202020202020202020202020202020202").unwrap(); @@ -3504,7 +3609,7 @@ mod tests { }.encode(), hex::decode("00000000014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap()); let init_msg = msgs::Init { features: InitFeatures::from_le_bytes(vec![]), networks: Some(vec![mainnet_hash]), - remote_network_address: Some(msgs::NetAddress::IPv4 { + remote_network_address: Some(NetAddress::IPv4 { addr: [127, 0, 0, 1], port: 1000, }), @@ -3869,4 +3974,47 @@ mod tests { } Ok(encoded_payload) } + + #[test] + #[cfg(feature = "std")] + fn test_net_address_from_str() { + assert_eq!(NetAddress::IPv4 { + addr: Ipv4Addr::new(127, 0, 0, 1).octets(), + port: 1234, + }, NetAddress::from_str("127.0.0.1:1234").unwrap()); + + assert_eq!(NetAddress::IPv6 { + addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).octets(), + port: 1234, + }, NetAddress::from_str("[0:0:0:0:0:0:0:1]:1234").unwrap()); + assert_eq!( + NetAddress::Hostname { + hostname: Hostname::try_from("lightning-node.mydomain.com".to_string()).unwrap(), + port: 1234, + }, NetAddress::from_str("lightning-node.mydomain.com:1234").unwrap()); + assert_eq!( + NetAddress::Hostname { + hostname: Hostname::try_from("example.com".to_string()).unwrap(), + port: 1234, + }, NetAddress::from_str("example.com:1234").unwrap()); + assert_eq!(NetAddress::OnionV3 { + ed25519_pubkey: [37, 24, 75, 5, 25, 73, 117, 194, 139, 102, 182, 107, 4, 105, 247, 246, 85, + 111, 177, 172, 49, 137, 167, 155, 64, 221, 163, 47, 31, 33, 71, 3], + checksum: 48326, + version: 121, + port: 1234 + }, NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:1234").unwrap()); + assert_eq!(Err(NetAddressParseError::InvalidOnionV3), NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6.onion:1234")); + assert_eq!(Err(NetAddressParseError::InvalidInput), NetAddress::from_str("127.0.0.1@1234")); + assert_eq!(Err(NetAddressParseError::InvalidInput), "".parse::()); + assert!(NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion.onion:9735:94").is_err()); + assert!(NetAddress::from_str("wrong$%#.com:1234").is_err()); + assert_eq!(Err(NetAddressParseError::InvalidPort), NetAddress::from_str("example.com:wrong")); + assert!("localhost".parse::().is_err()); + assert!("localhost:invalid-port".parse::().is_err()); + assert!( "invalid-onion-v3-hostname.onion:8080".parse::().is_err()); + assert!("b32.example.onion:invalid-port".parse::().is_err()); + assert!("invalid-address".parse::().is_err()); + assert!(NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion.onion:1234").is_err()); + } } -- 2.39.5