'auto' mode for detect_address_changes

This commit is contained in:
Christien Rioux 2025-06-14 09:12:59 -04:00
parent b7d725ab12
commit 5e11f945ef
16 changed files with 197 additions and 88 deletions

View file

@ -10,6 +10,10 @@
- Add private route example
- Add `require_inbound_relay` option in VeilidConfig. Default is false, but if enabled, forces OutboundOnly/InboundRelay mode. Can be used as an extra layer of IP address obscurity for some threat models. (@neequ57)
- Fix crash when peer info has missing or unsupported node ids
- Add 'auto' mode for detect_address_changes
- veilid-server:
- Use `detect_address_changes: auto` by default
**Changed in Veilid 0.4.7**

View file

@ -94,7 +94,7 @@ core:
member_watch_limit: 8
max_watch_expiration_ms: 600000
upnp: true
detect_address_changes: true
detect_address_changes: auto
restricted_nat_retries: 0
tls:
certificate_path: '%CERTIFICATE_PATH%'

View file

@ -74,7 +74,7 @@ impl AddressCheck {
let (detect_address_changes, ip6_prefix_size, require_inbound_relay) =
registry.config().with(|c| {
(
c.network.detect_address_changes,
net.resolved_detect_address_changes(),
c.network.max_connections_per_ip6_prefix_size as usize,
c.network.privacy.require_inbound_relay,
)

View file

@ -91,6 +91,8 @@ struct NetworkInner {
dial_info_failure_count: BTreeMap<RoutingDomain, usize>,
/// if we need to redo the publicinternet network class
needs_update_network_class: bool,
/// result of resolving 'auto'/None detect_address_changes mode
resolved_detect_address_changes: bool,
/// the next time we are allowed to check for better dialinfo when we are OutboundOnly
next_outbound_only_dial_info_check: Timestamp,
/// join handles for all the low level network background tasks
@ -157,6 +159,7 @@ impl Network {
network_needs_restart: false,
dial_info_failure_count: BTreeMap::new(),
needs_update_network_class: false,
resolved_detect_address_changes: false,
next_outbound_only_dial_info_check: Timestamp::default(),
join_handles: Vec::new(),
stop_source: None,
@ -743,7 +746,8 @@ impl Network {
// Caution: this -must- happen first because we use unwrap() in last_network_state()
let network_state = self.make_network_state().await?;
{
// Resolve 'auto'/None config fo detect_address_changes
let resolved_detect_address_changes = {
let mut inner = self.inner.lock();
// Create the shutdown stopper
@ -751,7 +755,43 @@ impl Network {
// Store the first network state snapshot
inner.network_state = Some(network_state.clone());
}
// Process the detect_address_changes 'auto' mode
let detect_address_changes = self.config().with(|c| c.network.detect_address_changes);
if let Some(detect_address_changes) = detect_address_changes {
inner.resolved_detect_address_changes = detect_address_changes;
if inner.resolved_detect_address_changes {
veilid_log!(self info "Manually-enabled detection of address changes");
} else {
veilid_log!(self info "Manually-disabled detection of address changes");
}
} else {
// Check for publicly routable IPv4 and IPv6 addresses
let mut global_ipv4 = false;
let mut global_ipv6 = false;
for siaddr in network_state.stable_interface_addresses {
if Address::from_ip_addr(siaddr).is_global() {
match siaddr {
IpAddr::V4(_ipv4_addr) => {
global_ipv4 = true;
}
IpAddr::V6(_ipv6_addr) => {
global_ipv6 = true;
}
}
}
}
// If both ipv4 and ipv6 global addresses are present, turn off detect_address_changes otherwise turn it on
inner.resolved_detect_address_changes = !(global_ipv4 && global_ipv6);
if inner.resolved_detect_address_changes {
veilid_log!(self info "Auto-enabled detection of address changes: global_ipv4={}, global_ipv6={}", global_ipv4, global_ipv6);
} else {
veilid_log!(self info "Auto-disabled detection of address changes because this node has global IPv4 and IPv6 addresses");
}
}
inner.resolved_detect_address_changes
};
// Start editing routing table
let routing_table = self.routing_table();
@ -768,9 +808,10 @@ impl Network {
true,
);
let confirmed_public_internet = self
.config()
.with(|c| !c.network.detect_address_changes || c.network.privacy.require_inbound_relay);
let confirmed_public_internet = !resolved_detect_address_changes
|| self
.config()
.with(|c| c.network.privacy.require_inbound_relay);
editor_public_internet.setup_network(
network_state.protocol_config.outbound,
network_state.protocol_config.inbound,
@ -991,6 +1032,15 @@ impl Network {
self.inner.lock().needs_update_network_class
}
pub fn resolved_detect_address_changes(&self) -> bool {
let Ok(_guard) = self.startup_lock.enter() else {
veilid_log!(self debug "ignoring due to not started up");
return false;
};
self.inner.lock().resolved_detect_address_changes
}
pub fn trigger_update_network_class(&self, routing_domain: RoutingDomain) {
let Ok(_guard) = self.startup_lock.enter() else {
veilid_log!(self debug "ignoring due to not started up");

View file

@ -140,15 +140,13 @@ impl Network {
#[instrument(level = "trace", skip_all)]
pub(super) async fn bind_udp_protocol_handlers(&self) -> EyreResult<StartupDisposition> {
veilid_log!(self trace "UDP: binding protocol handlers");
let (listen_address, public_address, detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.udp.listen_address.clone(),
c.network.protocol.udp.public_address.clone(),
c.network.detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
let (listen_address, public_address, require_inbound_relay) = self.config().with(|c| {
(
c.network.protocol.udp.listen_address.clone(),
c.network.protocol.udp.public_address.clone(),
c.network.privacy.require_inbound_relay,
)
});
// Get the binding parameters from the user-specified listen address
let bind_set = self
@ -177,7 +175,10 @@ impl Network {
{
let mut inner = self.inner.lock();
if public_address.is_some() && !detect_address_changes && !require_inbound_relay {
if public_address.is_some()
&& !inner.resolved_detect_address_changes
&& !require_inbound_relay
{
inner.static_public_dial_info.insert(ProtocolType::UDP);
}
}
@ -193,11 +194,11 @@ impl Network {
) -> EyreResult<()> {
veilid_log!(self trace "UDP: registering dial info");
let (public_address, detect_address_changes, require_inbound_relay) =
let (public_address, resolved_detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.udp.public_address.clone(),
c.network.detect_address_changes,
self.inner.lock().resolved_detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
@ -251,7 +252,7 @@ impl Network {
for di in &local_dial_info_list {
// If the local interface address is global, then register global dial info
// if no other public address is specified
if !detect_address_changes
if !resolved_detect_address_changes
&& !require_inbound_relay
&& public_address.is_none()
&& di.address().is_global()
@ -269,15 +270,13 @@ impl Network {
#[instrument(level = "trace", skip_all)]
pub(super) async fn start_ws_listeners(&self) -> EyreResult<StartupDisposition> {
veilid_log!(self trace "WS: binding protocol handlers");
let (listen_address, url, detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.ws.listen_address.clone(),
c.network.protocol.ws.url.clone(),
c.network.detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
let (listen_address, url, require_inbound_relay) = self.config().with(|c| {
(
c.network.protocol.ws.listen_address.clone(),
c.network.protocol.ws.url.clone(),
c.network.privacy.require_inbound_relay,
)
});
// Get the binding parameters from the user-specified listen address
let bind_set = self
@ -309,7 +308,7 @@ impl Network {
{
let mut inner = self.inner.lock();
if url.is_some() && !detect_address_changes && !require_inbound_relay {
if url.is_some() && !inner.resolved_detect_address_changes && !require_inbound_relay {
inner.static_public_dial_info.insert(ProtocolType::WS);
}
}
@ -324,14 +323,15 @@ impl Network {
editor_local_network: &mut RoutingDomainEditorLocalNetwork<'_>,
) -> EyreResult<()> {
veilid_log!(self trace "WS: registering dial info");
let (url, path, detect_address_changes, require_inbound_relay) = self.config().with(|c| {
(
c.network.protocol.ws.url.clone(),
c.network.protocol.ws.path.clone(),
c.network.detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
let (url, path, resolved_detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.ws.url.clone(),
c.network.protocol.ws.path.clone(),
self.inner.lock().resolved_detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
let mut registered_addresses: HashSet<IpAddr> = HashSet::new();
@ -400,7 +400,7 @@ impl Network {
let local_di =
DialInfo::try_ws(*socket_address, local_url).wrap_err("try_ws failed")?;
if !detect_address_changes
if !resolved_detect_address_changes
&& !require_inbound_relay
&& url.is_none()
&& local_di.address().is_global()
@ -420,12 +420,12 @@ impl Network {
pub(super) async fn start_wss_listeners(&self) -> EyreResult<StartupDisposition> {
veilid_log!(self trace "WSS: binding protocol handlers");
let (listen_address, url, detect_address_changes, require_inbound_relay) =
let (listen_address, url, resolved_detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.wss.listen_address.clone(),
c.network.protocol.wss.url.clone(),
c.network.detect_address_changes,
self.inner.lock().resolved_detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
@ -461,7 +461,7 @@ impl Network {
{
let mut inner = self.inner.lock();
if url.is_some() && !detect_address_changes && !require_inbound_relay {
if url.is_some() && !resolved_detect_address_changes && !require_inbound_relay {
inner.static_public_dial_info.insert(ProtocolType::WSS);
}
}
@ -531,15 +531,19 @@ impl Network {
pub(super) async fn start_tcp_listeners(&self) -> EyreResult<StartupDisposition> {
veilid_log!(self trace "TCP: binding protocol handlers");
let (listen_address, public_address, detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.tcp.listen_address.clone(),
c.network.protocol.tcp.public_address.clone(),
c.network.detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
let (
listen_address,
public_address,
resolved_detect_address_changes,
require_inbound_relay,
) = self.config().with(|c| {
(
c.network.protocol.tcp.listen_address.clone(),
c.network.protocol.tcp.public_address.clone(),
self.inner.lock().resolved_detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
// Get the binding parameters from the user-specified listen address
let bind_set = self
@ -571,7 +575,10 @@ impl Network {
{
let mut inner = self.inner.lock();
if public_address.is_some() && !detect_address_changes && !require_inbound_relay {
if public_address.is_some()
&& !resolved_detect_address_changes
&& !require_inbound_relay
{
inner.static_public_dial_info.insert(ProtocolType::TCP);
}
}
@ -587,11 +594,11 @@ impl Network {
) -> EyreResult<()> {
veilid_log!(self trace "TCP: registering dialinfo");
let (public_address, detect_address_changes, require_inbound_relay) =
let (public_address, resolved_detect_address_changes, require_inbound_relay) =
self.config().with(|c| {
(
c.network.protocol.tcp.public_address.clone(),
c.network.detect_address_changes,
self.inner.lock().resolved_detect_address_changes,
c.network.privacy.require_inbound_relay,
)
});
@ -650,7 +657,7 @@ impl Network {
let di = DialInfo::tcp(*socket_address);
// Register global dial info if no public address is specified
if !detect_address_changes
if !resolved_detect_address_changes
&& !require_inbound_relay
&& public_address.is_none()
&& di.address().is_global()

View file

@ -87,14 +87,10 @@ impl Network {
return Ok(());
}
let (detect_address_changes, upnp, require_inbound_relay) = {
let (upnp, require_inbound_relay) = {
let config = self.network_manager().config();
let c = config.get();
(
c.network.detect_address_changes,
c.network.upnp,
c.network.privacy.require_inbound_relay,
)
(c.network.upnp, c.network.privacy.require_inbound_relay)
};
if require_inbound_relay {
@ -104,7 +100,7 @@ impl Network {
}
// If we need to figure out our network class, tick the task for it
if detect_address_changes {
if self.resolved_detect_address_changes() {
// Check our network interfaces to see if they have changed
self.network_interfaces_task.tick().await?;

View file

@ -500,6 +500,15 @@ impl Network {
false
}
pub fn resolved_detect_address_changes(&self) -> bool {
let Ok(_guard) = self.startup_lock.enter() else {
veilid_log!(self debug "ignoring due to not started up");
return false;
};
false
}
pub fn trigger_update_network_class(&self, _routing_domain: RoutingDomain) {
let Ok(_guard) = self.startup_lock.enter() else {
veilid_log!(self debug "ignoring due to not started up");

View file

@ -255,7 +255,7 @@ pub fn config_callback(key: String) -> ConfigCallbackReturn {
"network.dht.member_watch_limit" => Ok(Box::new(8u32)),
"network.dht.max_watch_expiration_ms" => Ok(Box::new(600_000u32)),
"network.upnp" => Ok(Box::new(false)),
"network.detect_address_changes" => Ok(Box::new(true)),
"network.detect_address_changes" => Ok(Box::new(Some(true))),
"network.restricted_nat_retries" => Ok(Box::new(0u32)),
"network.tls.certificate_path" => Ok(Box::new(get_certfile_path())),
"network.tls.private_key_path" => Ok(Box::new(get_keyfile_path())),
@ -400,7 +400,7 @@ pub fn test_config() {
);
assert!(!inner.network.upnp);
assert!(inner.network.detect_address_changes);
assert_eq!(inner.network.detect_address_changes, Some(true));
assert_eq!(inner.network.restricted_nat_retries, 0u32);
assert_eq!(inner.network.tls.certificate_path, get_certfile_path());
assert_eq!(inner.network.tls.private_key_path, get_keyfile_path());

View file

@ -228,7 +228,7 @@ pub fn fix_veilidconfig() -> VeilidConfig {
max_watch_expiration_ms: 22,
},
upnp: true,
detect_address_changes: false,
detect_address_changes: Some(false),
restricted_nat_retries: 10000,
tls: VeilidConfigTLS {
certificate_path: "/etc/ssl/certs/cert.pem".to_string(),

View file

@ -556,7 +556,7 @@ pub struct VeilidConfigNetwork {
pub rpc: VeilidConfigRPC,
pub dht: VeilidConfigDHT,
pub upnp: bool,
pub detect_address_changes: bool,
pub detect_address_changes: Option<bool>,
pub restricted_nat_retries: u32,
pub tls: VeilidConfigTLS,
pub application: VeilidConfigApplication,
@ -583,7 +583,7 @@ impl Default for VeilidConfigNetwork {
rpc: VeilidConfigRPC::default(),
dht: VeilidConfigDHT::default(),
upnp: true,
detect_address_changes: true,
detect_address_changes: Some(true),
restricted_nat_retries: 0,
tls: VeilidConfigTLS::default(),
application: VeilidConfigApplication::default(),

View file

@ -377,7 +377,7 @@ sealed class VeilidConfigNetwork with _$VeilidConfigNetwork {
required VeilidConfigRPC rpc,
required VeilidConfigDHT dht,
required bool upnp,
required bool detectAddressChanges,
required bool? detectAddressChanges,
required int restrictedNatRetries,
required VeilidConfigTLS tls,
required VeilidConfigApplication application,

View file

@ -6080,7 +6080,7 @@ mixin _$VeilidConfigNetwork implements DiagnosticableTreeMixin {
VeilidConfigRPC get rpc;
VeilidConfigDHT get dht;
bool get upnp;
bool get detectAddressChanges;
bool? get detectAddressChanges;
int get restrictedNatRetries;
VeilidConfigTLS get tls;
VeilidConfigApplication get application;
@ -6234,7 +6234,7 @@ abstract mixin class $VeilidConfigNetworkCopyWith<$Res> {
VeilidConfigRPC rpc,
VeilidConfigDHT dht,
bool upnp,
bool detectAddressChanges,
bool? detectAddressChanges,
int restrictedNatRetries,
VeilidConfigTLS tls,
VeilidConfigApplication application,
@ -6277,7 +6277,7 @@ class _$VeilidConfigNetworkCopyWithImpl<$Res>
Object? rpc = null,
Object? dht = null,
Object? upnp = null,
Object? detectAddressChanges = null,
Object? detectAddressChanges = freezed,
Object? restrictedNatRetries = null,
Object? tls = null,
Object? application = null,
@ -6338,10 +6338,10 @@ class _$VeilidConfigNetworkCopyWithImpl<$Res>
? _self.upnp
: upnp // ignore: cast_nullable_to_non_nullable
as bool,
detectAddressChanges: null == detectAddressChanges
detectAddressChanges: freezed == detectAddressChanges
? _self.detectAddressChanges
: detectAddressChanges // ignore: cast_nullable_to_non_nullable
as bool,
as bool?,
restrictedNatRetries: null == restrictedNatRetries
? _self.restrictedNatRetries
: restrictedNatRetries // ignore: cast_nullable_to_non_nullable
@ -6496,7 +6496,7 @@ class _VeilidConfigNetwork
@override
final bool upnp;
@override
final bool detectAddressChanges;
final bool? detectAddressChanges;
@override
final int restrictedNatRetries;
@override
@ -6663,7 +6663,7 @@ abstract mixin class _$VeilidConfigNetworkCopyWith<$Res>
VeilidConfigRPC rpc,
VeilidConfigDHT dht,
bool upnp,
bool detectAddressChanges,
bool? detectAddressChanges,
int restrictedNatRetries,
VeilidConfigTLS tls,
VeilidConfigApplication application,
@ -6713,7 +6713,7 @@ class __$VeilidConfigNetworkCopyWithImpl<$Res>
Object? rpc = null,
Object? dht = null,
Object? upnp = null,
Object? detectAddressChanges = null,
Object? detectAddressChanges = freezed,
Object? restrictedNatRetries = null,
Object? tls = null,
Object? application = null,
@ -6774,10 +6774,10 @@ class __$VeilidConfigNetworkCopyWithImpl<$Res>
? _self.upnp
: upnp // ignore: cast_nullable_to_non_nullable
as bool,
detectAddressChanges: null == detectAddressChanges
detectAddressChanges: freezed == detectAddressChanges
? _self.detectAddressChanges
: detectAddressChanges // ignore: cast_nullable_to_non_nullable
as bool,
as bool?,
restrictedNatRetries: null == restrictedNatRetries
? _self.restrictedNatRetries
: restrictedNatRetries // ignore: cast_nullable_to_non_nullable

View file

@ -478,7 +478,7 @@ _VeilidConfigNetwork _$VeilidConfigNetworkFromJson(Map<String, dynamic> json) =>
rpc: VeilidConfigRPC.fromJson(json['rpc']),
dht: VeilidConfigDHT.fromJson(json['dht']),
upnp: json['upnp'] as bool,
detectAddressChanges: json['detect_address_changes'] as bool,
detectAddressChanges: json['detect_address_changes'] as bool?,
restrictedNatRetries: (json['restricted_nat_retries'] as num).toInt(),
tls: VeilidConfigTLS.fromJson(json['tls']),
application: VeilidConfigApplication.fromJson(json['application']),

View file

@ -25,7 +25,7 @@ class ConfigBase:
value = json_data[key]
try:
# See if this field's type knows how to load itself from JSON input.
loader = field.type.from_json
loader = field.type.from_json # type: ignore
except AttributeError:
# No, it doesn't. Use the raw value.
args[key] = value
@ -210,7 +210,7 @@ class VeilidConfigNetwork(ConfigBase):
rpc: VeilidConfigRPC
dht: VeilidConfigDHT
upnp: bool
detect_address_changes: bool
detect_address_changes: Optional[bool]
restricted_nat_retries: int
tls: VeilidConfigTLS
application: VeilidConfigApplication

View file

@ -4261,7 +4261,6 @@
"client_allowlist_timeout_ms",
"connection_inactivity_timeout_ms",
"connection_initial_timeout_ms",
"detect_address_changes",
"dht",
"hole_punch_receipt_time_ms",
"max_connection_frequency_per_min",
@ -4297,7 +4296,10 @@
"minimum": 0.0
},
"detect_address_changes": {
"type": "boolean"
"type": [
"boolean",
"null"
]
},
"dht": {
"$ref": "#/definitions/VeilidConfigDHT"

View file

@ -180,7 +180,7 @@ core:
member_watch_limit: 8
max_watch_expiration_ms: 600000
upnp: false
detect_address_changes: false
detect_address_changes: auto
restricted_nat_retries: 0
tls:
certificate_path: '%CERTIFICATE_PATH%'
@ -711,6 +711,20 @@ pub struct RoutingTable {
pub limit_attached_weak: u32,
}
fn auto_bool_from_str(s: &str) -> Result<Option<bool>, String> {
match s {
"auto" => Ok(None),
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err("Expected 'auto', 'true', or 'false'".to_owned()),
}
}
fn auto_bool<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<bool>, D::Error> {
let s: String = serde::de::Deserialize::deserialize(deserializer)?;
auto_bool_from_str(s.as_str()).map_err(serde::de::Error::custom)
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Network {
pub connection_initial_timeout_ms: u32,
@ -727,7 +741,8 @@ pub struct Network {
pub rpc: Rpc,
pub dht: Dht,
pub upnp: bool,
pub detect_address_changes: bool,
#[serde(deserialize_with = "auto_bool")]
pub detect_address_changes: Option<bool>,
pub restricted_nat_retries: u32,
pub tls: Tls,
pub application: Application,
@ -1068,6 +1083,28 @@ impl Settings {
}};
}
macro_rules! set_config_value_custom {
($innerkey:expr, $value:expr, $deserializer:expr) => {{
let innerkeyname = &stringify!($innerkey)[6..];
if innerkeyname == key {
match $deserializer(value) {
Ok(v) => {
$innerkey = v;
return Ok(());
}
Err(e) => {
return Err(eyre!(
"invalid type for key {}, value: {}: {}",
key,
value,
e
))
}
}
}
}};
}
set_config_value!(inner.daemon.enabled, value);
set_config_value!(inner.daemon.pid_file, value);
set_config_value!(inner.daemon.chroot, value);
@ -1219,7 +1256,11 @@ impl Settings {
set_config_value!(inner.core.network.dht.member_watch_limit, value);
set_config_value!(inner.core.network.dht.max_watch_expiration_ms, value);
set_config_value!(inner.core.network.upnp, value);
set_config_value!(inner.core.network.detect_address_changes, value);
set_config_value_custom!(
inner.core.network.detect_address_changes,
value,
auto_bool_from_str
);
set_config_value!(inner.core.network.restricted_nat_retries, value);
set_config_value!(inner.core.network.tls.certificate_path, value);
set_config_value!(inner.core.network.tls.private_key_path, value);
@ -1892,7 +1933,7 @@ mod tests {
assert_eq!(s.core.network.dht.max_watch_expiration_ms, 600_000u32);
//
assert!(!s.core.network.upnp);
assert!(!s.core.network.detect_address_changes);
assert_eq!(s.core.network.detect_address_changes, None);
assert_eq!(s.core.network.restricted_nat_retries, 0u32);
//
assert_eq!(
@ -1986,7 +2027,7 @@ mod tests {
);
assert_eq!(s.core.network.protocol.wss.url, None);
//
assert_eq!(s.core.network.privacy.require_inbound_relay, false);
assert!(!s.core.network.privacy.require_inbound_relay);
#[cfg(feature = "geolocation")]
assert_eq!(s.core.network.privacy.country_code_denylist, &[]);
#[cfg(feature = "virtual-network")]