diff --git a/src/builder.rs b/src/builder.rs index ddf9104cc2..eb816c8206 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -610,6 +610,19 @@ impl NodeBuilder { self } + /// Sets whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// Updates [`Config::disable_lightning_connections`]. + /// + /// Defaults to `false`. If `true`, automatic reconnections and explicit outbound connection + /// attempts are disabled for the built node. Inbound listening must also be disabled by + /// setting [`Config::listening_addresses`] to `None` or an empty list; + /// otherwise building fails with [`BuildError::InvalidListeningAddresses`]. + pub fn set_disable_lightning_connections(&mut self, disable: bool) -> &mut Self { + self.config.disable_lightning_connections = disable; + self + } + /// Sets the IP address and TCP port on which [`Node`] will listen for incoming network connections. pub fn set_listening_addresses( &mut self, listening_addresses: Vec, @@ -1259,6 +1272,18 @@ impl Builder { self.inner.write().expect("lock").set_network(network); } + /// Sets whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// Updates [`Config::disable_lightning_connections`]. + /// + /// Defaults to `false`. If `true`, automatic reconnections and explicit outbound connection + /// attempts are disabled for the built node. Inbound listening must also be disabled by + /// setting [`Config::listening_addresses`] to `None` or an empty list; + /// otherwise building fails with [`BuildError::InvalidListeningAddresses`]. + pub fn set_disable_lightning_connections(&self, disable: bool) { + self.inner.write().expect("lock").set_disable_lightning_connections(disable); + } + /// Sets the IP address and TCP port on which [`Node`] will listen for incoming network connections. pub fn set_listening_addresses( &self, listening_addresses: Vec, @@ -1531,6 +1556,16 @@ fn build_with_store_internal( ) -> Result { optionally_install_rustls_cryptoprovider(); + if config.disable_lightning_connections + && config.listening_addresses.as_ref().is_some_and(|addresses| !addresses.is_empty()) + { + log_error!( + logger, + "Listening addresses must be unset or empty when Lightning connections are disabled." + ); + return Err(BuildError::InvalidListeningAddresses); + } + if let Err(err) = may_announce_channel(&config) { if config.announcement_addresses.is_some() { log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err); @@ -2431,6 +2466,7 @@ fn build_with_store_internal( let connection_manager = Arc::new(ConnectionManager::new( Arc::clone(&peer_manager), + config.disable_lightning_connections, config.tor_config.clone(), Arc::clone(&keys_manager), Arc::clone(&logger), diff --git a/src/config.rs b/src/config.rs index d524eac58e..6af4823d65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -257,6 +257,7 @@ impl Default for ForwardedPaymentTrackingMode { /// | `storage_dir_path` | /tmp/ldk_node/ | /// | `network` | Bitcoin | /// | `listening_addresses` | None | +/// | `disable_lightning_connections` | false | /// | `announcement_addresses` | None | /// | `node_alias` | None | /// | `trusted_peers_0conf` | [] | @@ -286,6 +287,15 @@ pub struct Config { /// `listening_addresses` are set. When both are set, we also accept payment forwards over /// unannounced channels. pub listening_addresses: Option>, + /// Whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// If `true`, automatic reconnections and explicit outbound connection attempts are disabled. + /// [`Config::listening_addresses`] must also be `None` or an empty list; otherwise building + /// fails with [`BuildError::InvalidListeningAddresses`]. Chain synchronization and other + /// non-peer networking are unaffected. + /// + /// [`BuildError::InvalidListeningAddresses`]: crate::BuildError::InvalidListeningAddresses + pub disable_lightning_connections: bool, /// The addresses which the node will announce to the gossip network that it accepts connections on. /// /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. @@ -358,6 +368,7 @@ impl Default for Config { storage_dir_path: DEFAULT_STORAGE_DIR_PATH.to_string(), network: DEFAULT_NETWORK, listening_addresses: None, + disable_lightning_connections: false, announcement_addresses: None, trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, diff --git a/src/connection.rs b/src/connection.rs index ccb6f98464..70013cbb29 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -14,7 +14,7 @@ use bitcoin::secp256k1::PublicKey; use lightning::ln::msgs::SocketAddress; use crate::config::TorConfig; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +use crate::logger::{log_debug, log_error, log_info, log_warn, LdkLogger}; use crate::types::{KeysManager, PeerManager}; use crate::Error; @@ -57,6 +57,7 @@ where { pending_connections: PendingConnections, peer_manager: Arc, + disable_lightning_connections: bool, tor_proxy_config: Option, keys_manager: Arc, logger: L, @@ -67,12 +68,19 @@ where L::Target: LdkLogger, { pub(crate) fn new( - peer_manager: Arc, tor_proxy_config: Option, - keys_manager: Arc, logger: L, + peer_manager: Arc, disable_lightning_connections: bool, + tor_proxy_config: Option, keys_manager: Arc, logger: L, ) -> Self { let pending_connections = Mutex::new(HashMap::new()); - Self { pending_connections, peer_manager, tor_proxy_config, keys_manager, logger } + Self { + pending_connections, + peer_manager, + disable_lightning_connections, + tor_proxy_config, + keys_manager, + logger, + } } pub(crate) async fn connect_peer_if_necessary( @@ -115,6 +123,11 @@ where async fn do_connect_peer_internal( &self, node_id: PublicKey, addr: SocketAddress, ) -> Result<(), Error> { + if self.disable_lightning_connections { + log_warn!(self.logger, "Lightning peer connections are disabled."); + return Err(Error::ConnectionFailed); + } + log_info!(self.logger, "Connecting to peer: {}@{}", node_id, addr); match addr { diff --git a/src/lib.rs b/src/lib.rs index af19bf51e7..1283b83652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1322,6 +1322,8 @@ impl Node { /// Connect to a node on the peer-to-peer network. /// /// If `persist` is set to `true`, we'll remember the peer and reconnect to it on restart. + /// Outbound connection attempts fail with [`Error::ConnectionFailed`] if + /// [`Config::disable_lightning_connections`] is set to `true`. pub fn connect( &self, node_id: PublicKey, address: SocketAddress, persist: bool, ) -> Result<(), Error> { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index a48f38cc04..4e9081923a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -52,6 +52,7 @@ use ldk_node::payment::{ use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType, UserChannelId}; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::ln::channelmanager::{PaymentId, BREAKDOWN_TIMEOUT}; +use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; @@ -2914,6 +2915,113 @@ async fn do_connection_restart_behavior(persist: bool) { } } +#[cfg(feature = "storage-sqlite")] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn connection_restart_with_lightning_connections_disabled() { + for (listening_addresses, use_builder_setter) in [(None, true), (Some(Vec::new()), false)] { + do_connection_restart_with_lightning_connections_disabled( + listening_addresses, + use_builder_setter, + ) + .await; + } +} + +#[cfg(feature = "storage-sqlite")] +async fn do_connection_restart_with_lightning_connections_disabled( + listening_addresses: Option>, use_builder_setter: bool, +) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let mut config_a = random_config(); + config_a.store_type = TestStoreType::Sqlite; + let node_a = setup_node(&chain_source, config_a.clone()); + let node_b = setup_node(&chain_source, random_config()); + let node_id_a = node_a.node_id(); + let node_id_b = node_b.node_id(); + let node_addrs_a = node_a.listening_addresses().unwrap(); + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + + // Persist the outbound peer while connections are still enabled. + assert!(!node_a.config().disable_lightning_connections); + node_a.connect(node_id_b, node_addr_b.clone(), true).unwrap(); + let peers = node_a.list_peers(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].node_id, node_id_b); + assert!(peers[0].is_persisted); + assert!(peers[0].is_connected); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); + drop(node_a); + + // Rebuild from the same storage and entropy with Lightning connections disabled. Node B + // did not persist its inbound peer, so it cannot initiate a reconnect of its own. + node_b.start().unwrap(); + assert!(node_b.list_peers().is_empty()); + config_a.node_config.listening_addresses = listening_addresses; + config_a.node_config.node_alias = None; + config_a.node_config.disable_lightning_connections = !use_builder_setter; + let logger = Arc::new(CollectingLogWriter::new()); + setup_builder!(builder, config_a.node_config); + configure_chain_source(&chain_source, &mut builder, &config_a); + builder.set_custom_logger(logger.clone()); + if use_builder_setter { + builder.set_disable_lightning_connections(true); + } + let node_a = builder.build(config_a.node_entropy.into()).unwrap(); + assert!(node_a.config().disable_lightning_connections); + node_a.start().unwrap(); + assert_eq!(node_a.node_id(), node_id_a); + + // Chain access remains available while Lightning peer networking is disabled. + node_a.sync_wallets().unwrap(); + + // Wait for the background reconnect to hit the gate, so a slow task cannot make the + // disconnected assertions pass without actually exercising reconnection. + assert!(logger.wait_for("Lightning peer connections are disabled.").await); + let peers = node_a.list_peers(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].node_id, node_id_b); + assert_eq!(peers[0].address, node_addr_b); + assert!(peers[0].is_persisted); + assert!(!peers[0].is_connected); + assert!(node_b.list_peers().is_empty()); + + assert_eq!(node_a.connect(node_id_b, node_addr_b, true), Err(NodeError::ConnectionFailed)); + + // The peer cannot reconnect inbound on any of the node's previous listening addresses. + for node_addr_a in node_addrs_a { + assert_eq!(node_b.connect(node_id_a, node_addr_a, false), Err(NodeError::ConnectionFailed)); + } + assert!(!node_a.list_peers()[0].is_connected); + assert!(node_b.list_peers().is_empty()); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn connection_build_rejects_inbound_when_lightning_connections_disabled() { + for use_builder_setter in [false, true] { + let mut config = random_config(); + assert!(!config.node_config.listening_addresses.as_ref().unwrap().is_empty()); + config.node_config.disable_lightning_connections = !use_builder_setter; + let builder = Builder::from_config(config.node_config); + #[cfg(not(feature = "uniffi"))] + let mut builder = builder; + // Configuration rejection must not depend on reaching a chain source. + builder.set_chain_source_esplora("http://127.0.0.1:0".to_string(), None); + if use_builder_setter { + builder.set_disable_lightning_connections(true); + } + assert!(matches!( + builder.build_with_store(config.node_entropy.into(), InMemoryStore::new()), + Err(BuildError::InvalidListeningAddresses) + )); + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn concurrent_connections_succeed() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();