1use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2use std::time::Duration;
3
4use alpine::attestation::AttesterRegistry;
5use thiserror::Error;
6use tokio::time::{sleep, Duration as TokioDuration};
7use tracing::{info, warn};
8
9use crate::discovery::{
10 DiscoveryAttempt, DiscoveryClient, DiscoveryClientOptions, DiscoveryError, DiscoveryOutcome,
11 DiscoveryResult,
12};
13use crate::environment::ensure_supported_environment;
14use crate::phase::claim_discovery;
15use crate::self_check::run_sdk_self_check;
16
17#[derive(Debug, Clone)]
18pub struct DiscoveryRunOptions {
19 pub local_addr: Option<SocketAddr>,
20 pub prefer_multicast: bool,
21 pub allow_broadcast: bool,
22 pub cache_first: bool,
23 pub cached_targets: Vec<SocketAddr>,
24 pub scan_subnets: bool,
25 pub scan_rate_per_sec: u32,
26 pub scan_timeout_ms: u64,
27 pub scan_max_hosts: u32,
28 pub attester_registry: Option<AttesterRegistry>,
29}
30
31impl Default for DiscoveryRunOptions {
32 fn default() -> Self {
33 Self {
34 local_addr: None,
35 prefer_multicast: false,
36 allow_broadcast: true,
37 cache_first: false,
38 cached_targets: Vec::new(),
39 scan_subnets: false,
40 scan_rate_per_sec: 200,
41 scan_timeout_ms: 500,
42 scan_max_hosts: 1024,
43 attester_registry: None,
44 }
45 }
46}
47
48impl DiscoveryRunOptions {
49 pub fn defaults_for_lan() -> Self {
51 Self::default()
52 }
53
54 pub fn defaults_for_lab() -> Self {
56 Self {
57 prefer_multicast: false,
58 allow_broadcast: false,
59 cache_first: true,
60 scan_subnets: false,
61 scan_max_hosts: 0,
62 ..Self::default()
63 }
64 }
65}
66
67#[derive(Debug, Error)]
68pub enum DiscoveryRunError {
69 #[error("unicast discovery failed: {0}")]
70 Unicast(DiscoveryError),
71 #[error("broadcast discovery failed: {0}")]
72 Broadcast(DiscoveryError),
73 #[error("no viable interfaces for broadcast discovery (need a non-loopback IPv4 address)")]
74 NoInterfaces,
75 #[error("cached unicast discovery failed")]
76 CachedUnicastFailed,
77 #[error("subnet scan discovery failed")]
78 SubnetScanFailed,
79 #[error("subnet scan disabled (scan_max_hosts = 0)")]
80 SubnetScanDisabled,
81 #[error("unsupported environment: {0}")]
82 UnsupportedEnvironment(String),
83}
84
85#[derive(Debug)]
86pub struct DiscoveryRunReport {
87 pub result: Result<DiscoveryOutcome, DiscoveryRunError>,
88 pub attempts: Vec<DiscoveryAttempt>,
89}
90
91impl DiscoveryRunReport {
92 pub fn chosen_attempt(&self) -> Option<DiscoveryAttempt> {
93 let outcome = self.result.as_ref().ok()?;
94 self.attempts
95 .iter()
96 .find(|attempt| attempt.target == outcome.peer)
97 .cloned()
98 }
99
100 pub fn decision_summary(&self) -> String {
101 match &self.result {
102 Ok(outcome) => {
103 if let Some(attempt) = self
104 .attempts
105 .iter()
106 .find(|attempt| attempt.target == outcome.peer)
107 {
108 format!(
109 "selected {} via {} (local_bind {}); {} attempts",
110 outcome.peer,
111 attempt.mode.as_str(),
112 attempt.local_bind,
113 self.attempts.len()
114 )
115 } else {
116 format!(
117 "selected {} (attempt not recorded); {} attempts",
118 outcome.peer,
119 self.attempts.len()
120 )
121 }
122 }
123 Err(err) => format!(
124 "no device selected: {} ({} attempts)",
125 err,
126 self.attempts.len()
127 ),
128 }
129 }
130
131 pub fn summary(&self) -> String {
132 match &self.result {
133 Ok(outcome) => format!(
134 "discovered {} (device_id={}) after {} attempts",
135 outcome.peer,
136 outcome.reply.device_id,
137 self.attempts.len()
138 ),
139 Err(err) => {
140 let mut hints = Vec::new();
141 for attempt in &self.attempts {
142 if let Some(error) = &attempt.error {
143 if let Some(hint) = error.hint {
144 hints.push(hint);
145 }
146 }
147 }
148 hints.sort();
149 hints.dedup();
150 if hints.is_empty() {
151 format!(
152 "discovery failed: {} ({} attempts)",
153 err,
154 self.attempts.len()
155 )
156 } else {
157 format!(
158 "discovery failed: {} ({} attempts); hints: {}",
159 err,
160 self.attempts.len(),
161 hints.join("; ")
162 )
163 }
164 }
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
170pub struct DiscoveryInterface {
171 pub name: String,
172 pub local_ip: std::net::Ipv4Addr,
173 pub netmask: std::net::Ipv4Addr,
174 pub broadcast: std::net::Ipv4Addr,
175}
176
177#[derive(Debug, Clone)]
178pub struct DiscoveryDryRun {
179 pub remote_addr: SocketAddr,
180 pub unicast_target: Option<SocketAddr>,
181 pub prefer_multicast: bool,
182 pub allow_broadcast: bool,
183 pub cached_targets: Vec<SocketAddr>,
184 pub scan_subnets: bool,
185 pub scan_rate_per_sec: u32,
186 pub scan_timeout_ms: u64,
187 pub scan_max_hosts: u32,
188 pub interfaces: Vec<DiscoveryInterface>,
189 pub broadcast_targets: Vec<SocketAddr>,
190}
191
192impl DiscoveryDryRun {
193 pub fn print_table(&self) -> String {
194 let mut lines = Vec::new();
195 lines.push("interface, local_ip, netmask, broadcast".to_string());
196 for iface in &self.interfaces {
197 lines.push(format!(
198 "{}, {}, {}, {}",
199 iface.name, iface.local_ip, iface.netmask, iface.broadcast
200 ));
201 }
202 if let Some(target) = self.unicast_target {
203 lines.push(format!("unicast_target, {}", target));
204 }
205 for target in &self.broadcast_targets {
206 lines.push(format!("broadcast_target, {}", target));
207 }
208 if !self.cached_targets.is_empty() {
209 for target in &self.cached_targets {
210 lines.push(format!("cached_target, {}", target));
211 }
212 }
213 lines.join("\n")
214 }
215}
216
217#[derive(Debug, Clone)]
218pub struct DiscoveryRetryPolicy {
219 pub max_attempts: usize,
220 pub backoff_base_ms: u64,
221 pub backoff_max_ms: u64,
222}
223
224impl Default for DiscoveryRetryPolicy {
225 fn default() -> Self {
226 Self {
227 max_attempts: 3,
228 backoff_base_ms: 200,
229 backoff_max_ms: 2_000,
230 }
231 }
232}
233
234fn default_local_addr() -> SocketAddr {
235 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
236}
237
238fn resolve_local_addr(remote_addr: SocketAddr, override_addr: Option<SocketAddr>) -> SocketAddr {
239 if let Some(addr) = override_addr {
240 return addr;
241 }
242 if matches!(remote_addr.ip(), IpAddr::V4(v4) if v4.is_broadcast()) {
243 return default_local_addr();
244 }
245 let bind_addr = if remote_addr.is_ipv4() {
246 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
247 } else {
248 SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), 0)
249 };
250 if let Ok(sock) = std::net::UdpSocket::bind(bind_addr) {
251 if sock.connect(remote_addr).is_ok() {
252 if let Ok(local) = sock.local_addr() {
253 return SocketAddr::new(local.ip(), 0);
254 }
255 }
256 }
257 default_local_addr()
258}
259
260pub async fn run_discovery(remote_addr: SocketAddr) -> Result<DiscoveryOutcome, DiscoveryRunError> {
261 run_discovery_with_options(remote_addr, DiscoveryRunOptions::default()).await
262}
263
264pub async fn run_discovery_with_report(remote_addr: SocketAddr) -> DiscoveryRunReport {
266 run_discovery_with_options_report(remote_addr, DiscoveryRunOptions::default()).await
267}
268
269pub async fn run_discovery_with_retry(
270 remote_addr: SocketAddr,
271 policy: DiscoveryRetryPolicy,
272) -> Result<DiscoveryOutcome, DiscoveryRunError> {
273 run_discovery_with_options_retry(remote_addr, DiscoveryRunOptions::default(), policy).await
274}
275
276pub async fn run_discovery_with_options(
277 remote_addr: SocketAddr,
278 opts: DiscoveryRunOptions,
279) -> Result<DiscoveryOutcome, DiscoveryRunError> {
280 run_discovery_with_options_inner(remote_addr, opts, None).await
281}
282
283pub async fn discover_with_cache(
284 remote_addr: SocketAddr,
285 cached_targets: Vec<SocketAddr>,
286) -> Result<DiscoveryOutcome, DiscoveryRunError> {
287 let mut opts = DiscoveryRunOptions::defaults_for_lan();
288 opts.cached_targets = cached_targets;
289 run_discovery_with_options(remote_addr, opts).await
290}
291
292pub async fn discover_with_cache_report(
293 remote_addr: SocketAddr,
294 cached_targets: Vec<SocketAddr>,
295) -> DiscoveryRunReport {
296 let mut opts = DiscoveryRunOptions::defaults_for_lan();
297 opts.cached_targets = cached_targets;
298 run_discovery_with_options_report(remote_addr, opts).await
299}
300
301pub async fn run_discovery_with_options_retry(
302 remote_addr: SocketAddr,
303 opts: DiscoveryRunOptions,
304 policy: DiscoveryRetryPolicy,
305) -> Result<DiscoveryOutcome, DiscoveryRunError> {
306 let attempts = policy.max_attempts.max(1);
307 let mut attempt = 0usize;
308 loop {
309 attempt = attempt.saturating_add(1);
310 match run_discovery_with_options(remote_addr, opts.clone()).await {
311 Ok(outcome) => {
312 if attempt > 1 {
313 warn!(
314 "[ALPINE][DISCOVERY][WARN] discovery succeeded after {} retries",
315 attempt - 1
316 );
317 }
318 return Ok(outcome);
319 }
320 Err(err) => {
321 if attempt >= attempts {
322 return Err(err);
323 }
324 sleep(TokioDuration::from_millis(discovery_backoff(
325 &policy, attempt,
326 )))
327 .await;
328 }
329 }
330 }
331}
332
333async fn run_discovery_with_options_inner(
334 remote_addr: SocketAddr,
335 opts: DiscoveryRunOptions,
336 mut diagnostics: Option<&mut Vec<DiscoveryAttempt>>,
337) -> Result<DiscoveryOutcome, DiscoveryRunError> {
338 run_sdk_self_check();
339 if let Err(err) = ensure_supported_environment() {
340 return Err(DiscoveryRunError::UnsupportedEnvironment(err.to_string()));
341 }
342 let _phase_guard = claim_discovery()
343 .map_err(|_| DiscoveryRunError::Unicast(DiscoveryError::PermissionDenied))?;
344 let is_broadcast_target = matches!(remote_addr.ip(), IpAddr::V4(v4) if v4.is_broadcast());
345
346 if opts.cache_first {
347 if let Ok(outcome) = attempt_cached_unicast(&opts, &mut diagnostics) {
348 return Ok(outcome);
349 }
350 }
351
352 if !is_broadcast_target {
353 let local_addr = resolve_local_addr(remote_addr, opts.local_addr);
354 let mut options =
355 DiscoveryClientOptions::new(remote_addr, local_addr, Duration::from_secs(3));
356 options.attester_registry = opts.attester_registry.clone();
357 options = options.disable_multicast().disable_broadcast();
358 info!(
359 "[ALPINE][DISCOVERY] route_hint remote={} local_bind={}",
360 remote_addr, options.local_addr
361 );
362 let local_bind = options.local_addr;
363 let client = DiscoveryClient::new(options).map_err(DiscoveryRunError::Unicast)?;
364 let report = client.discover_with_report(&["alpine-control".to_string()]);
365 append_attempts(&mut diagnostics, &report);
366 match report.into_result() {
367 Ok(outcome) => return Ok(outcome),
368 Err(err) => {
369 if opts.allow_broadcast && remote_addr.is_ipv4() {
370 warn!(
371 "[ALPINE][DISCOVERY][WARN] unicast discovery failed ({}); falling back to broadcast",
372 err
373 );
374 match attempt_broadcast(
375 remote_addr.port(),
376 &opts,
377 Some(local_bind.ip()),
378 &mut diagnostics,
379 ) {
380 Ok(outcome) => return Ok(outcome),
381 Err(broadcast_err) => {
382 warn!(
383 "[ALPINE][DISCOVERY][WARN] falling back to cached unicast targets"
384 );
385 if let Ok(outcome) = attempt_cached_unicast(&opts, &mut diagnostics) {
386 return Ok(outcome);
387 }
388 if opts.scan_subnets {
389 warn!("[ALPINE][DISCOVERY][WARN] falling back to subnet scan");
390 return attempt_subnet_scan(
391 remote_addr.port(),
392 &opts,
393 Some(local_bind.ip()),
394 &mut diagnostics,
395 )
396 .await;
397 }
398 return Err(broadcast_err);
399 }
400 }
401 }
402 return Err(DiscoveryRunError::Unicast(err));
403 }
404 }
405 }
406
407 match attempt_broadcast(remote_addr.port(), &opts, None, &mut diagnostics) {
408 Ok(outcome) => Ok(outcome),
409 Err(broadcast_err) => {
410 warn!("[ALPINE][DISCOVERY][WARN] falling back to cached unicast targets");
411 if let Ok(outcome) = attempt_cached_unicast(&opts, &mut diagnostics) {
412 return Ok(outcome);
413 }
414 if opts.scan_subnets {
415 warn!("[ALPINE][DISCOVERY][WARN] falling back to subnet scan");
416 return attempt_subnet_scan(remote_addr.port(), &opts, None, &mut diagnostics)
417 .await;
418 }
419 Err(broadcast_err)
420 }
421 }
422}
423
424pub async fn run_discovery_with_options_report(
425 remote_addr: SocketAddr,
426 opts: DiscoveryRunOptions,
427) -> DiscoveryRunReport {
428 let mut attempts = Vec::new();
429 let result = run_discovery_with_options_inner(remote_addr, opts, Some(&mut attempts)).await;
430 DiscoveryRunReport { result, attempts }
431}
432
433pub fn discovery_dry_run(
435 remote_addr: SocketAddr,
436 opts: DiscoveryRunOptions,
437) -> Result<DiscoveryDryRun, DiscoveryRunError> {
438 let interfaces = collect_interfaces()?;
439 let broadcast_targets = if opts.allow_broadcast && remote_addr.is_ipv4() {
440 interfaces
441 .iter()
442 .map(|iface| SocketAddr::new(IpAddr::V4(iface.broadcast), remote_addr.port()))
443 .collect::<Vec<_>>()
444 } else {
445 Vec::new()
446 };
447 let unicast_target = if matches!(remote_addr.ip(), IpAddr::V4(v4) if v4.is_broadcast()) {
448 None
449 } else {
450 Some(remote_addr)
451 };
452 Ok(DiscoveryDryRun {
453 remote_addr,
454 unicast_target,
455 prefer_multicast: opts.prefer_multicast,
456 allow_broadcast: opts.allow_broadcast,
457 cached_targets: opts.cached_targets,
458 scan_subnets: opts.scan_subnets,
459 scan_rate_per_sec: opts.scan_rate_per_sec,
460 scan_timeout_ms: opts.scan_timeout_ms,
461 scan_max_hosts: opts.scan_max_hosts,
462 interfaces: interfaces
463 .into_iter()
464 .map(|iface| DiscoveryInterface {
465 name: iface.iface,
466 local_ip: iface.local_ip,
467 netmask: iface.netmask,
468 broadcast: iface.broadcast,
469 })
470 .collect(),
471 broadcast_targets,
472 })
473}
474
475fn attempt_broadcast(
476 port: u16,
477 opts: &DiscoveryRunOptions,
478 preferred_ip: Option<IpAddr>,
479 diagnostics: &mut Option<&mut Vec<DiscoveryAttempt>>,
480) -> Result<DiscoveryOutcome, DiscoveryRunError> {
481 let mut attempts = collect_interfaces()?;
482 if attempts.is_empty() {
483 return Err(DiscoveryRunError::NoInterfaces);
484 }
485
486 if let Some(pref) = preferred_ip {
487 if let Some(idx) = attempts.iter().position(|a| IpAddr::V4(a.local_ip) == pref) {
488 let preferred = attempts.remove(idx);
489 attempts.insert(0, preferred);
490 }
491 }
492
493 let mut last_err: Option<DiscoveryError> = None;
494 for attempt in attempts.iter() {
495 let mut options = DiscoveryClientOptions::new(
496 SocketAddr::new(IpAddr::V4(attempt.broadcast), port),
497 SocketAddr::new(IpAddr::V4(attempt.local_ip), 0),
498 Duration::from_secs(3),
499 );
500 options.interface = Some(attempt.iface.clone());
501 options.attester_registry = opts.attester_registry.clone();
502 if !opts.prefer_multicast {
503 options = options.disable_multicast();
504 }
505 if !opts.allow_broadcast {
506 options = options.disable_broadcast();
507 }
508 info!(
509 "[ALPINE][DISCOVERY] iface={} local_ip={} netmask={} broadcast={} bound={}:0 so_broadcast={}",
510 attempt.iface,
511 attempt.local_ip,
512 attempt.netmask,
513 attempt.broadcast,
514 attempt.local_ip,
515 options.allow_broadcast
516 );
517
518 match DiscoveryClient::new(options) {
519 Ok(client) => {
520 let report = client.discover_with_report(&["alpine-control".to_string()]);
521 append_attempts(diagnostics, &report);
522 match report.into_result() {
523 Ok(outcome) => return Ok(outcome),
524 Err(err) => {
525 warn!(
526 "[ALPINE][DISCOVERY][WARN] iface={} error={}",
527 attempt.iface, err
528 );
529 last_err = Some(err);
530 continue;
531 }
532 }
533 }
534 Err(err) => {
535 warn!(
536 "[ALPINE][DISCOVERY][WARN] iface={} error={}",
537 attempt.iface, err
538 );
539 last_err = Some(err);
540 continue;
541 }
542 }
543 }
544
545 Err(DiscoveryRunError::Broadcast(
546 last_err.unwrap_or(DiscoveryError::Timeout),
547 ))
548}
549
550fn attempt_cached_unicast(
551 opts: &DiscoveryRunOptions,
552 diagnostics: &mut Option<&mut Vec<DiscoveryAttempt>>,
553) -> Result<DiscoveryOutcome, DiscoveryRunError> {
554 if opts.cached_targets.is_empty() {
555 return Err(DiscoveryRunError::CachedUnicastFailed);
556 }
557 for target in opts.cached_targets.iter() {
558 let local_addr = resolve_local_addr(*target, None);
559 let mut options = DiscoveryClientOptions::new(
560 *target,
561 local_addr,
562 Duration::from_millis(opts.scan_timeout_ms),
563 );
564 options.attester_registry = opts.attester_registry.clone();
565 options = options.disable_multicast().disable_broadcast();
566 info!(
567 "[ALPINE][DISCOVERY] cached_unicast target={} local_bind={}",
568 target, options.local_addr
569 );
570 if let Ok(client) = DiscoveryClient::new(options) {
571 let report = client.discover_with_report(&["alpine-control".to_string()]);
572 append_attempts(diagnostics, &report);
573 match report.into_result() {
574 Ok(outcome) => return Ok(outcome),
575 Err(err) => {
576 warn!(
577 "[ALPINE][DISCOVERY][WARN] cached_unicast target={} error={}",
578 target, err
579 );
580 }
581 }
582 }
583 }
584 Err(DiscoveryRunError::CachedUnicastFailed)
585}
586
587async fn attempt_subnet_scan(
588 port: u16,
589 opts: &DiscoveryRunOptions,
590 preferred_ip: Option<IpAddr>,
591 diagnostics: &mut Option<&mut Vec<DiscoveryAttempt>>,
592) -> Result<DiscoveryOutcome, DiscoveryRunError> {
593 if opts.scan_max_hosts == 0 {
594 return Err(DiscoveryRunError::SubnetScanDisabled);
595 }
596 let mut attempts = collect_interfaces()?;
597 if attempts.is_empty() {
598 return Err(DiscoveryRunError::NoInterfaces);
599 }
600 if let Some(pref) = preferred_ip {
601 if let Some(idx) = attempts.iter().position(|a| IpAddr::V4(a.local_ip) == pref) {
602 let preferred = attempts.remove(idx);
603 attempts.insert(0, preferred);
604 }
605 }
606
607 let rate = opts.scan_rate_per_sec.max(1);
608 let delay_ms = (1000u64 / rate as u64).max(1);
609 let timeout_ms = opts.scan_timeout_ms.max(100);
610
611 for attempt in attempts.iter() {
612 let mut scanned = 0u32;
613 let (network, broadcast) = network_bounds(attempt.local_ip, attempt.netmask);
614 let start = network.saturating_add(1);
615 let end = broadcast.saturating_sub(1);
616 info!(
617 "[ALPINE][DISCOVERY] subnet_scan iface={} local_ip={} netmask={} range={}.{} timeout_ms={} rate_per_sec={} max_hosts={}",
618 attempt.iface,
619 attempt.local_ip,
620 attempt.netmask,
621 std::net::Ipv4Addr::from(network),
622 std::net::Ipv4Addr::from(broadcast),
623 timeout_ms,
624 rate,
625 opts.scan_max_hosts
626 );
627 let mut ip = start;
628 while ip <= end && scanned < opts.scan_max_hosts {
629 let target_ip = std::net::Ipv4Addr::from(ip);
630 if target_ip != attempt.local_ip {
631 let target = SocketAddr::new(IpAddr::V4(target_ip), port);
632 let local_addr = SocketAddr::new(IpAddr::V4(attempt.local_ip), 0);
633 let mut options = DiscoveryClientOptions::new(
634 target,
635 local_addr,
636 Duration::from_millis(timeout_ms),
637 );
638 options.attester_registry = opts.attester_registry.clone();
639 options = options.disable_multicast().disable_broadcast();
640 if let Ok(client) = DiscoveryClient::new(options) {
641 let report = client.discover_with_report(&["alpine-control".to_string()]);
642 append_attempts(diagnostics, &report);
643 if let Ok(outcome) = report.into_result() {
644 return Ok(outcome);
645 }
646 }
647 scanned += 1;
648 sleep(TokioDuration::from_millis(delay_ms)).await;
649 }
650 ip = ip.saturating_add(1);
651 }
652 }
653 Err(DiscoveryRunError::SubnetScanFailed)
654}
655
656struct IfaceAttempt {
657 iface: String,
658 local_ip: std::net::Ipv4Addr,
659 netmask: std::net::Ipv4Addr,
660 broadcast: std::net::Ipv4Addr,
661}
662
663fn collect_interfaces() -> Result<Vec<IfaceAttempt>, DiscoveryRunError> {
664 let mut attempts = Vec::new();
665 let ifaces = get_if_addrs::get_if_addrs().map_err(|_| DiscoveryRunError::NoInterfaces)?;
666 for iface in ifaces {
667 if iface.is_loopback() {
668 continue;
669 }
670 if let get_if_addrs::IfAddr::V4(v4) = iface.addr {
671 let ipv4 = v4.ip;
672 let maskv4 = v4.netmask;
673 let ip_u32 = u32::from_be_bytes(ipv4.octets());
674 let mask_u32 = u32::from_be_bytes(maskv4.octets());
675 let bcast = ip_u32 | (!mask_u32);
676 let bcast_ip = std::net::Ipv4Addr::from(bcast.to_be_bytes());
677 attempts.push(IfaceAttempt {
678 iface: iface.name,
679 local_ip: ipv4,
680 netmask: maskv4,
681 broadcast: bcast_ip,
682 });
683 }
684 }
685 if attempts.len() > 1 {
686 let names = attempts
687 .iter()
688 .map(|iface| iface.iface.as_str())
689 .collect::<Vec<_>>()
690 .join(", ");
691 warn!(
692 "[ALPINE][DISCOVERY][WARN] multiple NICs detected (heuristic selection): {}",
693 names
694 );
695 }
696 Ok(attempts)
697}
698
699fn network_bounds(ip: std::net::Ipv4Addr, netmask: std::net::Ipv4Addr) -> (u32, u32) {
700 let ip_u32 = u32::from_be_bytes(ip.octets());
701 let mask_u32 = u32::from_be_bytes(netmask.octets());
702 let network = ip_u32 & mask_u32;
703 let broadcast = network | (!mask_u32);
704 (network, broadcast)
705}
706
707fn append_attempts(diagnostics: &mut Option<&mut Vec<DiscoveryAttempt>>, report: &DiscoveryResult) {
708 if let Some(targets) = diagnostics.as_mut() {
709 targets.extend(report.diagnostics.attempts.clone());
710 }
711}
712
713fn discovery_backoff(policy: &DiscoveryRetryPolicy, attempt: usize) -> u64 {
714 let exponent = attempt.saturating_sub(1) as u32;
715 let factor = 1u64.checked_shl(exponent).unwrap_or(u64::MAX);
716 let delay = policy.backoff_base_ms.saturating_mul(factor);
717 delay.min(policy.backoff_max_ms)
718}