Skip to content

Commit efaac21

Browse files
authored
fix: reject empty hostname in peer address in FFI (#318)
* 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. * Drop step 1
1 parent f00fb2e commit efaac21

File tree

4 files changed

+48
-30
lines changed

4 files changed

+48
-30
lines changed

dash-spv-ffi/FFI_API.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Functions: 27
3939
| Function | Description | Module |
4040
|----------|-------------|--------|
4141
| `dash_spv_ffi_client_update_config` | Update the running client's configuration | client |
42-
| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts either a full socket... | config |
42+
| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts socket addresses with or... | config |
4343
| `dash_spv_ffi_config_destroy` | Destroys an FFIClientConfig and frees its memory # Safety - `config` must... | config |
4444
| `dash_spv_ffi_config_get_data_dir` | Gets the data directory path from the configuration # Safety - `config`... | config |
4545
| `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,)
254254
```
255255

256256
**Description:**
257-
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
257+
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
258258

259259
**Safety:**
260260
- `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

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -609,9 +609,14 @@ int32_t dash_spv_ffi_config_set_max_peers(struct FFIClientConfig *config,
609609
/**
610610
* Adds a peer address to the configuration
611611
*
612-
* Accepts either a full socket address (e.g., `192.168.1.1:9999` or `[::1]:19999`)
613-
* or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only
614-
* string is given, the default P2P port for the configured network is used.
612+
* Accepts socket addresses with or without port. When no port is specified,
613+
* the default P2P port for the configured network is used.
614+
*
615+
* Supported formats:
616+
* - IP with port: `192.168.1.1:9999`, `[::1]:19999`
617+
* - IP without port: `127.0.0.1`, `2001:db8::1`
618+
* - Hostname with port: `node.example.com:9999`
619+
* - Hostname without port: `node.example.com`
615620
*
616621
* # Safety
617622
* - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet

dash-spv-ffi/src/config.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,14 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_max_peers(
125125

126126
/// Adds a peer address to the configuration
127127
///
128-
/// Accepts either a full socket address (e.g., `192.168.1.1:9999` or `[::1]:19999`)
129-
/// or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only
130-
/// string is given, the default P2P port for the configured network is used.
128+
/// Accepts socket addresses with or without port. When no port is specified,
129+
/// the default P2P port for the configured network is used.
130+
///
131+
/// Supported formats:
132+
/// - IP with port: `192.168.1.1:9999`, `[::1]:19999`
133+
/// - IP without port: `127.0.0.1`, `2001:db8::1`
134+
/// - Hostname with port: `node.example.com:9999`
135+
/// - Hostname without port: `node.example.com`
131136
///
132137
/// # Safety
133138
/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
@@ -158,26 +163,26 @@ pub unsafe extern "C" fn dash_spv_ffi_config_add_peer(
158163
}
159164
};
160165

161-
// 1) Try parsing as full SocketAddr first (handles IPv6 [::1]:port forms)
162-
if let Ok(sock) = addr_str.parse::<SocketAddr>() {
163-
cfg.peers.push(sock);
164-
return FFIErrorCode::Success as i32;
165-
}
166-
167-
// 2) If that fails, try parsing as bare IP address and apply default port
166+
// Try parsing as bare IP address and apply default port
168167
if let Ok(ip) = addr_str.parse::<IpAddr>() {
169168
let sock = SocketAddr::new(ip, default_port);
170169
cfg.peers.push(sock);
171170
return FFIErrorCode::Success as i32;
172171
}
173172

174-
// 3) Optionally attempt DNS name with explicit port only; if no port, reject
175-
if !addr_str.contains(':') {
176-
set_last_error("Missing port for hostname; supply 'host:port' or IP only");
173+
// If not, must be a hostname - reject empty or missing hostname
174+
if addr_str.is_empty() || addr_str.starts_with(':') {
175+
set_last_error("Empty or missing hostname");
177176
return FFIErrorCode::InvalidArgument as i32;
178177
}
179178

180-
match addr_str.to_socket_addrs() {
179+
let addr_with_port = if addr_str.contains(':') {
180+
addr_str.to_string()
181+
} else {
182+
format!("{}:{}", addr_str, default_port)
183+
};
184+
185+
match addr_with_port.to_socket_addrs() {
181186
Ok(mut iter) => match iter.next() {
182187
Some(sock) => {
183188
cfg.peers.push(sock);

dash-spv-ffi/tests/unit/test_configuration.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,26 @@ mod tests {
5454

5555
// Test various invalid addresses
5656
let invalid_addrs = [
57-
"not-an-ip:9999",
58-
"256.256.256.256:9999",
59-
"127.0.0.1:99999", // port too high
60-
"127.0.0.1:-1", // negative port
61-
"localhost", // hostname without port should be rejected
62-
":9999", // missing IP
63-
":::", // invalid IPv6
64-
"localhost:abc", // non-numeric port
57+
"", // empty string
58+
"256.256.256.256:9999", // invalid IP octets
59+
"127.0.0.1:99999", // port too high
60+
"127.0.0.1:-1", // negative port
61+
":9999", // missing hostname
62+
"localhost:", // missing port
63+
":", // missing hostname and port
64+
":::", // invalid IPv6
65+
"localhost:abc", // non-numeric port
6566
];
6667

6768
for addr in &invalid_addrs {
6869
let c_addr = CString::new(*addr).unwrap();
6970
let result = dash_spv_ffi_config_add_peer(config, c_addr.as_ptr());
70-
assert_eq!(result, FFIErrorCode::InvalidArgument as i32);
71+
assert_eq!(
72+
result,
73+
FFIErrorCode::InvalidArgument as i32,
74+
"Expected '{}' to be invalid",
75+
addr
76+
);
7177

7278
// Check error message
7379
let error_ptr = dash_spv_ffi_get_last_error();
@@ -80,8 +86,10 @@ mod tests {
8086
"192.168.1.1:8333",
8187
"[::1]:9999",
8288
"[2001:db8::1]:8333",
83-
"127.0.0.1", // IP-only v4
84-
"2001:db8::1", // IP-only v6
89+
"127.0.0.1", // IP-only v4
90+
"2001:db8::1", // IP-only v6
91+
"localhost:9999", // Hostname with port
92+
"localhost", // Hostname without port (uses default)
8593
];
8694

8795
for addr in &valid_addrs {

0 commit comments

Comments
 (0)