Skip to content

Commit a9c2ea6

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 last_bound_addresses on Node, preserved across restarts so the node rebinds the same ports. Node::listening_addresses() returns the last bound addresses when available, falling back to configured addresses. The gossip broadcast task and announcement_addresses() prefer actual bound addresses over configured ones, so OS-assigned ports are correctly announced. Port 0 is only allowed under cfg(test). In production, the builder rejects it to prevent accidentally announcing ephemeral ports. Tests now use 127.0.0.1:0 instead of a deterministic port picker, eliminating potential port collisions between concurrent test runs. AI tools were used in preparing this commit.
1 parent 3aef2b3 commit a9c2ea6

File tree

5 files changed

+82
-31
lines changed

5 files changed

+82
-31
lines changed

src/builder.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,11 @@ impl NodeBuilder {
541541
return Err(BuildError::InvalidListeningAddresses);
542542
}
543543

544+
#[cfg(not(test))]
545+
if listening_addresses.iter().any(crate::config::has_port_zero) {
546+
return Err(BuildError::InvalidListeningAddresses);
547+
}
548+
544549
self.config.listening_addresses = Some(listening_addresses);
545550
Ok(self)
546551
}
@@ -1944,6 +1949,7 @@ fn build_with_store_internal(
19441949

19451950
let (stop_sender, _) = tokio::sync::watch::channel(());
19461951
let (background_processor_stop_sender, _) = tokio::sync::watch::channel(());
1952+
let last_bound_addresses: Arc<RwLock<Option<Vec<SocketAddress>>>> = Arc::new(RwLock::new(None));
19471953
let is_running = Arc::new(RwLock::new(false));
19481954

19491955
let pathfinding_scores_sync_url = pathfinding_scores_sync_config.map(|c| c.url.clone());
@@ -1988,6 +1994,7 @@ fn build_with_store_internal(
19881994
peer_store,
19891995
payment_store,
19901996
lnurl_auth,
1997+
last_bound_addresses,
19911998
is_running,
19921999
node_metrics,
19932000
om_mailbox,

src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,17 @@ pub(crate) fn may_announce_channel(config: &Config) -> Result<(), AnnounceError>
331331
}
332332
}
333333

334+
#[cfg_attr(test, allow(dead_code))]
335+
pub(crate) fn has_port_zero(addr: &SocketAddress) -> bool {
336+
match addr {
337+
SocketAddress::TcpIpV4 { port, .. }
338+
| SocketAddress::TcpIpV6 { port, .. }
339+
| SocketAddress::OnionV3 { port, .. }
340+
| SocketAddress::Hostname { port, .. } => *port == 0,
341+
_ => false,
342+
}
343+
}
344+
334345
pub(crate) fn default_user_config(config: &Config) -> UserConfig {
335346
// Initialize the default config values.
336347
//

src/lib.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ pub struct Node {
234234
peer_store: Arc<PeerStore<Arc<Logger>>>,
235235
payment_store: Arc<PaymentStore>,
236236
lnurl_auth: Arc<LnurlAuth>,
237+
last_bound_addresses: Arc<RwLock<Option<Vec<SocketAddress>>>>,
237238
is_running: Arc<RwLock<bool>>,
238239
node_metrics: Arc<RwLock<NodeMetrics>>,
239240
om_mailbox: Option<Arc<OnionMessageMailbox>>,
@@ -356,7 +357,14 @@ impl Node {
356357
);
357358
}
358359

359-
if let Some(listening_addresses) = &self.config.listening_addresses {
360+
let effective_listening_addresses = self
361+
.last_bound_addresses
362+
.read()
363+
.unwrap()
364+
.clone()
365+
.or_else(|| self.config.listening_addresses.clone());
366+
367+
if let Some(listening_addresses) = &effective_listening_addresses {
360368
// Setup networking
361369
let peer_manager_connection_handler = Arc::clone(&self.peer_manager);
362370
let listening_logger = Arc::clone(&self.logger);
@@ -378,14 +386,31 @@ impl Node {
378386
}
379387

380388
let logger = Arc::clone(&listening_logger);
381-
let listeners = self.runtime.block_on(async move {
389+
let (listeners, bound_addrs) = self.runtime.block_on(async move {
382390
let mut listeners = Vec::new();
391+
let mut bound_addrs = Vec::new();
383392

384-
// Try to bind to all addresses
385393
for addr in &*bind_addrs {
386394
match tokio::net::TcpListener::bind(addr).await {
387395
Ok(listener) => {
388-
log_trace!(logger, "Listener bound to {}", addr);
396+
let local_addr = listener.local_addr().map_err(|e| {
397+
log_error!(
398+
logger,
399+
"Failed to retrieve local address from listener: {}",
400+
e
401+
);
402+
Error::InvalidSocketAddress
403+
})?;
404+
let socket_address = match local_addr {
405+
std::net::SocketAddr::V4(a) => {
406+
SocketAddress::TcpIpV4 { addr: a.ip().octets(), port: a.port() }
407+
},
408+
std::net::SocketAddr::V6(a) => {
409+
SocketAddress::TcpIpV6 { addr: a.ip().octets(), port: a.port() }
410+
},
411+
};
412+
log_info!(logger, "Listening on {}", socket_address);
413+
bound_addrs.push(socket_address);
389414
listeners.push(listener);
390415
},
391416
Err(e) => {
@@ -400,9 +425,11 @@ impl Node {
400425
}
401426
}
402427

403-
Ok(listeners)
428+
Ok((listeners, bound_addrs))
404429
})?;
405430

431+
*self.last_bound_addresses.write().unwrap() = Some(bound_addrs);
432+
406433
for listener in listeners {
407434
let logger = Arc::clone(&listening_logger);
408435
let peer_mgr = Arc::clone(&peer_manager_connection_handler);
@@ -475,6 +502,7 @@ impl Node {
475502
let bcast_cm = Arc::clone(&self.channel_manager);
476503
let bcast_pm = Arc::clone(&self.peer_manager);
477504
let bcast_config = Arc::clone(&self.config);
505+
let bcast_bound_addrs = Arc::clone(&self.last_bound_addresses);
478506
let bcast_store = Arc::clone(&self.kv_store);
479507
let bcast_logger = Arc::clone(&self.logger);
480508
let bcast_node_metrics = Arc::clone(&self.node_metrics);
@@ -525,6 +553,8 @@ impl Node {
525553

526554
let addresses = if let Some(announcement_addresses) = bcast_config.announcement_addresses.clone() {
527555
announcement_addresses
556+
} else if let Some(bound_addresses) = bcast_bound_addrs.read().unwrap().clone() {
557+
bound_addresses
528558
} else if let Some(listening_addresses) = bcast_config.listening_addresses.clone() {
529559
listening_addresses
530560
} else {
@@ -842,15 +872,28 @@ impl Node {
842872
}
843873

844874
/// Returns our own listening addresses.
875+
///
876+
/// If the node has been started, this returns the actual bound addresses (which may differ
877+
/// from the configured addresses if port 0 was used). Otherwise, this returns the configured
878+
/// addresses.
845879
pub fn listening_addresses(&self) -> Option<Vec<SocketAddress>> {
846-
self.config.listening_addresses.clone()
880+
self.last_bound_addresses
881+
.read()
882+
.unwrap()
883+
.clone()
884+
.or_else(|| self.config.listening_addresses.clone())
847885
}
848886

849887
/// Returns the addresses that the node will announce to the network.
888+
///
889+
/// Returns the configured announcement addresses if set, otherwise falls back to the
890+
/// actual bound addresses (which may differ from configured addresses if port 0 was used),
891+
/// or the configured listening addresses.
850892
pub fn announcement_addresses(&self) -> Option<Vec<SocketAddress>> {
851893
self.config
852894
.announcement_addresses
853895
.clone()
896+
.or_else(|| self.last_bound_addresses.read().unwrap().clone())
854897
.or_else(|| self.config.listening_addresses.clone())
855898
}
856899

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: 14 additions & 13 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};
@@ -1424,29 +1425,28 @@ async fn test_node_announcement_propagation() {
14241425
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
14251426
let chain_source = random_chain_source(&bitcoind, &electrsd);
14261427

1427-
// Node A will use both listening and announcement addresses
14281428
let mut config_a = random_config(true);
14291429
let node_a_alias_string = "ldk-node-a".to_string();
14301430
let mut node_a_alias_bytes = [0u8; 32];
14311431
node_a_alias_bytes[..node_a_alias_string.as_bytes().len()]
14321432
.copy_from_slice(node_a_alias_string.as_bytes());
14331433
let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes));
1434-
let node_a_announcement_addresses = generate_listening_addresses();
14351434
config_a.node_config.node_alias = node_a_node_alias.clone();
1436-
config_a.node_config.listening_addresses = Some(generate_listening_addresses());
1435+
// Set explicit announcement addresses to verify they take priority over bound addresses.
1436+
let node_a_announcement_addresses = vec![
1437+
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 10001 },
1438+
SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 10002 },
1439+
];
14371440
config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone());
14381441

1439-
// Node B will only use listening addresses
1442+
// Node B uses default config to verify that bound addresses are announced.
14401443
let mut config_b = random_config(true);
14411444
let node_b_alias_string = "ldk-node-b".to_string();
14421445
let mut node_b_alias_bytes = [0u8; 32];
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());
1449-
config_b.node_config.announcement_addresses = None;
14501450

14511451
let node_a = setup_node(&chain_source, config_a);
14521452
let node_b = setup_node(&chain_source, config_b);
@@ -1505,10 +1505,11 @@ async fn test_node_announcement_propagation() {
15051505
#[cfg(feature = "uniffi")]
15061506
assert_eq!(node_b_announcement_info.alias, node_b_alias_string);
15071507

1508+
let node_b_announcement_addresses = node_b.announcement_addresses().unwrap();
15081509
#[cfg(not(feature = "uniffi"))]
1509-
assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses);
1510+
assert_eq!(node_b_announcement_info.addresses(), &node_b_announcement_addresses);
15101511
#[cfg(feature = "uniffi")]
1511-
assert_eq!(node_b_announcement_info.addresses, node_b_listening_addresses);
1512+
assert_eq!(node_b_announcement_info.addresses, node_b_announcement_addresses);
15121513
}
15131514

15141515
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]

0 commit comments

Comments
 (0)