diff --git a/.gitignore b/.gitignore index fbeffa8a9c9..8507aea8368 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ lightning/net_graph-*.bin lightning-rapid-gossip-sync/res/full_graph.lngossip lightning-custom-message/target lightning-transaction-sync/target +lightning-dns-resolver/target no-std-check/target msrv-no-dev-deps-check/target diff --git a/Cargo.toml b/Cargo.toml index add69f35afa..ec51b33f47c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "lightning-rapid-gossip-sync", "lightning-custom-message", "lightning-transaction-sync", + "lightning-dns-resolver", "possiblyrandom", ] diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index ed2efbaad2d..d358ded8fae 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -46,6 +46,7 @@ WORKSPACE_MEMBERS=( lightning-rapid-gossip-sync lightning-custom-message lightning-transaction-sync + lightning-dns-resolver possiblyrandom ) @@ -56,10 +57,6 @@ for DIR in "${WORKSPACE_MEMBERS[@]}"; do cargo doc -p "$DIR" --document-private-items done -echo -e "\n\nChecking and testing lightning crate with dnssec feature" -cargo test -p lightning --verbose --color always --features dnssec -cargo check -p lightning --verbose --color always --features dnssec - echo -e "\n\nChecking and testing Block Sync Clients with features" cargo test -p lightning-block-sync --verbose --color always --features rest-client diff --git a/lightning-dns-resolver/Cargo.toml b/lightning-dns-resolver/Cargo.toml new file mode 100644 index 00000000000..84337e148a9 --- /dev/null +++ b/lightning-dns-resolver/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lightning-resolver" +version = "0.1.0" +edition = "2021" + +[dependencies] +lightning = { version = "0.0.124-rc1", path = "../lightning", default-features = false } +dnssec-prover = { version = "0.6", default-features = false, features = [ "std", "tokio" ] } +tokio = { version = "1.0", default-features = false, features = ["rt"] } + +[dev-dependencies] +bitcoin = { version = "0.32" } +tokio = { version = "1.0", default-features = false, features = ["macros", "time"] } +lightning = { version = "0.0.124-rc1", path = "../lightning", features = ["dnssec", "_test_utils"] } diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs new file mode 100644 index 00000000000..f06b8abc312 --- /dev/null +++ b/lightning-dns-resolver/src/lib.rs @@ -0,0 +1,421 @@ +//! A simple crate which uses [`dnssec_prover`] to create DNSSEC Proofs in response to bLIP 32 +//! Onion Message DNSSEC Proof Queries. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::private_intra_doc_links)] + +use std::net::SocketAddr; +use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use dnssec_prover::query::build_txt_proof_async; + +use lightning::blinded_path::message::DNSResolverContext; +use lightning::ln::peer_handler::IgnoringMessageHandler; +use lightning::onion_message::dns_resolution::{ + DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, +}; +use lightning::onion_message::messenger::{ + MessageSendInstructions, Responder, ResponseInstruction, +}; + +#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] +const WE_REQUIRE_32_OR_64_BIT_USIZE: u8 = 424242; + +/// A resolver which implements [`DNSResolverMessageHandler`] and replies to [`DNSSECQuery`] +/// messages with with [`DNSSECProof`]s. +pub struct OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + state: Arc, + proof_handler: Option, +} + +const MAX_PENDING_RESPONSES: usize = 1024; +struct OMResolverState { + resolver: SocketAddr, + pending_replies: Mutex>, + pending_query_count: AtomicUsize, +} + +impl OMDomainResolver { + /// Creates a new [`OMDomainResolver`] given the [`SocketAddr`] of a DNS resolver listening on + /// TCP (e.g. 8.8.8.8:53, 1.1.1.1:53 or your local DNS resolver). + /// + /// Ignores any incoming [`DNSSECProof`] messages. + pub fn ignoring_incoming_proofs(resolver: SocketAddr) -> Self { + Self::new(resolver, None) + } +} + +impl OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + /// Creates a new [`OMDomainResolver`] given the [`SocketAddr`] of a DNS resolver listening on + /// TCP (e.g. 8.8.8.8:53, 1.1.1.1:53 or your local DNS resolver). + /// + /// The optional `proof_handler` can be provided to pass proofs coming back to us to the + /// underlying handler. This is useful when this resolver is handling incoming resolution + /// requests but some other handler is making proof requests of remote nodes and wants to get + /// results. + pub fn new(resolver: SocketAddr, proof_handler: Option) -> Self { + Self { + state: Arc::new(OMResolverState { + resolver, + pending_replies: Mutex::new(Vec::new()), + pending_query_count: AtomicUsize::new(0), + }), + proof_handler, + } + } +} + +impl DNSResolverMessageHandler for OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + fn handle_dnssec_proof(&self, proof: DNSSECProof, context: DNSResolverContext) { + if let Some(proof_handler) = &self.proof_handler { + proof_handler.handle_dnssec_proof(proof, context); + } + } + + fn handle_dnssec_query( + &self, q: DNSSECQuery, responder_opt: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + let responder = match responder_opt { + Some(responder) => responder, + None => return None, + }; + if self.state.pending_query_count.fetch_add(1, Ordering::Relaxed) > MAX_PENDING_RESPONSES { + self.state.pending_query_count.fetch_sub(1, Ordering::Relaxed); + return None; + } + let us = Arc::clone(&self.state); + tokio::spawn(async move { + if let Ok((proof, _ttl)) = build_txt_proof_async(us.resolver, &q.0).await { + let contents = DNSResolverMessage::DNSSECProof(DNSSECProof { name: q.0, proof }); + let instructions = responder.respond().into_instructions(); + us.pending_replies.lock().unwrap().push((contents, instructions)); + us.pending_query_count.fetch_sub(1, Ordering::Relaxed); + } + }); + None + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut *self.state.pending_replies.lock().unwrap()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; + use bitcoin::Block; + + use lightning::blinded_path::message::{BlindedMessagePath, MessageContext}; + use lightning::blinded_path::NodeIdLookUp; + use lightning::events::{Event, PaymentPurpose}; + use lightning::ln::channelmanager::{PaymentId, Retry}; + use lightning::ln::features::InitFeatures; + use lightning::ln::functional_test_utils::*; + use lightning::ln::msgs::{ChannelMessageHandler, Init, OnionMessageHandler}; + use lightning::ln::peer_handler::IgnoringMessageHandler; + use lightning::onion_message::dns_resolution::{HumanReadableName, OMNameResolver}; + use lightning::onion_message::messenger::{ + AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, + }; + use lightning::sign::{KeysManager, NodeSigner, Recipient}; + use lightning::types::payment::PaymentHash; + use lightning::util::logger::Logger; + + use lightning::{ + commitment_signed_dance, expect_payment_claimed, expect_pending_htlcs_forwardable, + get_htlc_update_msgs, + }; + + use std::ops::Deref; + use std::sync::Mutex; + use std::time::{Duration, Instant, SystemTime}; + + struct TestLogger { + node: &'static str, + } + impl Logger for TestLogger { + fn log(&self, record: lightning::util::logger::Record) { + eprintln!("{}: {}", self.node, record.args); + } + } + impl Deref for TestLogger { + type Target = TestLogger; + fn deref(&self) -> &TestLogger { + self + } + } + + struct DummyNodeLookup {} + impl NodeIdLookUp for DummyNodeLookup { + fn next_node_id(&self, _: u64) -> Option { + None + } + } + impl Deref for DummyNodeLookup { + type Target = DummyNodeLookup; + fn deref(&self) -> &DummyNodeLookup { + self + } + } + + struct DirectlyConnectedRouter {} + impl MessageRouter for DirectlyConnectedRouter { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, destination: Destination, + ) -> Result { + Ok(OnionMessagePath { + destination, + first_node_addresses: None, + intermediate_nodes: Vec::new(), + }) + } + + fn create_blinded_paths( + &self, recipient: PublicKey, context: MessageContext, _peers: Vec, + secp_ctx: &Secp256k1, + ) -> Result, ()> { + let keys = KeysManager::new(&[0; 32], 42, 43); + Ok(vec![BlindedMessagePath::one_hop(recipient, context, &keys, secp_ctx).unwrap()]) + } + } + impl Deref for DirectlyConnectedRouter { + type Target = DirectlyConnectedRouter; + fn deref(&self) -> &DirectlyConnectedRouter { + self + } + } + + struct URIResolver { + resolved_uri: Mutex>, + resolver: OMNameResolver, + pending_messages: Mutex>, + } + impl DNSResolverMessageHandler for URIResolver { + fn handle_dnssec_query( + &self, _: DNSSECQuery, _: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + panic!(); + } + + fn handle_dnssec_proof(&self, msg: DNSSECProof, context: DNSResolverContext) { + let mut proof = self.resolver.handle_dnssec_proof_for_uri(msg, context).unwrap(); + assert_eq!(proof.0.len(), 1); + let payment = proof.0.pop().unwrap(); + let mut result = Some((payment.0, payment.1, proof.1)); + core::mem::swap(&mut *self.resolved_uri.lock().unwrap(), &mut result); + assert!(result.is_none()); + } + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut *self.pending_messages.lock().unwrap()) + } + } + + fn create_resolver() -> (impl AOnionMessenger, PublicKey) { + let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43)); + let resolver_logger = TestLogger { node: "resolver" }; + let resolver = Arc::new(OMDomainResolver::new("8.8.8.8:53".parse().unwrap())); + ( + OnionMessenger::new( + Arc::clone(&resolver_keys), + Arc::clone(&resolver_keys), + resolver_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&resolver), + IgnoringMessageHandler {}, + ), + resolver_keys.get_node_id(Recipient::Node).unwrap(), + ) + } + + fn get_om_init() -> Init { + let mut init_msg = + Init { features: InitFeatures::empty(), networks: None, remote_network_address: None }; + init_msg.features.set_onion_messages_optional(); + init_msg + } + + #[tokio::test] + async fn resolution_test() { + let secp_ctx = Secp256k1::new(); + + let (resolver_messenger, resolver_id) = create_resolver(); + + let resolver_dest = Destination::Node(resolver_id); + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + + let payment_id = PaymentId([42; 32]); + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43)); + let payer_logger = TestLogger { node: "payer" }; + let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); + let payer = Arc::new(URIResolver { + resolved_uri: Mutex::new(None), + resolver: OMNameResolver::new(now as u32, 1), + pending_messages: Mutex::new(Vec::new()), + }); + let payer_messenger = Arc::new(OnionMessenger::new( + Arc::clone(&payer_keys), + Arc::clone(&payer_keys), + payer_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&payer), + IgnoringMessageHandler {}, + )); + + let init_msg = get_om_init(); + payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + + let (msg, context) = + payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); + let query_context = MessageContext::DNSResolver(context); + let reply_path = + BlindedMessagePath::one_hop(payer_id, query_context, &*payer_keys, &secp_ctx).unwrap(); + payer.pending_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(msg), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: resolver_dest, + reply_path, + }, + )); + + let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.get_om().handle_onion_message(payer_id, &query); + + assert!(resolver_messenger.get_om().next_onion_message_for_peer(payer_id).is_none()); + let start = Instant::now(); + let response = loop { + tokio::time::sleep(Duration::from_millis(10)).await; + if let Some(msg) = resolver_messenger.get_om().next_onion_message_for_peer(payer_id) { + break msg; + } + assert!(start.elapsed() < Duration::from_secs(10), "Resolution took too long"); + }; + + payer_messenger.handle_onion_message(resolver_id, &response); + let resolution = payer.resolved_uri.lock().unwrap().take().unwrap(); + assert_eq!(resolution.0, name); + assert_eq!(resolution.1, payment_id); + assert!(resolution.2[.."bitcoin:".len()].eq_ignore_ascii_case("bitcoin:")); + } + + #[tokio::test] + async fn end_to_end_test() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + + // The DNSSEC validation will only work with the current time, so set the time on the + // resolver. + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + let block = Block { + header: create_dummy_header(nodes[0].best_block_hash(), now as u32), + txdata: Vec::new(), + }; + connect_block(&nodes[0], &block); + connect_block(&nodes[1], &block); + + let payer_id = nodes[0].node.get_our_node_id(); + let payee_id = nodes[1].node.get_our_node_id(); + + let (resolver_messenger, resolver_id) = create_resolver(); + let init_msg = get_om_init(); + nodes[0].onion_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + // When we get the proof back, override its contents to an offer from nodes[1] + let bs_offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + nodes[0] + .node + .testing_dnssec_proof_offer_resolution_override + .lock() + .unwrap() + .insert(name.clone(), bs_offer); + + let payment_id = PaymentId([42; 32]); + let resolvers = vec![Destination::Node(resolver_id)]; + let retry = Retry::Attempts(0); + let amt = 42_000; + nodes[0] + .node + .pay_for_offer_from_human_readable_name(name, amt, payment_id, retry, None, resolvers) + .unwrap(); + + let query = nodes[0].onion_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.get_om().handle_onion_message(payer_id, &query); + + assert!(resolver_messenger.get_om().next_onion_message_for_peer(payer_id).is_none()); + let start = Instant::now(); + let response = loop { + tokio::time::sleep(Duration::from_millis(10)).await; + if let Some(msg) = resolver_messenger.get_om().next_onion_message_for_peer(payer_id) { + break msg; + } + assert!(start.elapsed() < Duration::from_secs(10), "Resolution took too long"); + }; + + nodes[0].onion_messenger.handle_onion_message(resolver_id, &response); + + let invreq = nodes[0].onion_messenger.next_onion_message_for_peer(payee_id).unwrap(); + nodes[1].onion_messenger.handle_onion_message(payer_id, &invreq); + + let inv = nodes[1].onion_messenger.next_onion_message_for_peer(payer_id).unwrap(); + nodes[0].onion_messenger.handle_onion_message(payee_id, &inv); + + check_added_monitors(&nodes[0], 1); + let updates = get_htlc_update_msgs!(nodes[0], payee_id); + nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]); + commitment_signed_dance!(nodes[1], nodes[0], updates.commitment_signed, false); + expect_pending_htlcs_forwardable!(nodes[1]); + + let claimable_events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(claimable_events.len(), 1); + let our_payment_preimage; + if let Event::PaymentClaimable { purpose, amount_msat, .. } = &claimable_events[0] { + assert_eq!(*amount_msat, amt); + if let PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. } = purpose { + our_payment_preimage = payment_preimage.unwrap(); + nodes[1].node.claim_funds(our_payment_preimage); + let payment_hash: PaymentHash = our_payment_preimage.into(); + expect_payment_claimed!(nodes[1], payment_hash, amt); + } else { + panic!(); + } + } else { + panic!(); + } + + check_added_monitors(&nodes[1], 1); + let updates = get_htlc_update_msgs!(nodes[1], payer_id); + nodes[0].node.handle_update_fulfill_htlc(payee_id, &updates.update_fulfill_htlcs[0]); + commitment_signed_dance!(nodes[0], nodes[1], updates.commitment_signed, false); + + expect_payment_sent(&nodes[0], our_payment_preimage, None, true, true); + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b7c9acc13f8..6aa310f17cb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2439,6 +2439,14 @@ where #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex>, + #[cfg(feature = "_test_utils")] + /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an + /// offer generated in the test. + /// + /// This allows for doing so, validating proofs as normal, but, if they pass, replacing the + /// offer they resolve to to the given one. + pub testing_dnssec_proof_offer_resolution_override: Mutex>, + entropy_source: ES, node_signer: NS, signer_provider: SP, @@ -3267,6 +3275,9 @@ where dns_resolver: OMNameResolver::new(current_timestamp, params.best_block.height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), } } @@ -11464,8 +11475,15 @@ where fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { let offer_opt = self.dns_resolver.handle_dnssec_proof_for_offer(message, context); - if let Some((completed_requests, offer)) = offer_opt { + #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] + if let Some((completed_requests, mut offer)) = offer_opt { for (name, payment_id) in completed_requests { + #[cfg(feature = "_test_utils")] + if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { + // If we have multiple pending requests we may end up over-using the override + // offer, but tests can deal with that. + offer = replacement_offer; + } if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) { let offer_pay_res = self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), @@ -13241,6 +13259,9 @@ where dns_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), }; for htlc_source in failed_htlcs.drain(..) { diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index c84e3c6c53c..c53a54d7ed9 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -406,7 +406,9 @@ pub struct ResponseInstruction { } impl ResponseInstruction { - fn into_instructions(self) -> MessageSendInstructions { + /// Converts this [`ResponseInstruction`] into a [`MessageSendInstructions`] so that it can be + /// used to send the response via a normal message sending method. + pub fn into_instructions(self) -> MessageSendInstructions { MessageSendInstructions::ForReply { instructions: self } } }