Skip to content

Commit 37defe0

Browse files
committed
Allow binding to port 0 for OS-assigned ports
Add support for configuring listening addresses with port 0, letting the OS pick a free port. After binding, the actual port is resolved via local_addr() and stored in two new fields on Node: - active_listening_addresses: set on start(), cleared on stop() - last_bound_addresses: set on start(), preserved across restarts so the node rebinds the same ports (important for peer reconnection with persisted addresses) The listening_addresses() and announcement_addresses() getters return the active addresses when running, last bound when stopped, and fall back to configured addresses if the node was never started. The gossip broadcast task also prefers active addresses over config. This eliminates the need for the deterministic port picker in tests, which was fragile due to potential port collisions. Tests now use 127.0.0.1:0 and query the actual port after start(). AI tools were used in preparing this commit.
1 parent 3aef2b3 commit 37defe0

File tree

4 files changed

+71
-26
lines changed

4 files changed

+71
-26
lines changed

src/builder.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,6 +1988,8 @@ fn build_with_store_internal(
19881988
peer_store,
19891989
payment_store,
19901990
lnurl_auth,
1991+
active_listening_addresses: Arc::new(RwLock::new(None)),
1992+
last_bound_addresses: Arc::new(RwLock::new(None)),
19911993
is_running,
19921994
node_metrics,
19931995
om_mailbox,

src/lib.rs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ pub struct Node {
234234
peer_store: Arc<PeerStore<Arc<Logger>>>,
235235
payment_store: Arc<PaymentStore>,
236236
lnurl_auth: Arc<LnurlAuth>,
237+
/// Addresses the node is actively listening on. Set on `start()`, cleared on `stop()`.
238+
active_listening_addresses: Arc<RwLock<Option<Vec<SocketAddress>>>>,
239+
/// Addresses resolved from the last successful bind, used to rebind the same ports on
240+
/// restart when the configured addresses contain port 0.
241+
last_bound_addresses: Arc<RwLock<Option<Vec<SocketAddress>>>>,
237242
is_running: Arc<RwLock<bool>>,
238243
node_metrics: Arc<RwLock<NodeMetrics>>,
239244
om_mailbox: Option<Arc<OnionMessageMailbox>>,
@@ -356,7 +361,14 @@ impl Node {
356361
);
357362
}
358363

359-
if let Some(listening_addresses) = &self.config.listening_addresses {
364+
let effective_listening_addresses = self
365+
.last_bound_addresses
366+
.read()
367+
.unwrap()
368+
.clone()
369+
.or_else(|| self.config.listening_addresses.clone());
370+
371+
if let Some(listening_addresses) = &effective_listening_addresses {
360372
// Setup networking
361373
let peer_manager_connection_handler = Arc::clone(&self.peer_manager);
362374
let listening_logger = Arc::clone(&self.logger);
@@ -403,6 +415,36 @@ impl Node {
403415
Ok(listeners)
404416
})?;
405417

418+
// Store the actual bound addresses (resolving port 0 to real ports).
419+
let mut bound_addrs = Vec::with_capacity(listeners.len());
420+
for listener in &listeners {
421+
let local_addr = listener.local_addr().map_err(|e| {
422+
log_error!(
423+
self.logger,
424+
"Failed to retrieve local address from listener: {}",
425+
e
426+
);
427+
Error::InvalidSocketAddress
428+
})?;
429+
let socket_address = match local_addr {
430+
std::net::SocketAddr::V4(a) => {
431+
SocketAddress::TcpIpV4 { addr: a.ip().octets(), port: a.port() }
432+
},
433+
std::net::SocketAddr::V6(a) => {
434+
SocketAddress::TcpIpV6 { addr: a.ip().octets(), port: a.port() }
435+
},
436+
};
437+
bound_addrs.push(socket_address);
438+
}
439+
440+
log_info!(
441+
self.logger,
442+
"Listening on {}",
443+
bound_addrs.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(", ")
444+
);
445+
*self.active_listening_addresses.write().unwrap() = Some(bound_addrs.clone());
446+
*self.last_bound_addresses.write().unwrap() = Some(bound_addrs);
447+
406448
for listener in listeners {
407449
let logger = Arc::clone(&listening_logger);
408450
let peer_mgr = Arc::clone(&peer_manager_connection_handler);
@@ -478,6 +520,7 @@ impl Node {
478520
let bcast_store = Arc::clone(&self.kv_store);
479521
let bcast_logger = Arc::clone(&self.logger);
480522
let bcast_node_metrics = Arc::clone(&self.node_metrics);
523+
let bcast_active_addrs = Arc::clone(&self.active_listening_addresses);
481524
let mut stop_bcast = self.stop_sender.subscribe();
482525
let node_alias = self.config.node_alias.clone();
483526
if may_announce_channel(&self.config).is_ok() {
@@ -525,6 +568,8 @@ impl Node {
525568

526569
let addresses = if let Some(announcement_addresses) = bcast_config.announcement_addresses.clone() {
527570
announcement_addresses
571+
} else if let Some(bound_addrs) = bcast_active_addrs.read().unwrap().clone() {
572+
bound_addrs
528573
} else if let Some(listening_addresses) = bcast_config.listening_addresses.clone() {
529574
listening_addresses
530575
} else {
@@ -740,6 +785,7 @@ impl Node {
740785
#[cfg(tokio_unstable)]
741786
self.runtime.log_metrics();
742787

788+
*self.active_listening_addresses.write().unwrap() = None;
743789
log_info!(self.logger, "Shutdown complete.");
744790
*is_running_lock = false;
745791
Ok(())
@@ -842,16 +888,22 @@ impl Node {
842888
}
843889

844890
/// Returns our own listening addresses.
891+
///
892+
/// If the node has been started, this returns the actual bound addresses (which may differ
893+
/// from the configured addresses if port 0 was used). Otherwise, this returns the configured
894+
/// addresses.
845895
pub fn listening_addresses(&self) -> Option<Vec<SocketAddress>> {
846-
self.config.listening_addresses.clone()
896+
self.active_listening_addresses
897+
.read()
898+
.unwrap()
899+
.clone()
900+
.or_else(|| self.last_bound_addresses.read().unwrap().clone())
901+
.or_else(|| self.config.listening_addresses.clone())
847902
}
848903

849904
/// Returns the addresses that the node will announce to the network.
850905
pub fn announcement_addresses(&self) -> Option<Vec<SocketAddress>> {
851-
self.config
852-
.announcement_addresses
853-
.clone()
854-
.or_else(|| self.config.listening_addresses.clone())
906+
self.config.announcement_addresses.clone().or_else(|| self.listening_addresses())
855907
}
856908

857909
/// Returns our node alias.

tests/common/mod.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use std::collections::{HashMap, HashSet};
1414
use std::env;
1515
use std::future::Future;
1616
use std::path::PathBuf;
17-
use std::sync::atomic::{AtomicU16, Ordering};
1817
use std::sync::{Arc, RwLock};
1918
use std::time::Duration;
2019

@@ -269,16 +268,6 @@ pub(crate) fn random_storage_path() -> PathBuf {
269268
temp_path
270269
}
271270

272-
static NEXT_PORT: AtomicU16 = AtomicU16::new(20000);
273-
274-
pub(crate) fn generate_listening_addresses() -> Vec<SocketAddress> {
275-
let port = NEXT_PORT.fetch_add(2, Ordering::Relaxed);
276-
vec![
277-
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port },
278-
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: port + 1 },
279-
]
280-
}
281-
282271
pub(crate) fn random_node_alias() -> Option<NodeAlias> {
283272
let mut rng = rng();
284273
let rand_val = rng.random_range(0..1000);
@@ -302,7 +291,7 @@ pub(crate) fn random_config(anchor_channels: bool) -> TestConfig {
302291
println!("Setting random LDK storage dir: {}", rand_dir.display());
303292
node_config.storage_dir_path = rand_dir.to_str().unwrap().to_owned();
304293

305-
let listening_addresses = generate_listening_addresses();
294+
let listening_addresses = vec![SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 0 }];
306295
println!("Setting LDK listening addresses: {:?}", listening_addresses);
307296
node_config.listening_addresses = Some(listening_addresses);
308297

tests/integration_tests_rust.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ use common::{
2121
expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events,
2222
expect_event, expect_payment_claimable_event, expect_payment_received_event,
2323
expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait,
24-
generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all,
25-
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
26-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all,
27-
wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
24+
open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds,
25+
premine_blocks, prepare_rbf, random_chain_source, random_config, setup_bitcoind_and_electrsd,
26+
setup_builder, setup_node, setup_two_nodes, splice_in_with_all, wait_for_tx, TestChainSource,
27+
TestStoreType, TestSyncStore,
2828
};
2929
use electrsd::corepc_node::Node as BitcoinD;
3030
use electrsd::ElectrsD;
@@ -37,6 +37,7 @@ use ldk_node::payment::{
3737
};
3838
use ldk_node::{Builder, Event, NodeError};
3939
use lightning::ln::channelmanager::PaymentId;
40+
use lightning::ln::msgs::SocketAddress;
4041
use lightning::routing::gossip::{NodeAlias, NodeId};
4142
use lightning::routing::router::RouteParametersConfig;
4243
use lightning_invoice::{Bolt11InvoiceDescription, Description};
@@ -1431,9 +1432,11 @@ async fn test_node_announcement_propagation() {
14311432
node_a_alias_bytes[..node_a_alias_string.as_bytes().len()]
14321433
.copy_from_slice(node_a_alias_string.as_bytes());
14331434
let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes));
1434-
let node_a_announcement_addresses = generate_listening_addresses();
1435+
let node_a_announcement_addresses = vec![
1436+
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 10001 },
1437+
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 10002 },
1438+
];
14351439
config_a.node_config.node_alias = node_a_node_alias.clone();
1436-
config_a.node_config.listening_addresses = Some(generate_listening_addresses());
14371440
config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone());
14381441

14391442
// Node B will only use listening addresses
@@ -1443,9 +1446,7 @@ async fn test_node_announcement_propagation() {
14431446
node_b_alias_bytes[..node_b_alias_string.as_bytes().len()]
14441447
.copy_from_slice(node_b_alias_string.as_bytes());
14451448
let node_b_node_alias = Some(NodeAlias(node_b_alias_bytes));
1446-
let node_b_listening_addresses = generate_listening_addresses();
14471449
config_b.node_config.node_alias = node_b_node_alias.clone();
1448-
config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone());
14491450
config_b.node_config.announcement_addresses = None;
14501451

14511452
let node_a = setup_node(&chain_source, config_a);
@@ -1505,6 +1506,7 @@ async fn test_node_announcement_propagation() {
15051506
#[cfg(feature = "uniffi")]
15061507
assert_eq!(node_b_announcement_info.alias, node_b_alias_string);
15071508

1509+
let node_b_listening_addresses = node_b.listening_addresses().unwrap();
15081510
#[cfg(not(feature = "uniffi"))]
15091511
assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses);
15101512
#[cfg(feature = "uniffi")]

0 commit comments

Comments
 (0)