diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index d58b44fa7b6..934d748d6a5 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -102,7 +102,7 @@ impl MessageRouter for TestMessageRouter { fn find_path( &self, _sender: PublicKey, _peers: Vec, destination: Destination, ) -> Result { - Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: None }) + Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: vec![] }) } fn create_blinded_paths( @@ -430,7 +430,7 @@ mod tests { super::do_test(&>::from_hex(two_unblinded_hops_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } let two_unblinded_two_blinded_om = "\ @@ -471,7 +471,7 @@ mod tests { super::do_test(&>::from_hex(two_unblinded_two_blinded_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } let three_blinded_om = "\ @@ -512,7 +512,7 @@ mod tests { super::do_test(&>::from_hex(three_blinded_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } } } diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 55e3ad7dd50..75fe06fcc9a 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -222,7 +222,7 @@ mod test { ) -> Result { Ok(OnionMessagePath { destination, - first_node_addresses: None, + first_node_addresses: Vec::new(), intermediate_nodes: Vec::new(), }) } diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index df89894f089..d7a13d59e3d 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -972,7 +972,9 @@ pub enum Event { ConnectionNeeded { /// The node id for the node needing a connection. node_id: PublicKey, - /// Sockets for connecting to the node. + /// Sockets for connecting to the node, if available. We don't require these addresses to be + /// present in case the node id corresponds to a known peer that is offline and can be awoken, + /// such as via the LSPS5 protocol. addresses: Vec, }, /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was @@ -1617,6 +1619,9 @@ pub enum Event { /// `OnionMessenger` was initialized with /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs. /// + /// The offline peer should be awoken if possible on receipt of this event, such as via the LSPS5 + /// protocol. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`), but won't be persisted across restarts. @@ -1661,6 +1666,14 @@ pub enum Event { /// recipient is online to provide a new invoice. This path should be persisted and /// later provided to [`ChannelManager::respond_to_static_invoice_request`]. /// + /// This path's [`BlindedMessagePath::introduction_node`] MUST be set to our node or one of our + /// peers. This is because, for DoS protection, invoice requests forwarded over this path are + /// treated by our node like any other onion message forward and will not generate + /// [`Event::ConnectionNeeded`] if the first hop in the path is not our peer. + /// + /// If the next-hop peer in the path is offline, if configured to do so we will generate an + /// [`Event::OnionMessageIntercepted`] for the invoice request. + /// /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request invoice_request_path: BlindedMessagePath, /// Useful for the recipient to replace a specific invoice stored by us as the static invoice diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ccef4480efc..9d327d0060b 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -2887,6 +2887,21 @@ fn async_payment_e2e() { .into_iter() .find_map(|ev| { if let Event::OnionMessageIntercepted { message, .. } = ev { + // At least one of the intercepted onion messages will be an invoice request that the + // invoice server is attempting to forward to the recipient, ignore that as we're testing + // the static invoice flow + let peeled_onion = recipient.onion_messenger.peel_onion_message(&message).unwrap(); + if matches!( + peeled_onion, + PeeledOnion::Offers(OffersMessage::InvoiceRequest { .. }, _, _) + ) { + return None; + } + + assert!(matches!( + peeled_onion, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(_), _, _) + )); Some(message) } else { None diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index a6484f0076e..6b0132fda7a 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1169,9 +1169,9 @@ where ) { let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); let message = OffersMessage::InvoiceRequest(invoice_request); - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + let instructions = MessageSendInstructions::ForwardedMessage { destination: Destination::BlindedPath(destination), - reply_path: reply_path.into_blinded_path(), + reply_path: Some(reply_path.into_blinded_path()), }; pending_offers_messages.push((message, instructions)); } diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 877af435bb4..127126e150f 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -169,6 +169,11 @@ pub struct ServeStaticInvoice { /// [`Bolt12Invoice`] if the recipient is online at the time. Use this path to forward the /// [`InvoiceRequest`] to the async recipient. /// + /// This path's [`BlindedMessagePath::introduction_node`] MUST be set to the static invoice server + /// node or one of its peers. This is because, for DoS protection, invoice requests forwarded over + /// this path are treated by the server node like any other onion message forward and the server + /// will not directly connect to the introduction node if they are not already peers. + /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub forward_invoice_request_path: BlindedMessagePath, diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 1cbea9fef8d..605a81a4f95 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -419,7 +419,7 @@ fn two_unblinded_hops() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id], destination: Destination::Node(nodes[2].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); @@ -494,7 +494,7 @@ fn two_unblinded_two_blinded() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::BlindedPath(blinded_path), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); @@ -660,7 +660,7 @@ fn too_big_packet_error() { let path = OnionMessagePath { intermediate_nodes: hops, destination: Destination::Node(hop_node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; let err = nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap_err(); assert_eq!(err, SendError::TooBigPacket); @@ -822,7 +822,7 @@ fn reply_path() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::Node(nodes[3].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; let intermediate_nodes = [ MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, @@ -959,7 +959,7 @@ fn many_hops() { let path = OnionMessagePath { intermediate_nodes, destination: Destination::Node(nodes[num_nodes - 1].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); nodes[num_nodes - 1].custom_message_handler.expect_message(TestCustomMessage::Pong); @@ -1012,6 +1012,29 @@ fn requests_peer_connection_for_buffered_messages() { connect_peers(&nodes[0], &nodes[1]); assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_some()); assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_none()); + + // Buffer an onion message for a disconnected node who is not in the network graph. + disconnect_peers(&nodes[0], &nodes[2]); + + let message = TestCustomMessage::Ping; + let destination = Destination::Node(nodes[2].node_id); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + nodes[0].messenger.send_onion_message(message.clone(), instructions.clone()).unwrap(); + + // Check that a ConnectionNeeded event for the peer is provided + let events = release_events(&nodes[0]); + assert_eq!(events.len(), 1); + match &events[0] { + Event::ConnectionNeeded { node_id, addresses } => { + assert_eq!(*node_id, nodes[2].node_id); + assert!(addresses.is_empty()); + }, + e => panic!("Unexpected event: {:?}", e), + } + + // Release the buffered onion message when reconnected + connect_peers(&nodes[0], &nodes[2]); + assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[2].node_id).is_some()); } #[test] diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 4fe2a63ae16..890eee8859b 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -210,7 +210,7 @@ where /// # Ok(OnionMessagePath { /// # intermediate_nodes: vec![hop_node_id1, hop_node_id2], /// # destination, -/// # first_node_addresses: None, +/// # first_node_addresses: Vec::new(), /// # }) /// # } /// # fn create_blinded_paths( @@ -490,6 +490,21 @@ pub enum MessageSendInstructions { /// The instructions provided by the [`Responder`]. instructions: ResponseInstruction, }, + /// Indicates that this onion message did not originate from our node and is being forwarded + /// through us from another node on the network to the destination. + /// + /// We separate out this case because forwarded onion messages are treated differently from + /// outbound onion messages initiated by our node. Outbounds are buffered internally, whereas, for + /// DoS protection, forwards should never be buffered internally and instead will either be + /// dropped or generate an [`Event::OnionMessageIntercepted`] if the next-hop node is + /// disconnected. + ForwardedMessage { + /// The destination where we need to send the forwarded onion message. + destination: Destination, + /// The reply path which should be included in the message, that terminates at the original + /// sender of this forwarded message. + reply_path: Option, + }, } /// A trait defining behavior for routing an [`OnionMessage`]. @@ -666,7 +681,7 @@ where Ok(OnionMessagePath { intermediate_nodes: vec![], destination, - first_node_addresses: None, + first_node_addresses: vec![], }) } else { let node_details = network_graph @@ -680,11 +695,19 @@ where Some((features, addresses)) if features.supports_onion_messages() && addresses.len() > 0 => { - let first_node_addresses = Some(addresses.to_vec()); Ok(OnionMessagePath { intermediate_nodes: vec![], destination, - first_node_addresses, + first_node_addresses: addresses.to_vec(), + }) + }, + None => { + // If the destination is an unannounced node, they may be a known peer that is offline and + // can be woken by the sender. + Ok(OnionMessagePath { + intermediate_nodes: vec![], + destination, + first_node_addresses: vec![], }) }, _ => Err(()), @@ -826,9 +849,9 @@ pub struct OnionMessagePath { /// Addresses that may be used to connect to [`OnionMessagePath::first_node`]. /// - /// Only needs to be set if a connection to the node is required. [`OnionMessenger`] may use - /// this to initiate such a connection. - pub first_node_addresses: Option>, + /// Only needs to be filled in if a connection to the node is required and it is not a known peer. + /// [`OnionMessenger`] may use this to initiate such a connection. + pub first_node_addresses: Vec, } impl OnionMessagePath { @@ -1006,7 +1029,7 @@ pub fn create_onion_message_resolving_destination< entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, network_graph: &ReadOnlyNetworkGraph, secp_ctx: &Secp256k1, mut path: OnionMessagePath, contents: T, reply_path: Option, -) -> Result<(PublicKey, OnionMessage, Option>), SendError> +) -> Result<(PublicKey, OnionMessage, Vec), SendError> where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1039,7 +1062,7 @@ pub fn create_onion_message, path: OnionMessagePath, contents: T, reply_path: Option, -) -> Result<(PublicKey, OnionMessage, Option>), SendError> +) -> Result<(PublicKey, OnionMessage, Vec), SendError> where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1467,6 +1490,7 @@ where fn send_onion_message_internal( &self, contents: T, instructions: MessageSendInstructions, log_suffix: fmt::Arguments, ) -> Result { + let is_forward = matches!(instructions, MessageSendInstructions::ForwardedMessage { .. }); let (destination, reply_path) = match instructions { MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path } => { (destination, Some(reply_path)) @@ -1490,14 +1514,52 @@ where | MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: None }, } => (destination, None), + MessageSendInstructions::ForwardedMessage { destination, reply_path } => { + (destination, reply_path) + }, }; - let mut logger = WithContext::from(&self.logger, None, None, None); - let result = self.find_path(destination).and_then(|path| { - let first_hop = path.intermediate_nodes.get(0).map(|p| *p); - logger = WithContext::from(&self.logger, first_hop, None, None); - self.enqueue_onion_message(path, contents, reply_path, log_suffix) - }); + let path = if is_forward { + // If this onion message is being treated as a forward, we shouldn't pathfind to the next hop. + OnionMessagePath { + intermediate_nodes: Vec::new(), + first_node_addresses: Vec::new(), + destination, + } + } else { + self.find_path(destination).map_err(|e| { + log_trace!(self.logger, "Failed to find path {}", log_suffix); + e + })? + }; + let first_hop = path.intermediate_nodes.get(0).map(|p| *p); + let logger = WithContext::from(&self.logger, first_hop, None, None); + + log_trace!(logger, "Constructing onion message {}: {:?}", log_suffix, contents); + let (first_node_id, onion_message, addresses) = create_onion_message( + &self.entropy_source, + &self.node_signer, + &self.node_id_lookup, + &self.secp_ctx, + path, + contents, + reply_path, + ) + .map_err(|e| { + log_warn!(logger, "Failed to create onion message with {:?} {}", e, log_suffix); + e + })?; + + let result = if is_forward { + self.enqueue_forwarded_onion_message( + NextMessageHop::NodeId(first_node_id), + onion_message, + log_suffix, + ) + .map(|()| SendSuccess::Buffered) + } else { + self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses) + }; match result.as_ref() { Err(SendError::GetNodeIdFailed) => { @@ -1578,36 +1640,20 @@ where .map_err(|_| SendError::PathNotFound) } - fn enqueue_onion_message( - &self, path: OnionMessagePath, contents: T, reply_path: Option, - log_suffix: fmt::Arguments, + fn enqueue_outbound_onion_message( + &self, onion_message: OnionMessage, first_node_id: PublicKey, addresses: Vec, ) -> Result { - log_trace!(self.logger, "Constructing onion message {}: {:?}", log_suffix, contents); - - let (first_node_id, onion_message, addresses) = create_onion_message( - &self.entropy_source, - &self.node_signer, - &self.node_id_lookup, - &self.secp_ctx, - path, - contents, - reply_path, - )?; - let mut message_recipients = self.message_recipients.lock().unwrap(); if outbound_buffer_full(&first_node_id, &message_recipients) { return Err(SendError::BufferFull); } match message_recipients.entry(first_node_id) { - hash_map::Entry::Vacant(e) => match addresses { - None => Err(SendError::InvalidFirstHop(first_node_id)), - Some(addresses) => { - e.insert(OnionMessageRecipient::pending_connection(addresses)) - .enqueue_message(onion_message); - self.event_notifier.notify(); - Ok(SendSuccess::BufferedAwaitingConnection(first_node_id)) - }, + hash_map::Entry::Vacant(e) => { + e.insert(OnionMessageRecipient::pending_connection(addresses)) + .enqueue_message(onion_message); + self.event_notifier.notify(); + Ok(SendSuccess::BufferedAwaitingConnection(first_node_id)) }, hash_map::Entry::Occupied(mut e) => { e.get_mut().enqueue_message(onion_message); @@ -1620,6 +1666,74 @@ where } } + fn enqueue_forwarded_onion_message( + &self, next_hop: NextMessageHop, onion_message: OnionMessage, log_suffix: fmt::Arguments, + ) -> Result<(), SendError> { + let next_node_id = match next_hop { + NextMessageHop::NodeId(pubkey) => pubkey, + NextMessageHop::ShortChannelId(scid) => match self.node_id_lookup.next_node_id(scid) { + Some(pubkey) => pubkey, + None => { + log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {} {}", scid, log_suffix); + return Err(SendError::GetNodeIdFailed); + }, + }, + }; + + let mut message_recipients = self.message_recipients.lock().unwrap(); + if outbound_buffer_full(&next_node_id, &message_recipients) { + log_trace!( + self.logger, + "Dropping forwarded onion message to peer {}: outbound buffer full {}", + next_node_id, + log_suffix + ); + return Err(SendError::BufferFull); + } + + #[cfg(fuzzing)] + message_recipients + .entry(next_node_id) + .or_insert_with(|| OnionMessageRecipient::ConnectedPeer(VecDeque::new())); + + match message_recipients.entry(next_node_id) { + hash_map::Entry::Occupied(mut e) + if matches!(e.get(), OnionMessageRecipient::ConnectedPeer(..)) => + { + e.get_mut().enqueue_message(onion_message); + log_trace!( + self.logger, + "Forwarding an onion message to peer {} {}", + next_node_id, + log_suffix + ); + Ok(()) + }, + _ if self.intercept_messages_for_offline_peers => { + log_trace!( + self.logger, + "Generating OnionMessageIntercepted event for peer {} {}", + next_node_id, + log_suffix + ); + self.enqueue_intercepted_event(Event::OnionMessageIntercepted { + peer_node_id: next_node_id, + message: onion_message, + }); + Ok(()) + }, + _ => { + log_trace!( + self.logger, + "Dropping forwarded onion message to disconnected peer {} {}", + next_node_id, + log_suffix + ); + Err(SendError::InvalidFirstHop(next_node_id)) + }, + } + } + /// Forwards an [`OnionMessage`] to `peer_node_id`. Useful if we initialized /// the [`OnionMessenger`] with [`Self::new_with_offline_peer_interception`] /// and want to forward a previously intercepted onion message to a peer that @@ -1645,7 +1759,16 @@ where pub fn send_onion_message_using_path( &self, path: OnionMessagePath, contents: T, reply_path: Option, ) -> Result { - self.enqueue_onion_message(path, contents, reply_path, format_args!("")) + let (first_node_id, onion_message, addresses) = create_onion_message( + &self.entropy_source, + &self.node_signer, + &self.node_id_lookup, + &self.secp_ctx, + path, + contents, + reply_path, + )?; + self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses) } pub(crate) fn peel_onion_message( @@ -2204,56 +2327,11 @@ where } }, Ok(PeeledOnion::Forward(next_hop, onion_message)) => { - let next_node_id = match next_hop { - NextMessageHop::NodeId(pubkey) => pubkey, - NextMessageHop::ShortChannelId(scid) => { - match self.node_id_lookup.next_node_id(scid) { - Some(pubkey) => pubkey, - None => { - log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {}", scid); - return; - }, - } - }, - }; - - let mut message_recipients = self.message_recipients.lock().unwrap(); - if outbound_buffer_full(&next_node_id, &message_recipients) { - log_trace!( - logger, - "Dropping forwarded onion message to peer {}: outbound buffer full", - next_node_id - ); - return; - } - - #[cfg(fuzzing)] - message_recipients - .entry(next_node_id) - .or_insert_with(|| OnionMessageRecipient::ConnectedPeer(VecDeque::new())); - - match message_recipients.entry(next_node_id) { - hash_map::Entry::Occupied(mut e) - if matches!(e.get(), OnionMessageRecipient::ConnectedPeer(..)) => - { - e.get_mut().enqueue_message(onion_message); - log_trace!(logger, "Forwarding an onion message to peer {}", next_node_id); - }, - _ if self.intercept_messages_for_offline_peers => { - self.enqueue_intercepted_event(Event::OnionMessageIntercepted { - peer_node_id: next_node_id, - message: onion_message, - }); - }, - _ => { - log_trace!( - logger, - "Dropping forwarded onion message to disconnected peer {}", - next_node_id - ); - return; - }, - } + let _ = self.enqueue_forwarded_onion_message( + next_hop, + onion_message, + format_args!("when forwarding peeled onion message from {}", peer_node_id), + ); }, Err(e) => { log_error!(logger, "Failed to process onion message {:?}", e);