Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SocketAddress>,
Expand Down Expand Up @@ -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<SocketAddress>,
Expand Down Expand Up @@ -1531,6 +1556,16 @@ fn build_with_store_internal(
) -> Result<Node, BuildError> {
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);
Expand Down Expand Up @@ -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),
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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` | [] |
Expand Down Expand Up @@ -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<Vec<SocketAddress>>,
/// 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.
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 17 additions & 4 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -57,6 +57,7 @@ where
{
pending_connections: PendingConnections,
peer_manager: Arc<PeerManager>,
disable_lightning_connections: bool,
tor_proxy_config: Option<TorConfig>,
keys_manager: Arc<KeysManager>,
logger: L,
Expand All @@ -67,12 +68,19 @@ where
L::Target: LdkLogger,
{
pub(crate) fn new(
peer_manager: Arc<PeerManager>, tor_proxy_config: Option<TorConfig>,
keys_manager: Arc<KeysManager>, logger: L,
peer_manager: Arc<PeerManager>, disable_lightning_connections: bool,
tor_proxy_config: Option<TorConfig>, keys_manager: Arc<KeysManager>, 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(
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, wouldn't we still want to allow manual overrides? I.e. allow the user to selectively connect peers, e.g., to trigger the channel-reestablish recovery flow?

@tankyleo tankyleo Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the node that reconnects to peers to trigger the force-closes must not have the stale database, it must be launched from the seed alone. That node will be launched with outbound connections enabled.

If the node with the stale database tries to reconnect with the peers, the channel-reestablish flow causes the node to panic in rust-lightning. That is why we would like to add this disable_outbound_lightning_connections flag here, and also disable manual overrides.

See the full procedure I have in mind here: lightningdevkit/ldk-server#257 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I went further and now enforce that inbound connections are disabled as well, see the rationale in the commit message below.

&self, node_id: PublicKey, address: SocketAddress, persist: bool,
) -> Result<(), Error> {
Expand Down
108 changes: 108 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Vec<SocketAddress>>, 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();
Expand Down
Loading