1use std::{
2 collections::HashMap,
3 fmt, io,
4 net::{IpAddr, SocketAddr, UdpSocket},
5 sync::{Mutex, OnceLock},
6 time::Duration,
7};
8
9use alpine::attestation::{verify_device_identity_attestation, AttesterRegistry};
10use alpine::messages::{DiscoveryReply, DiscoveryRequest};
11use rand::{rngs::OsRng, RngCore};
12use serde_cbor;
13use tracing::{info, warn};
14use uuid::Uuid;
15
16use socket2::{Domain, Protocol, Socket, Type};
17
18use crate::error::AlpineSdkError;
19use crate::phase::{current_phase, Phase};
20use crate::quarantine::mark_quarantine;
21
22const DEFAULT_MULTICAST_IPV4: &str = "239.255.255.250:19455";
23const DEFAULT_MULTICAST_IPV6: &str = "[ff12::1]:19455";
24const DEFAULT_BROADCAST_IPV4: &str = "255.255.255.255:19455";
25const DISCOVERY_SAFE_MTU: usize = 1200;
26
27static IDENTITY_SEEN: OnceLock<Mutex<HashMap<IpAddr, Vec<u8>>>> = OnceLock::new();
28
29fn identity_map() -> &'static Mutex<HashMap<IpAddr, Vec<u8>>> {
30 IDENTITY_SEEN.get_or_init(|| Mutex::new(HashMap::new()))
31}
32
33pub struct DiscoveryClientOptions {
35 pub remote_addr: SocketAddr,
36 pub local_addr: SocketAddr,
37 pub timeout: Duration,
38 pub prefer_multicast: bool,
39 pub allow_broadcast: bool,
40 pub interface: Option<String>,
41 pub attester_registry: Option<AttesterRegistry>,
42}
43
44impl DiscoveryClientOptions {
45 pub fn new(remote_addr: SocketAddr, local_addr: SocketAddr, timeout: Duration) -> Self {
47 Self {
48 remote_addr,
49 local_addr,
50 timeout,
51 prefer_multicast: false,
52 allow_broadcast: true,
53 interface: None,
54 attester_registry: None,
55 }
56 }
57
58 pub fn disable_multicast(mut self) -> Self {
59 self.prefer_multicast = false;
60 self
61 }
62
63 pub fn disable_broadcast(mut self) -> Self {
64 self.allow_broadcast = false;
65 self
66 }
67
68 pub fn with_attester_registry(mut self, registry: AttesterRegistry) -> Self {
69 self.attester_registry = Some(registry);
70 self
71 }
72}
73
74#[derive(Debug)]
76pub enum DiscoveryError {
77 Io(io::Error),
78 Decode(serde_cbor::Error),
79 Timeout,
80 PermissionDenied,
81 MulticastUnavailable,
82 BroadcastBlocked,
83}
84
85impl DiscoveryError {
86 pub fn label(&self) -> &'static str {
87 match self {
88 DiscoveryError::Io(_) => "io error",
89 DiscoveryError::Decode(_) => "cbor decode error",
90 DiscoveryError::Timeout => "discovery timed out",
91 DiscoveryError::PermissionDenied => "discovery channel permission denied",
92 DiscoveryError::MulticastUnavailable => "multicast discovery unavailable",
93 DiscoveryError::BroadcastBlocked => "broadcast discovery blocked",
94 }
95 }
96
97 pub fn hint(&self) -> Option<&'static str> {
98 match self {
99 DiscoveryError::PermissionDenied => Some("udp send/recv denied by OS policy"),
100 DiscoveryError::BroadcastBlocked => Some("broadcast disabled on this network"),
101 DiscoveryError::MulticastUnavailable => Some("multicast not permitted on this network"),
102 DiscoveryError::Timeout => Some("no discovery replies before timeout"),
103 _ => None,
104 }
105 }
106}
107
108impl fmt::Display for DiscoveryError {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 DiscoveryError::Io(err) => write!(f, "io error: {}", err),
112 DiscoveryError::Decode(err) => write!(f, "cbors serialization error: {}", err),
113 DiscoveryError::Timeout => write!(f, "discovery timed out"),
114 DiscoveryError::PermissionDenied => {
115 write!(f, "discovery channel permission denied")
116 }
117 DiscoveryError::MulticastUnavailable => {
118 write!(f, "multicast discovery unavailable")
119 }
120 DiscoveryError::BroadcastBlocked => write!(f, "broadcast discovery blocked"),
121 }
122 }
123}
124
125impl std::error::Error for DiscoveryError {}
126
127impl From<io::Error> for DiscoveryError {
128 fn from(err: io::Error) -> Self {
129 match err.kind() {
130 io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
131 _ => DiscoveryError::Io(err),
132 }
133 }
134}
135
136impl From<serde_cbor::Error> for DiscoveryError {
137 fn from(err: serde_cbor::Error) -> Self {
138 DiscoveryError::Decode(err)
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum DiscoveryTargetKind {
144 Multicast,
145 Broadcast,
146 UnicastConfigured,
147 UnicastFallback,
148}
149
150impl DiscoveryTargetKind {
151 pub fn as_str(&self) -> &'static str {
152 match self {
153 DiscoveryTargetKind::Multicast => "multicast",
154 DiscoveryTargetKind::Broadcast => "broadcast",
155 DiscoveryTargetKind::UnicastConfigured => "unicast-configured",
156 DiscoveryTargetKind::UnicastFallback => "unicast",
157 }
158 }
159}
160
161#[derive(Debug, Clone)]
162pub struct DiscoveryErrorDetails {
163 pub label: &'static str,
164 pub hint: Option<&'static str>,
165 pub kind: Option<io::ErrorKind>,
166 pub message: Option<String>,
167}
168
169#[derive(Debug, Clone)]
170pub struct DiscoveryAttempt {
171 pub target: SocketAddr,
172 pub mode: DiscoveryTargetKind,
173 pub local_bind: SocketAddr,
174 pub payload_len: usize,
175 pub bytes_sent: Option<usize>,
176 pub error: Option<DiscoveryErrorDetails>,
177}
178
179impl DiscoveryAttempt {
180 pub fn debug_string(&self) -> String {
181 let error = self.error.as_ref().map(|err| err.label).unwrap_or("ok");
182 format!(
183 "target={} mode={} local_bind={} payload_len={} bytes_sent={} error={}",
184 self.target,
185 self.mode.as_str(),
186 self.local_bind,
187 self.payload_len,
188 self.bytes_sent
189 .map(|val| val.to_string())
190 .unwrap_or_else(|| "-".to_string()),
191 error
192 )
193 }
194}
195
196#[derive(Debug, Clone)]
197pub struct DiscoveryDiagnostics {
198 pub attempts: Vec<DiscoveryAttempt>,
199 pub recv_error: Option<DiscoveryErrorDetails>,
200}
201
202impl DiscoveryDiagnostics {
203 fn new() -> Self {
204 Self {
205 attempts: Vec::new(),
206 recv_error: None,
207 }
208 }
209}
210
211#[derive(Debug)]
212pub struct DiscoveryResult {
213 pub outcome: Option<DiscoveryOutcome>,
214 pub diagnostics: DiscoveryDiagnostics,
215 pub error: Option<DiscoveryError>,
216}
217
218impl DiscoveryResult {
219 pub fn into_result(self) -> Result<DiscoveryOutcome, DiscoveryError> {
220 match (self.outcome, self.error) {
221 (Some(outcome), _) => Ok(outcome),
222 (None, Some(err)) => Err(err),
223 (None, None) => Err(DiscoveryError::Timeout),
224 }
225 }
226}
227
228#[must_use]
230#[derive(Debug, Clone)]
231pub struct DiscoveryOutcome {
232 pub reply: DiscoveryReply,
233 pub peer: SocketAddr,
234 pub client_nonce: Vec<u8>,
235 pub local_addr: SocketAddr,
236 pub device_identity_pubkey: Option<Vec<u8>>,
237 pub device_identity_trusted: bool,
238 pub device_identity_attestation_error: Option<String>,
239 pub interface: Option<String>,
240 pub run_id: String,
241}
242
243#[derive(Debug, Clone)]
244pub struct TrustDecision {
245 pub state: DeviceTrustState,
246 pub reason: String,
247}
248
249impl TrustDecision {
250 pub fn explain(&self) -> &str {
251 &self.reason
252 }
253}
254
255#[derive(Debug, Clone)]
256pub struct ProtocolNegotiationReport {
257 pub client_version: String,
258 pub device_version: String,
259 pub compatible: bool,
260 pub note: String,
261}
262
263#[must_use]
264#[derive(Debug, Clone)]
265pub struct TrustedDiscoveryOutcome {
266 pub outcome: DiscoveryOutcome,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub enum DeviceTrustState {
271 Trusted,
272 UntrustedNoAttestation,
273 UntrustedNoRegistry,
274 UntrustedInvalid(String),
275}
276
277impl DeviceTrustState {
278 pub fn as_str(&self) -> &'static str {
279 match self {
280 DeviceTrustState::Trusted => "trusted",
281 DeviceTrustState::UntrustedNoAttestation => "untrusted-no-attestation",
282 DeviceTrustState::UntrustedNoRegistry => "untrusted-no-registry",
283 DeviceTrustState::UntrustedInvalid(_) => "untrusted-invalid",
284 }
285 }
286}
287
288#[derive(Debug, Clone, Default)]
289pub struct DeviceSelectionPolicy {
290 pub allow_device_ids: Vec<String>,
291 pub deny_device_ids: Vec<String>,
292 pub allow_models: Vec<String>,
293 pub deny_models: Vec<String>,
294 pub allow_manufacturers: Vec<String>,
295 pub deny_manufacturers: Vec<String>,
296}
297
298#[derive(Debug, Clone)]
299pub struct DeviceSelectionDecision {
300 pub allowed: bool,
301 pub reason: String,
302}
303
304impl DeviceSelectionPolicy {
305 pub fn evaluate(&self, outcome: &DiscoveryOutcome) -> DeviceSelectionDecision {
306 let reply = &outcome.reply;
307 if self.deny_device_ids.iter().any(|id| id == &reply.device_id) {
308 return DeviceSelectionDecision {
309 allowed: false,
310 reason: format!("device_id {} denied", reply.device_id),
311 };
312 }
313 if self
314 .deny_models
315 .iter()
316 .any(|model| model == &reply.model_id)
317 {
318 return DeviceSelectionDecision {
319 allowed: false,
320 reason: format!("model_id {} denied", reply.model_id),
321 };
322 }
323 if self
324 .deny_manufacturers
325 .iter()
326 .any(|mfg| mfg == &reply.manufacturer_id)
327 {
328 return DeviceSelectionDecision {
329 allowed: false,
330 reason: format!("manufacturer_id {} denied", reply.manufacturer_id),
331 };
332 }
333 if !self.allow_device_ids.is_empty()
334 && !self
335 .allow_device_ids
336 .iter()
337 .any(|id| id == &reply.device_id)
338 {
339 return DeviceSelectionDecision {
340 allowed: false,
341 reason: format!("device_id {} not in allowlist", reply.device_id),
342 };
343 }
344 if !self.allow_models.is_empty()
345 && !self
346 .allow_models
347 .iter()
348 .any(|model| model == &reply.model_id)
349 {
350 return DeviceSelectionDecision {
351 allowed: false,
352 reason: format!("model_id {} not in allowlist", reply.model_id),
353 };
354 }
355 if !self.allow_manufacturers.is_empty()
356 && !self
357 .allow_manufacturers
358 .iter()
359 .any(|mfg| mfg == &reply.manufacturer_id)
360 {
361 return DeviceSelectionDecision {
362 allowed: false,
363 reason: format!("manufacturer_id {} not in allowlist", reply.manufacturer_id),
364 };
365 }
366 DeviceSelectionDecision {
367 allowed: true,
368 reason: "policy allowed".to_string(),
369 }
370 }
371
372 pub fn enforce(&self, outcome: &DiscoveryOutcome) -> Result<(), AlpineSdkError> {
373 let decision = self.evaluate(outcome);
374 if decision.allowed {
375 Ok(())
376 } else {
377 Err(AlpineSdkError::SelectionDenied(decision.reason))
378 }
379 }
380}
381
382impl DiscoveryOutcome {
383 pub fn trust_state(&self) -> DeviceTrustState {
384 if self.device_identity_trusted {
385 return DeviceTrustState::Trusted;
386 }
387 match self.device_identity_attestation_error.as_deref() {
388 Some("device identity attestation missing") => DeviceTrustState::UntrustedNoAttestation,
389 Some("attester registry not configured") => DeviceTrustState::UntrustedNoRegistry,
390 Some(other) => DeviceTrustState::UntrustedInvalid(other.to_string()),
391 None => DeviceTrustState::UntrustedInvalid("attestation failed".to_string()),
392 }
393 }
394
395 pub fn require_trusted(self) -> Result<TrustedDiscoveryOutcome, AlpineSdkError> {
396 match self.trust_state() {
397 DeviceTrustState::Trusted => Ok(TrustedDiscoveryOutcome { outcome: self }),
398 DeviceTrustState::UntrustedNoAttestation => Err(AlpineSdkError::UntrustedDevice(
399 "device identity attestation missing".into(),
400 )),
401 DeviceTrustState::UntrustedNoRegistry => Err(AlpineSdkError::UntrustedDevice(
402 "attester registry not configured".into(),
403 )),
404 DeviceTrustState::UntrustedInvalid(reason) => {
405 Err(AlpineSdkError::UntrustedDevice(reason))
406 }
407 }
408 }
409
410 pub fn trust_decision(&self) -> TrustDecision {
411 let state = self.trust_state();
412 let reason = match &state {
413 DeviceTrustState::Trusted => "attestation verified".to_string(),
414 DeviceTrustState::UntrustedNoAttestation => {
415 "device identity attestation missing".to_string()
416 }
417 DeviceTrustState::UntrustedNoRegistry => "attester registry not configured".to_string(),
418 DeviceTrustState::UntrustedInvalid(reason) => reason.clone(),
419 };
420 TrustDecision { state, reason }
421 }
422
423 pub fn protocol_report(&self) -> ProtocolNegotiationReport {
424 let client_version = alpine::messages::ALPINE_VERSION.to_string();
425 let device_version = self.reply.alpine_version.clone();
426 let compatible = client_version == device_version;
427 let note = if compatible {
428 "protocol versions match".to_string()
429 } else {
430 format!(
431 "client {} vs device {}; upgrade/downgrade may be required",
432 client_version, device_version
433 )
434 };
435 ProtocolNegotiationReport {
436 client_version,
437 device_version,
438 compatible,
439 note,
440 }
441 }
442
443 pub fn require_capabilities(
444 &self,
445 required: &alpine::messages::CapabilitySet,
446 ) -> Result<(), AlpineSdkError> {
447 validate_capability_subset(required, &self.reply.capabilities)
448 }
449}
450pub struct DiscoveryClient {
452 socket: UdpSocket,
453 remote_addr: SocketAddr,
454 prefer_multicast: bool,
455 allow_broadcast: bool,
456 ipv6: bool,
457 interface: Option<String>,
458 attester_registry: Option<AttesterRegistry>,
459}
460
461impl DiscoveryClient {
462 pub fn new(options: DiscoveryClientOptions) -> Result<Self, DiscoveryError> {
464 let domain = if options.remote_addr.is_ipv4() {
465 Domain::IPV4
466 } else {
467 Domain::IPV6
468 };
469 let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
470 if options.allow_broadcast && options.remote_addr.is_ipv4() {
471 socket.set_broadcast(true)?;
472 }
473 socket.bind(&options.local_addr.into())?;
474
475 let socket: UdpSocket = socket.into();
476 socket.set_read_timeout(Some(options.timeout))?;
477 if options.prefer_multicast || options.remote_addr.ip().is_multicast() {
479 let _ = socket.set_multicast_ttl_v4(4);
480 }
481 let local_addr = socket.local_addr().unwrap_or(options.local_addr);
482 info!(
483 "[ALPINE][DISCOVERY][SOCKET] discovery socket created local_addr={} remote_addr={} prefer_multicast={}",
484 local_addr,
485 options.remote_addr,
486 options.prefer_multicast
487 );
488 Ok(Self {
489 socket,
490 remote_addr: options.remote_addr,
491 prefer_multicast: options.prefer_multicast,
492 allow_broadcast: options.allow_broadcast,
493 ipv6: options.remote_addr.is_ipv6() || options.local_addr.is_ipv6(),
494 interface: options.interface.clone(),
495 attester_registry: options.attester_registry.clone(),
496 })
497 }
498
499 pub fn discover(&self, requested: &[String]) -> Result<DiscoveryOutcome, DiscoveryError> {
501 self.discover_with_report(requested).into_result()
502 }
503
504 pub fn discover_with_report(&self, requested: &[String]) -> DiscoveryResult {
506 let phase = current_phase();
507 if phase == Phase::Handshake {
508 warn!(
509 "[ALPINE][BUG] discovery attempted during handshake phase (no sends expected); local_addr={}",
510 self.socket
511 .local_addr()
512 .unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)))
513 );
514 }
515
516 let mut diagnostics = DiscoveryDiagnostics::new();
517
518 let mut nonce = vec![0u8; 32];
519 OsRng.fill_bytes(&mut nonce);
520 let request = DiscoveryRequest::new(requested.to_vec(), nonce.clone());
521 let payload = match serde_cbor::to_vec(&request) {
522 Ok(payload) => payload,
523 Err(err) => {
524 return DiscoveryResult {
525 outcome: None,
526 diagnostics,
527 error: Some(DiscoveryError::from(err)),
528 };
529 }
530 };
531 let payload_len = payload.len();
532 debug_assert!(
533 payload_len > 8,
534 "discovery payload unexpectedly small; framing may have drifted"
535 );
536 if payload_len > DISCOVERY_SAFE_MTU {
537 warn!(
538 "[ALPINE][DISCOVERY][WARN] payload_len={} exceeds safe MTU {}; fragmentation likely",
539 payload_len, DISCOVERY_SAFE_MTU
540 );
541 }
542
543 let mut send_error: Option<DiscoveryError> = None;
544 let mut sent = false;
545 for target in self.discovery_targets() {
546 let mode = classify_target(target, self.remote_addr);
547 let local_bind_addr = self
548 .socket
549 .local_addr()
550 .unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
551 info!(
552 "[ALPINE][DISCOVERY][TX][attempt] target={} mode={} local_bind={} payload_len={}",
553 target,
554 mode.as_str(),
555 local_bind_addr,
556 payload.len()
557 );
558 match self.socket.send_to(&payload, target) {
559 Ok(bytes) => {
560 info!(
561 "[ALPINE][DISCOVERY][TX][result] target={} mode={} local_bind={} bytes_sent={}",
562 target,
563 mode.as_str(),
564 local_bind_addr,
565 bytes
566 );
567 diagnostics.attempts.push(DiscoveryAttempt {
568 target,
569 mode,
570 local_bind: local_bind_addr,
571 payload_len,
572 bytes_sent: Some(bytes),
573 error: None,
574 });
575 sent = true;
576 }
577 Err(err) => {
578 let kind = err.kind();
579 let message = err.to_string();
580 let mapped = self.map_send_error(err, target);
581 warn!(
582 "[ALPINE][DISCOVERY][TX][result] target={} mode={} local_bind={} phase={} error={}",
583 target,
584 mode.as_str(),
585 local_bind_addr,
586 phase.label(),
587 mapped
588 );
589 diagnostics.attempts.push(DiscoveryAttempt {
590 target,
591 mode,
592 local_bind: local_bind_addr,
593 payload_len,
594 bytes_sent: None,
595 error: Some(DiscoveryErrorDetails {
596 label: mapped.label(),
597 hint: mapped.hint(),
598 kind: Some(kind),
599 message: Some(message),
600 }),
601 });
602 send_error = Some(mapped);
603 if !self.should_continue_after_error(&kind) {
604 return DiscoveryResult {
605 outcome: None,
606 diagnostics,
607 error: send_error,
608 };
609 }
610 }
611 }
612 }
613
614 if !sent {
615 let error = send_error.unwrap_or(DiscoveryError::PermissionDenied);
616 warn!(
617 "[ALPINE][DISCOVERY][WARN] no discovery payloads sent; possible UDP egress block"
618 );
619 return DiscoveryResult {
620 outcome: None,
621 diagnostics,
622 error: Some(error),
623 };
624 }
625
626 let timeout_ms = self
627 .socket
628 .read_timeout()
629 .ok()
630 .flatten()
631 .map(|d| d.as_millis())
632 .unwrap_or_default();
633 let local_port = self
634 .socket
635 .local_addr()
636 .map(|addr| addr.port())
637 .unwrap_or_default();
638 info!(
639 "[ALPINE][DISCOVERY] awaiting reply timeout_ms={} local_port={} remote_hint={}",
640 timeout_ms, local_port, self.remote_addr
641 );
642
643 let mut buf = vec![0u8; 2048];
644 let (len, peer) = match self.socket.recv_from(&mut buf) {
645 Ok(res) => res,
646 Err(err) => {
647 let kind = err.kind();
648 let message = err.to_string();
649 let mapped = self.map_recv_error(err);
650 if matches!(mapped, DiscoveryError::Timeout) && sent {
651 warn!(
652 "[ALPINE][DISCOVERY][WARN] discovery sends ok but no replies; inbound UDP may be blocked"
653 );
654 }
655 diagnostics.recv_error = Some(DiscoveryErrorDetails {
656 label: mapped.label(),
657 hint: mapped.hint(),
658 kind: Some(kind),
659 message: Some(message),
660 });
661 return DiscoveryResult {
662 outcome: None,
663 diagnostics,
664 error: Some(mapped),
665 };
666 }
667 };
668 let reply: DiscoveryReply = match serde_cbor::from_slice(&buf[..len]) {
669 Ok(reply) => reply,
670 Err(err) => {
671 return DiscoveryResult {
672 outcome: None,
673 diagnostics,
674 error: Some(DiscoveryError::from(err)),
675 };
676 }
677 };
678 let local_addr = match self.socket.local_addr() {
679 Ok(local_addr) => local_addr,
680 Err(err) => {
681 return DiscoveryResult {
682 outcome: None,
683 diagnostics,
684 error: Some(DiscoveryError::from(err)),
685 };
686 }
687 };
688 info!(
689 "[ALPINE][DISCOVERY][RX] reply received peer={} local_addr={} iface={:?} bytes={}",
690 peer, local_addr, self.interface, len
691 );
692 let (device_identity_trusted, device_identity_attestation_error) =
693 self.verify_attestation(&reply);
694 if !reply.device_identity_pubkey.is_empty() {
695 let peer_ip = peer.ip();
696 if let Some(reason) = record_identity_change(peer_ip, &reply.device_identity_pubkey) {
697 warn!(
698 "[ALPINE][DISCOVERY][WARN] identity change detected peer={} reason={}",
699 peer, reason
700 );
701 let _ = mark_quarantine(peer_ip, reason);
702 }
703 }
704 let outcome = DiscoveryOutcome {
705 reply: reply.clone(),
706 peer,
707 client_nonce: nonce,
708 local_addr,
709 device_identity_pubkey: if reply.device_identity_pubkey.is_empty() {
710 None
711 } else {
712 Some(reply.device_identity_pubkey.clone())
713 },
714 device_identity_trusted,
715 device_identity_attestation_error,
716 interface: self.interface.clone(),
717 run_id: Uuid::new_v4().to_string(),
718 };
719 DiscoveryResult {
720 outcome: Some(outcome),
721 diagnostics,
722 error: None,
723 }
724 }
725
726 fn discovery_targets(&self) -> Vec<SocketAddr> {
727 let mut targets = Vec::new();
728
729 if self.prefer_multicast {
730 if !self.ipv6 {
731 if let Ok(addr) = DEFAULT_MULTICAST_IPV4.parse() {
732 push_if_unique(&mut targets, addr);
733 }
734 }
735 if self.ipv6 {
736 if let Ok(addr) = DEFAULT_MULTICAST_IPV6.parse() {
737 push_if_unique(&mut targets, addr);
738 }
739 }
740 }
741
742 push_if_unique(&mut targets, self.remote_addr);
743
744 if self.allow_broadcast && self.remote_addr.ip().is_ipv4() && !self.ipv6 {
745 if let Ok(addr) = DEFAULT_BROADCAST_IPV4.parse() {
746 push_if_unique(&mut targets, addr);
747 }
748 }
749
750 targets
751 }
752
753 fn map_send_error(&self, err: io::Error, target: SocketAddr) -> DiscoveryError {
754 match err.kind() {
755 io::ErrorKind::PermissionDenied => {
756 if target.ip().is_multicast() {
757 DiscoveryError::MulticastUnavailable
758 } else if is_broadcast_addr(target.ip()) {
759 DiscoveryError::BroadcastBlocked
760 } else {
761 DiscoveryError::PermissionDenied
762 }
763 }
764 io::ErrorKind::ConnectionReset | io::ErrorKind::WouldBlock => {
765 DiscoveryError::PermissionDenied
766 }
767 _ => DiscoveryError::Io(err),
768 }
769 }
770
771 fn map_recv_error(&self, err: io::Error) -> DiscoveryError {
772 match err.kind() {
773 io::ErrorKind::TimedOut => DiscoveryError::Timeout,
774 io::ErrorKind::PermissionDenied | io::ErrorKind::ConnectionReset => {
775 DiscoveryError::PermissionDenied
776 }
777 io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
778 _ => DiscoveryError::Io(err),
779 }
780 }
781
782 fn should_continue_after_error(&self, kind: &io::ErrorKind) -> bool {
783 matches!(
784 kind,
785 io::ErrorKind::PermissionDenied
786 | io::ErrorKind::WouldBlock
787 | io::ErrorKind::ConnectionReset
788 )
789 }
790
791 fn verify_attestation(&self, reply: &DiscoveryReply) -> (bool, Option<String>) {
792 if reply.device_identity_attestation.is_empty() {
793 return (false, Some("device identity attestation missing".into()));
794 }
795 let Some(registry) = &self.attester_registry else {
796 return (false, Some("attester registry not configured".into()));
797 };
798 match verify_device_identity_attestation(reply, registry, std::time::SystemTime::now()) {
799 Ok(_) => (true, None),
800 Err(err) => {
801 warn!(
802 "[ALPINE][DISCOVERY][TRUST] attestation verification failed device_id={} err={}",
803 reply.device_id,
804 err
805 );
806 (false, Some(err.to_string()))
807 }
808 }
809 }
810}
811
812fn record_identity_change(peer: IpAddr, pubkey: &[u8]) -> Option<String> {
813 let mut guard = identity_map()
814 .lock()
815 .unwrap_or_else(|poisoned| poisoned.into_inner());
816 if let Some(existing) = guard.get(&peer) {
817 if existing != pubkey {
818 guard.insert(peer, pubkey.to_vec());
819 return Some("device identity pubkey changed for peer".to_string());
820 }
821 return None;
822 }
823 guard.insert(peer, pubkey.to_vec());
824 None
825}
826
827impl Drop for DiscoveryClient {
828 fn drop(&mut self) {
829 if let Ok(local) = self.socket.local_addr() {
830 info!(
831 "[ALPINE][DISCOVERY][SOCKET] discovery socket dropped local_addr={} remote_addr={}",
832 local, self.remote_addr
833 );
834 } else {
835 info!(
836 "[ALPINE][DISCOVERY][SOCKET] discovery socket dropped remote_addr={}",
837 self.remote_addr
838 );
839 }
840 }
841}
842
843fn is_broadcast_addr(ip: IpAddr) -> bool {
844 matches!(ip, IpAddr::V4(addr) if addr.is_broadcast())
845}
846
847fn push_if_unique(targets: &mut Vec<SocketAddr>, candidate: SocketAddr) {
848 if !targets.contains(&candidate) {
849 targets.push(candidate);
850 }
851}
852
853fn classify_target(target: SocketAddr, configured: SocketAddr) -> DiscoveryTargetKind {
854 if target.ip().is_multicast() {
855 DiscoveryTargetKind::Multicast
856 } else if is_broadcast_addr(target.ip()) {
857 DiscoveryTargetKind::Broadcast
858 } else if target == configured {
859 DiscoveryTargetKind::UnicastConfigured
860 } else {
861 DiscoveryTargetKind::UnicastFallback
862 }
863}
864
865fn validate_capability_subset(
866 required: &alpine::messages::CapabilitySet,
867 device: &alpine::messages::CapabilitySet,
868) -> Result<(), AlpineSdkError> {
869 for format in required.channel_formats.iter() {
870 if !device.channel_formats.contains(format) {
871 return Err(AlpineSdkError::InvalidCapabilities(format!(
872 "channel format {:?} not supported by device",
873 format
874 )));
875 }
876 }
877 if required.max_channels > device.max_channels {
878 return Err(AlpineSdkError::InvalidCapabilities(format!(
879 "max channels {} exceeds device max {}",
880 required.max_channels, device.max_channels
881 )));
882 }
883 if required.grouping_supported && !device.grouping_supported {
884 return Err(AlpineSdkError::InvalidCapabilities(
885 "grouping not supported by device".into(),
886 ));
887 }
888 if required.streaming_supported && !device.streaming_supported {
889 return Err(AlpineSdkError::InvalidCapabilities(
890 "streaming not supported by device".into(),
891 ));
892 }
893 if required.encryption_supported && !device.encryption_supported {
894 return Err(AlpineSdkError::InvalidCapabilities(
895 "encryption not supported by device".into(),
896 ));
897 }
898 if let Some(extensions) = required.vendor_extensions.as_ref() {
899 if let Some(device_extensions) = device.vendor_extensions.as_ref() {
900 for key in extensions.keys() {
901 if !device_extensions.contains_key(key) {
902 return Err(AlpineSdkError::InvalidCapabilities(format!(
903 "vendor extension {} not supported by device",
904 key
905 )));
906 }
907 }
908 } else {
909 return Err(AlpineSdkError::InvalidCapabilities(
910 "vendor extensions not supported by device".into(),
911 ));
912 }
913 }
914 Ok(())
915}