diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index 23763b765..8ee97a12a 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -72,6 +72,14 @@ pub struct Config { /// their address books. pub listen_addr: SocketAddr, + /// The external address of this node if any. + /// + /// Zebra bind to `listen_addr` but this can be an internal address if the node + /// is behind a firewall, load balancer or NAT. This field can be used to + /// advertise a different address to peers making it possible to receive inbound + /// connections and contribute to the P2P network from behind a firewall, load balancer, or NAT. + pub external_addr: Option, + /// The network to connect to. pub network: Network, @@ -601,6 +609,7 @@ impl Default for Config { listen_addr: "0.0.0.0:8233" .parse() .expect("Hardcoded address should be parseable"), + external_addr: None, network: Network::Mainnet, initial_mainnet_peers: mainnet_peers, initial_testnet_peers: testnet_peers, @@ -635,6 +644,7 @@ impl<'de> Deserialize<'de> for Config { #[serde(deny_unknown_fields, default)] struct DConfig { listen_addr: String, + external_addr: Option, network: NetworkKind, testnet_parameters: Option, initial_mainnet_peers: IndexSet, @@ -651,6 +661,7 @@ impl<'de> Deserialize<'de> for Config { let config = Config::default(); Self { listen_addr: "0.0.0.0".to_string(), + external_addr: None, network: Default::default(), testnet_parameters: None, initial_mainnet_peers: config.initial_mainnet_peers, @@ -665,6 +676,7 @@ impl<'de> Deserialize<'de> for Config { let DConfig { listen_addr, + external_addr, network: network_kind, testnet_parameters, initial_mainnet_peers, @@ -737,6 +749,20 @@ impl<'de> Deserialize<'de> for Config { }, }?; + let external_socket_addr = if let Some(address) = &external_addr { + match address.parse::() { + Ok(socket) => Ok(Some(socket)), + Err(_) => match address.parse::() { + Ok(ip) => Ok(Some(SocketAddr::new(ip, network.default_port()))), + Err(err) => Err(de::Error::custom(format!( + "{err}; Hint: addresses can be a IPv4, IPv6 (with brackets), or a DNS name, the port is optional" + ))), + }, + }? + } else { + None + }; + let [max_connections_per_ip, peerset_initial_target_size] = [ ("max_connections_per_ip", max_connections_per_ip, DEFAULT_MAX_CONNS_PER_IP), // If we want Zebra to operate with no network, @@ -756,6 +782,7 @@ impl<'de> Deserialize<'de> for Config { Ok(Config { listen_addr: canonical_socket_addr(listen_addr), + external_addr: external_socket_addr, network, initial_mainnet_peers, initial_testnet_peers, diff --git a/zebra-network/src/peer/handshake.rs b/zebra-network/src/peer/handshake.rs index 963b49086..c071cae98 100644 --- a/zebra-network/src/peer/handshake.rs +++ b/zebra-network/src/peer/handshake.rs @@ -654,7 +654,17 @@ where let their_addr = connected_addr .get_transient_addr() .expect("non-Isolated connections have a remote addr"); - (their_addr, our_services, config.listen_addr) + + // Include the configured external address in our version message, if any, otherwise, include our listen address. + let advertise_addr = match config.external_addr { + Some(external_addr) => { + info!(?their_addr, ?config.listen_addr, "using external address for Version messages"); + external_addr + } + None => config.listen_addr, + }; + + (their_addr, our_services, advertise_addr) } }; diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index d8e2edf6c..80d4386ad 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -184,7 +184,8 @@ use common::{ check::{is_zebrad_version, EphemeralCheck, EphemeralConfig}, config::random_known_rpc_port_config, config::{ - config_file_full_path, configs_dir, default_test_config, persistent_test_config, testdir, + config_file_full_path, configs_dir, default_test_config, external_address_test_config, + persistent_test_config, testdir, }, launch::{ spawn_zebrad_for_rpc, spawn_zebrad_without_rpc, ZebradTestDirExt, BETWEEN_NODES_DELAY, @@ -3128,6 +3129,33 @@ async fn validate_regtest_genesis_block() { ) } +/// Test that Version messages are sent with the external address when configured to do so. +#[test] +fn external_address() -> Result<()> { + let _init_guard = zebra_test::init(); + let testdir = testdir()?.with_config(&mut external_address_test_config(&Mainnet)?)?; + let mut child = testdir.spawn_child(args!["start"])?; + + // Give enough time to start connecting to some peers. + std::thread::sleep(Duration::from_secs(10)); + + child.kill(false)?; + + let output = child.wait_with_output()?; + let output = output.assert_failure()?; + + // Zebra started + output.stdout_line_contains("Starting zebrad")?; + + // Make sure we are using external address for Version messages. + output.stdout_line_contains("using external address for Version messages")?; + + // Make sure the command was killed. + output.assert_was_killed()?; + + Ok(()) +} + /// Test successful `getblocktemplate` and `submitblock` RPC calls on Regtest on Canopy. /// /// See [`common::regtest::submit_blocks`] for more information. diff --git a/zebrad/tests/common/config.rs b/zebrad/tests/common/config.rs index eac81c865..560e9d333 100644 --- a/zebrad/tests/common/config.rs +++ b/zebrad/tests/common/config.rs @@ -119,6 +119,12 @@ pub fn persistent_test_config(network: &Network) -> Result { Ok(config) } +pub fn external_address_test_config(network: &Network) -> Result { + let mut config = default_test_config(network)?; + config.network.external_addr = Some("127.0.0.1:0".parse()?); + Ok(config) +} + pub fn testdir() -> Result { tempfile::Builder::new() .prefix("zebrad_tests")