Skip to content

Commit bc39a9a

Browse files
committed
fix(#18): use ip_echo to get public_ip and shred_version
Fix error re: non-blocking and using from_std on a std TcpListener by setting it non-blocking In debug mode: thread 'main' panicked at src/ip_echo.rs:128:32: Registering a blocking socket with the tokio runtime is unsupported. If you wish to do anyways, please add `--cfg tokio_allow_from_blocking_fd` to your RUSTFLAGS. See github.com/tokio-rs/tokio/issues/7172 for details.
1 parent 8d1bfe6 commit bc39a9a

File tree

4 files changed

+122
-19
lines changed

4 files changed

+122
-19
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Use `--config <path>` to specify a custom config file location. Default is `conf
8686
| `keypair_path` | `keypair.json` | Path to keypair file |
8787
| `listen_ip` | `0.0.0.0` | Local bind/listen IP |
8888
| `public_ip` | Auto (STUN) | Public IP address |
89+
| `enable_stun` | `false` | Use STUN to discover public IP instead of `ip_echo` |
8990
| `stun_server` | `stun.l.google.com:3478` | STUN server address |
9091
| `gossip_port` | `8001` | Gossip listen port |
9192
| `rpc_port` | `8899` | RPC listen port |
@@ -110,7 +111,22 @@ See [Solana Cluster Information](https://docs.anza.xyz/clusters/available) for t
110111
- TCP/UDP port 8001 (gossip) - if you need to be a publicly reachable gossip entrypoint (optional)
111112
- TCP port 8899 (RPC) - A publicly reachable RPC endpoint is required for validators to accept snapshots from you
112113

113-
**Note**: STUN-based IP detection and UPnP port forwarding are not recommended for production. Use explicit `public_ip` configuration instead, and configure port firewall/forwarding rules manually.
114+
**Note**: STUN-based IP detection and UPnP port forwarding are not recommended for production.
115+
Configure port firewall/forwarding rules manually. IP detection will be done via `ip_echo` to each entrypoint by default.
116+
117+
IP resolution preference order:
118+
1. `public_ip` from user config (if provided)
119+
2. IP echo result (if `public_ip` not provided)
120+
3. STUN result (only if IP echo fails and STUN is enabled)
121+
122+
Shred version resolution:
123+
1. If both configured and discovered versions exist and differ, it's an error
124+
2. Use whichever version is available (either configured or discovered)
125+
3. Return None if neither version is available
126+
127+
Note that even when `public_ip` is configured, IP echo is still attempted to get the shred version.
128+
129+
Explicit `public_ip` and `shred_version` configuration is always checked against `ip_echo` results.
114130

115131
## Known Issues
116132
- Agave validators refuse to download snapshots from us, even though we are publicly reachable [issue #20](https://github.com/Blockdaemon/agave-snapshot-gossip-client/issues/20)

example-config.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@
3030
# Gossip is hardcoded to 0.0.0.0 and can't be changed.
3131
#listen_ip = "127.0.0.1"
3232

33-
# Optional: Public IP address (default: use STUN)
33+
# Optional: Public IP address (default: use STUN if enabled, otherwise ip_echo to each entrypoint)
3434
#public_ip = "1.2.3.4"
3535

36+
# Optional: Enable STUN (default: false)
37+
# By default, use ip_echo and not STUN
38+
#enable_stun = true
39+
3640
# Optional: STUN server (default: stun.l.google.com:3478)
3741
#stun_server = "1.2.3.4:3478"
3842

src/config.rs

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::str::FromStr;
44

55
use dns_lookup::lookup_host;
66
use hyper::Uri;
7-
use log::{error, warn};
7+
use log::{error, info, warn};
88
use serde::Deserialize;
99

1010
use crate::constants::{
@@ -28,6 +28,7 @@ pub struct Config {
2828

2929
// What public IP to advertise, or how to discover it
3030
pub public_ip: Option<String>,
31+
pub enable_stun: Option<bool>,
3132
pub stun_server: Option<String>,
3233

3334
// What gossip and RPC ports to listen on and advertise
@@ -65,6 +66,7 @@ pub enum ConfigError {
6566
InvalidAddress(String),
6667
DnsLookupError(String),
6768
ParseError(String),
69+
IpEchoError(String),
6870
}
6971

7072
impl std::error::Error for ConfigError {}
@@ -76,6 +78,7 @@ impl std::fmt::Display for ConfigError {
7678
ConfigError::InvalidAddress(e) => write!(f, "Invalid address: {}", e),
7779
ConfigError::DnsLookupError(e) => write!(f, "DNS lookup error: {}", e),
7880
ConfigError::ParseError(e) => write!(f, "Parse error: {}", e),
81+
ConfigError::IpEchoError(e) => write!(f, "IP echo error: {}", e),
7982
}
8083
}
8184
}
@@ -123,19 +126,38 @@ impl Config {
123126
stun_client.get_public_ip(false).await
124127
}
125128

126-
pub async fn resolve(&self) -> Result<ResolvedConfig, ConfigError> {
127-
let public_ip = match &self.public_ip {
128-
Some(addr) => addr
129-
.parse()
130-
.map_err(|e| ConfigError::ParseError(format!("Invalid public address: {}", e)))?,
131-
None => {
132-
warn!("No public_addr in config, attempting to discover...");
133-
self.get_external_ip_with_stun()
134-
.await
135-
.map_err(ConfigError::StunError)?
129+
pub async fn ip_echo(&self, entrypoints: &[SocketAddr]) -> Result<(IpAddr, u16), ConfigError> {
130+
let mut discovered_ip = None;
131+
let mut discovered_shred_version = None;
132+
133+
// Try each entrypoint with IP echo client
134+
for entrypoint in entrypoints {
135+
let request = crate::ip_echo::IpEchoServerMessage::new(&[], &[]);
136+
info!("IP echo request to {}: {:?}", entrypoint, request);
137+
if let Ok((ip, shred_version)) =
138+
crate::ip_echo::ip_echo_client(*entrypoint, request).await
139+
{
140+
discovered_ip = Some(ip);
141+
discovered_shred_version = Some(shred_version);
142+
info!(
143+
"IP echo response from {}: {:?} {:?}",
144+
entrypoint, ip, shred_version
145+
);
146+
break;
136147
}
137-
};
148+
}
149+
Ok((
150+
discovered_ip.ok_or_else(|| {
151+
ConfigError::IpEchoError("Failed to discover public IP through IP echo".into())
152+
})?,
153+
discovered_shred_version.ok_or_else(|| {
154+
ConfigError::IpEchoError("Failed to discover shred version through IP echo".into())
155+
})?,
156+
))
157+
}
138158

159+
pub async fn resolve(&self) -> Result<ResolvedConfig, ConfigError> {
160+
// Resolve entrypoints first since we need them for both IP echo and final config
139161
let entrypoints = self
140162
.entrypoints
141163
.clone()
@@ -144,6 +166,63 @@ impl Config {
144166
.map(|addr| Self::parse_addr(&addr, DEFAULT_GOSSIP_PORT))
145167
.collect::<Result<Vec<_>, _>>()?;
146168

169+
let (public_ip, discovered_shred_version) = {
170+
// First try IP echo to get public ip and shred version at the same time
171+
let ip_echo_result = self.ip_echo(&entrypoints).await;
172+
173+
// If public_ip is configured, use that regardless of IP echo result
174+
// If we get ip echo result and user configured a public ip, compare them
175+
if let Some(addr) = &self.public_ip {
176+
let ip = addr.parse().map_err(|e| {
177+
ConfigError::ParseError(format!("Invalid public address: {}", e))
178+
})?;
179+
if let Ok((echo_ip, _)) = &ip_echo_result {
180+
if ip != *echo_ip {
181+
error!(
182+
"Configured public IP {} differs from IP echo result {}",
183+
ip, echo_ip
184+
);
185+
}
186+
}
187+
(ip, ip_echo_result.ok().map(|(_, version)| version))
188+
} else {
189+
match ip_echo_result {
190+
Ok((ip, shred_version)) => (ip, Some(shred_version)),
191+
Err(e) => {
192+
warn!("IP echo failed: {}", e);
193+
if self.enable_stun.unwrap_or(false) {
194+
(
195+
self.get_external_ip_with_stun()
196+
.await
197+
.map_err(ConfigError::StunError)?,
198+
None,
199+
)
200+
} else {
201+
return Err(ConfigError::IpEchoError(
202+
"Failed to discover public IP through IP echo and STUN is disabled"
203+
.into(),
204+
));
205+
}
206+
}
207+
}
208+
}
209+
};
210+
211+
// Validate and resolve shred version:
212+
// - Error if both versions exist and differ
213+
// - Use whichever version is available
214+
// - Return None if neither version is available
215+
let shred_version = match (discovered_shred_version, self.shred_version) {
216+
(Some(discovered), Some(configured)) if discovered != configured => {
217+
return Err(ConfigError::ParseError(format!(
218+
"Shred version mismatch: {} from ip echo, {} from config",
219+
discovered, configured
220+
)));
221+
}
222+
(Some(discovered), _) => Some(discovered),
223+
(_, configured) => configured,
224+
};
225+
147226
let storage_path = match self.storage_path.as_deref() {
148227
None => None,
149228
Some(s) => Some(Uri::from_str(s).map_err(|e| {
@@ -158,7 +237,7 @@ impl Config {
158237
.unwrap_or_else(|| DEFAULT_KEYPAIR_PATH.to_string()),
159238
entrypoints,
160239
expected_genesis_hash: self.expected_genesis_hash.clone(),
161-
shred_version: self.shred_version.clone(),
240+
shred_version,
162241
listen_ip: self
163242
.listen_ip
164243
.as_ref()
@@ -182,6 +261,7 @@ pub fn load_config(config_path: Option<&str>) -> Config {
182261
error!("Failed to parse {}: {}", path, e);
183262
std::process::exit(1);
184263
});
264+
config.enable_stun.get_or_insert(false);
185265
config.enable_upnp.get_or_insert(false);
186266
config.enable_proxy.get_or_insert(false);
187267
config
@@ -191,6 +271,7 @@ pub fn load_config(config_path: Option<&str>) -> Config {
191271
Config {
192272
keypair_path: None,
193273
entrypoints: None,
274+
enable_stun: None,
194275
stun_server: None,
195276
public_ip: None,
196277
enable_upnp: None,

src/ip_echo.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub struct IpEchoServerMessage {
2121
}
2222

2323
// IpEchoServerMessage is used by the client to send its port array to the server
24-
#[allow(dead_code)]
2524
impl IpEchoServerMessage {
2625
pub fn new(tcp_ports: &[u16], udp_ports: &[u16]) -> Self {
2726
let mut request = Self::default();
@@ -44,7 +43,6 @@ pub struct IpEchoServerResponse {
4443
shred_version: Option<u16>,
4544
}
4645

47-
#[allow(dead_code)]
4846
impl IpEchoServerResponse {
4947
pub fn new(address: IpAddr, shred_version: Option<u16>) -> Self {
5048
Self {
@@ -123,6 +121,9 @@ pub fn create_ip_echo_server(ip_echo: Option<TcpListener>, shred_version: u16) {
123121
tcp_listener.local_addr().unwrap()
124122
);
125123

124+
// Set non-blocking mode once before cloning
125+
tcp_listener.set_nonblocking(true).unwrap();
126+
126127
// Spawn multiple tasks in the existing runtime
127128
for _ in 0..DEFAULT_IP_ECHO_SERVER_THREADS {
128129
let tcp_listener = TokioTcpListener::from_std(tcp_listener.try_clone().unwrap())
@@ -145,7 +146,6 @@ pub fn create_ip_echo_server(ip_echo: Option<TcpListener>, shred_version: u16) {
145146
});
146147
}
147148

148-
#[allow(dead_code)]
149149
pub async fn ip_echo_client(
150150
addr: SocketAddr,
151151
request: IpEchoServerMessage,
@@ -326,7 +326,9 @@ mod tests {
326326
create_ip_echo_server(Some(tcp_listener.into_std().unwrap()), shred_version);
327327

328328
// Use our client to connect and get the response
329-
let (address, version) = ip_echo_client(server_addr, create_test_request()).await.unwrap();
329+
let (address, version) = ip_echo_client(server_addr, create_test_request())
330+
.await
331+
.unwrap();
330332

331333
// Verify the response
332334
assert_eq!(address, IpAddr::from([127, 0, 0, 1]));

0 commit comments

Comments
 (0)