From c669d1a8f16f8c14861d21c947522585f0747745 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 19 Dec 2025 01:43:09 +1100 Subject: [PATCH 1/2] fix: reject empty hostname in peer address in FFI Fix a bug in Windows discovered in CI overhaul PR #253 where `:9999` was being accepted as a valid peer address on Windows due to DNS resolution differences. --- dash-spv-ffi/FFI_API.md | 4 +-- dash-spv-ffi/include/dash_spv_ffi.h | 11 +++++-- dash-spv-ffi/src/config.rs | 25 +++++++++++----- dash-spv-ffi/tests/unit/test_configuration.rs | 30 ++++++++++++------- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index 2f156c0a8..4ba4a46e5 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -39,7 +39,7 @@ Functions: 27 | Function | Description | Module | |----------|-------------|--------| | `dash_spv_ffi_client_update_config` | Update the running client's configuration | client | -| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts either a full socket... | config | +| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts socket addresses with or... | config | | `dash_spv_ffi_config_destroy` | Destroys an FFIClientConfig and frees its memory # Safety - `config` must... | config | | `dash_spv_ffi_config_get_data_dir` | Gets the data directory path from the configuration # Safety - `config`... | config | | `dash_spv_ffi_config_get_mempool_strategy` | Gets the mempool synchronization strategy # Safety - `config` must be a... | config | @@ -254,7 +254,7 @@ dash_spv_ffi_config_add_peer(config: *mut FFIClientConfig, addr: *const c_char,) ``` **Description:** -Adds a peer address to the configuration Accepts either a full socket address (e.g., `192.168.1.1:9999` or `[::1]:19999`) or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only string is given, the default P2P port for the configured network is used. # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call +Adds a peer address to the configuration Accepts socket addresses with or without port. When no port is specified, the default P2P port for the configured network is used. Supported formats: - IP with port: `192.168.1.1:9999`, `[::1]:19999` - IP without port: `127.0.0.1`, `2001:db8::1` - Hostname with port: `node.example.com:9999` - Hostname without port: `node.example.com` # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call **Safety:** - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index cf4b26613..3b56d3342 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -609,9 +609,14 @@ int32_t dash_spv_ffi_config_set_max_peers(struct FFIClientConfig *config, /** * Adds a peer address to the configuration * - * Accepts either a full socket address (e.g., `192.168.1.1:9999` or `[::1]:19999`) - * or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only - * string is given, the default P2P port for the configured network is used. + * Accepts socket addresses with or without port. When no port is specified, + * the default P2P port for the configured network is used. + * + * Supported formats: + * - IP with port: `192.168.1.1:9999`, `[::1]:19999` + * - IP without port: `127.0.0.1`, `2001:db8::1` + * - Hostname with port: `node.example.com:9999` + * - Hostname without port: `node.example.com` * * # Safety * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet diff --git a/dash-spv-ffi/src/config.rs b/dash-spv-ffi/src/config.rs index 702c72421..e1be5c590 100644 --- a/dash-spv-ffi/src/config.rs +++ b/dash-spv-ffi/src/config.rs @@ -125,9 +125,14 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_max_peers( /// Adds a peer address to the configuration /// -/// Accepts either a full socket address (e.g., `192.168.1.1:9999` or `[::1]:19999`) -/// or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only -/// string is given, the default P2P port for the configured network is used. +/// Accepts socket addresses with or without port. When no port is specified, +/// the default P2P port for the configured network is used. +/// +/// Supported formats: +/// - IP with port: `192.168.1.1:9999`, `[::1]:19999` +/// - IP without port: `127.0.0.1`, `2001:db8::1` +/// - Hostname with port: `node.example.com:9999` +/// - Hostname without port: `node.example.com` /// /// # Safety /// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet @@ -171,13 +176,19 @@ pub unsafe extern "C" fn dash_spv_ffi_config_add_peer( return FFIErrorCode::Success as i32; } - // 3) Optionally attempt DNS name with explicit port only; if no port, reject - if !addr_str.contains(':') { - set_last_error("Missing port for hostname; supply 'host:port' or IP only"); + // 3) Must be a hostname - reject empty or missing hostname + if addr_str.is_empty() || addr_str.starts_with(':') { + set_last_error("Empty or missing hostname"); return FFIErrorCode::InvalidArgument as i32; } - match addr_str.to_socket_addrs() { + let addr_with_port = if addr_str.contains(':') { + addr_str.to_string() + } else { + format!("{}:{}", addr_str, default_port) + }; + + match addr_with_port.to_socket_addrs() { Ok(mut iter) => match iter.next() { Some(sock) => { cfg.peers.push(sock); diff --git a/dash-spv-ffi/tests/unit/test_configuration.rs b/dash-spv-ffi/tests/unit/test_configuration.rs index 47916532c..695dc3d0b 100644 --- a/dash-spv-ffi/tests/unit/test_configuration.rs +++ b/dash-spv-ffi/tests/unit/test_configuration.rs @@ -54,20 +54,26 @@ mod tests { // Test various invalid addresses let invalid_addrs = [ - "not-an-ip:9999", - "256.256.256.256:9999", - "127.0.0.1:99999", // port too high - "127.0.0.1:-1", // negative port - "localhost", // hostname without port should be rejected - ":9999", // missing IP - ":::", // invalid IPv6 - "localhost:abc", // non-numeric port + "", // empty string + "256.256.256.256:9999", // invalid IP octets + "127.0.0.1:99999", // port too high + "127.0.0.1:-1", // negative port + ":9999", // missing hostname + "localhost:", // missing port + ":", // missing hostname and port + ":::", // invalid IPv6 + "localhost:abc", // non-numeric port ]; for addr in &invalid_addrs { let c_addr = CString::new(*addr).unwrap(); let result = dash_spv_ffi_config_add_peer(config, c_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); + assert_eq!( + result, + FFIErrorCode::InvalidArgument as i32, + "Expected '{}' to be invalid", + addr + ); // Check error message let error_ptr = dash_spv_ffi_get_last_error(); @@ -80,8 +86,10 @@ mod tests { "192.168.1.1:8333", "[::1]:9999", "[2001:db8::1]:8333", - "127.0.0.1", // IP-only v4 - "2001:db8::1", // IP-only v6 + "127.0.0.1", // IP-only v4 + "2001:db8::1", // IP-only v6 + "localhost:9999", // Hostname with port + "localhost", // Hostname without port (uses default) ]; for addr in &valid_addrs { From 27bdadbf7f5c28c7f5cb476b8b7465742d5047ac Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 2 Jan 2026 16:58:19 +0100 Subject: [PATCH 2/2] Drop step 1 --- dash-spv-ffi/src/config.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dash-spv-ffi/src/config.rs b/dash-spv-ffi/src/config.rs index e1be5c590..ef7e35b56 100644 --- a/dash-spv-ffi/src/config.rs +++ b/dash-spv-ffi/src/config.rs @@ -163,20 +163,14 @@ pub unsafe extern "C" fn dash_spv_ffi_config_add_peer( } }; - // 1) Try parsing as full SocketAddr first (handles IPv6 [::1]:port forms) - if let Ok(sock) = addr_str.parse::() { - cfg.peers.push(sock); - return FFIErrorCode::Success as i32; - } - - // 2) If that fails, try parsing as bare IP address and apply default port + // Try parsing as bare IP address and apply default port if let Ok(ip) = addr_str.parse::() { let sock = SocketAddr::new(ip, default_port); cfg.peers.push(sock); return FFIErrorCode::Success as i32; } - // 3) Must be a hostname - reject empty or missing hostname + // If not, must be a hostname - reject empty or missing hostname if addr_str.is_empty() || addr_str.starts_with(':') { set_last_error("Empty or missing hostname"); return FFIErrorCode::InvalidArgument as i32;