use std::{
collections::HashMap,
fmt, io,
net::{IpAddr, SocketAddr, UdpSocket},
sync::{Mutex, OnceLock},
time::Duration,
};
use alpine::attestation::{verify_device_identity_attestation, AttesterRegistry};
use alpine::messages::{DiscoveryReply, DiscoveryRequest};
use rand::{rngs::OsRng, RngCore};
use serde_cbor;
use tracing::{info, warn};
use uuid::Uuid;
use socket2::{Domain, Protocol, Socket, Type};
use crate::error::AlpineSdkError;
use crate::phase::{current_phase, Phase};
use crate::quarantine::mark_quarantine;
const DEFAULT_MULTICAST_IPV4: &str = "239.255.255.250:19455";
const DEFAULT_MULTICAST_IPV6: &str = "[ff12::1]:19455";
const DEFAULT_BROADCAST_IPV4: &str = "255.255.255.255:19455";
const DISCOVERY_SAFE_MTU: usize = 1200;
static IDENTITY_SEEN: OnceLock<Mutex<HashMap<IpAddr, Vec<u8>>>> = OnceLock::new();
fn identity_map() -> &'static Mutex<HashMap<IpAddr, Vec<u8>>> {
IDENTITY_SEEN.get_or_init(|| Mutex::new(HashMap::new()))
}
pub struct DiscoveryClientOptions {
pub remote_addr: SocketAddr,
pub local_addr: SocketAddr,
pub timeout: Duration,
pub prefer_multicast: bool,
pub allow_broadcast: bool,
pub interface: Option<String>,
pub attester_registry: Option<AttesterRegistry>,
}
impl DiscoveryClientOptions {
pub fn new(remote_addr: SocketAddr, local_addr: SocketAddr, timeout: Duration) -> Self {
Self {
remote_addr,
local_addr,
timeout,
prefer_multicast: false,
allow_broadcast: true,
interface: None,
attester_registry: None,
}
}
pub fn disable_multicast(mut self) -> Self {
self.prefer_multicast = false;
self
}
pub fn disable_broadcast(mut self) -> Self {
self.allow_broadcast = false;
self
}
pub fn with_attester_registry(mut self, registry: AttesterRegistry) -> Self {
self.attester_registry = Some(registry);
self
}
}
#[derive(Debug)]
pub enum DiscoveryError {
Io(io::Error),
Decode(serde_cbor::Error),
Timeout,
PermissionDenied,
MulticastUnavailable,
BroadcastBlocked,
}
impl DiscoveryError {
pub fn label(&self) -> &'static str {
match self {
DiscoveryError::Io(_) => "io error",
DiscoveryError::Decode(_) => "cbor decode error",
DiscoveryError::Timeout => "discovery timed out",
DiscoveryError::PermissionDenied => "discovery channel permission denied",
DiscoveryError::MulticastUnavailable => "multicast discovery unavailable",
DiscoveryError::BroadcastBlocked => "broadcast discovery blocked",
}
}
pub fn hint(&self) -> Option<&'static str> {
match self {
DiscoveryError::PermissionDenied => Some("udp send/recv denied by OS policy"),
DiscoveryError::BroadcastBlocked => Some("broadcast disabled on this network"),
DiscoveryError::MulticastUnavailable => Some("multicast not permitted on this network"),
DiscoveryError::Timeout => Some("no discovery replies before timeout"),
_ => None,
}
}
}
impl fmt::Display for DiscoveryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiscoveryError::Io(err) => write!(f, "io error: {}", err),
DiscoveryError::Decode(err) => write!(f, "cbors serialization error: {}", err),
DiscoveryError::Timeout => write!(f, "discovery timed out"),
DiscoveryError::PermissionDenied => {
write!(f, "discovery channel permission denied")
}
DiscoveryError::MulticastUnavailable => {
write!(f, "multicast discovery unavailable")
}
DiscoveryError::BroadcastBlocked => write!(f, "broadcast discovery blocked"),
}
}
}
impl std::error::Error for DiscoveryError {}
impl From<io::Error> for DiscoveryError {
fn from(err: io::Error) -> Self {
match err.kind() {
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
_ => DiscoveryError::Io(err),
}
}
}
impl From<serde_cbor::Error> for DiscoveryError {
fn from(err: serde_cbor::Error) -> Self {
DiscoveryError::Decode(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryTargetKind {
Multicast,
Broadcast,
UnicastConfigured,
UnicastFallback,
}
impl DiscoveryTargetKind {
pub fn as_str(&self) -> &'static str {
match self {
DiscoveryTargetKind::Multicast => "multicast",
DiscoveryTargetKind::Broadcast => "broadcast",
DiscoveryTargetKind::UnicastConfigured => "unicast-configured",
DiscoveryTargetKind::UnicastFallback => "unicast",
}
}
}
#[derive(Debug, Clone)]
pub struct DiscoveryErrorDetails {
pub label: &'static str,
pub hint: Option<&'static str>,
pub kind: Option<io::ErrorKind>,
pub message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DiscoveryAttempt {
pub target: SocketAddr,
pub mode: DiscoveryTargetKind,
pub local_bind: SocketAddr,
pub payload_len: usize,
pub bytes_sent: Option<usize>,
pub error: Option<DiscoveryErrorDetails>,
}
impl DiscoveryAttempt {
pub fn debug_string(&self) -> String {
let error = self.error.as_ref().map(|err| err.label).unwrap_or("ok");
format!(
"target={} mode={} local_bind={} payload_len={} bytes_sent={} error={}",
self.target,
self.mode.as_str(),
self.local_bind,
self.payload_len,
self.bytes_sent
.map(|val| val.to_string())
.unwrap_or_else(|| "-".to_string()),
error
)
}
}
#[derive(Debug, Clone)]
pub struct DiscoveryDiagnostics {
pub attempts: Vec<DiscoveryAttempt>,
pub recv_error: Option<DiscoveryErrorDetails>,
}
impl DiscoveryDiagnostics {
fn new() -> Self {
Self {
attempts: Vec::new(),
recv_error: None,
}
}
}
#[derive(Debug)]
pub struct DiscoveryResult {
pub outcome: Option<DiscoveryOutcome>,
pub diagnostics: DiscoveryDiagnostics,
pub error: Option<DiscoveryError>,
}
impl DiscoveryResult {
pub fn into_result(self) -> Result<DiscoveryOutcome, DiscoveryError> {
match (self.outcome, self.error) {
(Some(outcome), _) => Ok(outcome),
(None, Some(err)) => Err(err),
(None, None) => Err(DiscoveryError::Timeout),
}
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct DiscoveryOutcome {
pub reply: DiscoveryReply,
pub peer: SocketAddr,
pub client_nonce: Vec<u8>,
pub local_addr: SocketAddr,
pub device_identity_pubkey: Option<Vec<u8>>,
pub device_identity_trusted: bool,
pub device_identity_attestation_error: Option<String>,
pub interface: Option<String>,
pub run_id: String,
}
#[derive(Debug, Clone)]
pub struct TrustDecision {
pub state: DeviceTrustState,
pub reason: String,
}
impl TrustDecision {
pub fn explain(&self) -> &str {
&self.reason
}
}
#[derive(Debug, Clone)]
pub struct ProtocolNegotiationReport {
pub client_version: String,
pub device_version: String,
pub compatible: bool,
pub note: String,
}
#[must_use]
#[derive(Debug, Clone)]
pub struct TrustedDiscoveryOutcome {
pub outcome: DiscoveryOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceTrustState {
Trusted,
UntrustedNoAttestation,
UntrustedNoRegistry,
UntrustedInvalid(String),
}
impl DeviceTrustState {
pub fn as_str(&self) -> &'static str {
match self {
DeviceTrustState::Trusted => "trusted",
DeviceTrustState::UntrustedNoAttestation => "untrusted-no-attestation",
DeviceTrustState::UntrustedNoRegistry => "untrusted-no-registry",
DeviceTrustState::UntrustedInvalid(_) => "untrusted-invalid",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DeviceSelectionPolicy {
pub allow_device_ids: Vec<String>,
pub deny_device_ids: Vec<String>,
pub allow_models: Vec<String>,
pub deny_models: Vec<String>,
pub allow_manufacturers: Vec<String>,
pub deny_manufacturers: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DeviceSelectionDecision {
pub allowed: bool,
pub reason: String,
}
impl DeviceSelectionPolicy {
pub fn evaluate(&self, outcome: &DiscoveryOutcome) -> DeviceSelectionDecision {
let reply = &outcome.reply;
if self.deny_device_ids.iter().any(|id| id == &reply.device_id) {
return DeviceSelectionDecision {
allowed: false,
reason: format!("device_id {} denied", reply.device_id),
};
}
if self
.deny_models
.iter()
.any(|model| model == &reply.model_id)
{
return DeviceSelectionDecision {
allowed: false,
reason: format!("model_id {} denied", reply.model_id),
};
}
if self
.deny_manufacturers
.iter()
.any(|mfg| mfg == &reply.manufacturer_id)
{
return DeviceSelectionDecision {
allowed: false,
reason: format!("manufacturer_id {} denied", reply.manufacturer_id),
};
}
if !self.allow_device_ids.is_empty()
&& !self
.allow_device_ids
.iter()
.any(|id| id == &reply.device_id)
{
return DeviceSelectionDecision {
allowed: false,
reason: format!("device_id {} not in allowlist", reply.device_id),
};
}
if !self.allow_models.is_empty()
&& !self
.allow_models
.iter()
.any(|model| model == &reply.model_id)
{
return DeviceSelectionDecision {
allowed: false,
reason: format!("model_id {} not in allowlist", reply.model_id),
};
}
if !self.allow_manufacturers.is_empty()
&& !self
.allow_manufacturers
.iter()
.any(|mfg| mfg == &reply.manufacturer_id)
{
return DeviceSelectionDecision {
allowed: false,
reason: format!("manufacturer_id {} not in allowlist", reply.manufacturer_id),
};
}
DeviceSelectionDecision {
allowed: true,
reason: "policy allowed".to_string(),
}
}
pub fn enforce(&self, outcome: &DiscoveryOutcome) -> Result<(), AlpineSdkError> {
let decision = self.evaluate(outcome);
if decision.allowed {
Ok(())
} else {
Err(AlpineSdkError::SelectionDenied(decision.reason))
}
}
}
impl DiscoveryOutcome {
pub fn trust_state(&self) -> DeviceTrustState {
if self.device_identity_trusted {
return DeviceTrustState::Trusted;
}
match self.device_identity_attestation_error.as_deref() {
Some("device identity attestation missing") => DeviceTrustState::UntrustedNoAttestation,
Some("attester registry not configured") => DeviceTrustState::UntrustedNoRegistry,
Some(other) => DeviceTrustState::UntrustedInvalid(other.to_string()),
None => DeviceTrustState::UntrustedInvalid("attestation failed".to_string()),
}
}
pub fn require_trusted(self) -> Result<TrustedDiscoveryOutcome, AlpineSdkError> {
match self.trust_state() {
DeviceTrustState::Trusted => Ok(TrustedDiscoveryOutcome { outcome: self }),
DeviceTrustState::UntrustedNoAttestation => Err(AlpineSdkError::UntrustedDevice(
"device identity attestation missing".into(),
)),
DeviceTrustState::UntrustedNoRegistry => Err(AlpineSdkError::UntrustedDevice(
"attester registry not configured".into(),
)),
DeviceTrustState::UntrustedInvalid(reason) => {
Err(AlpineSdkError::UntrustedDevice(reason))
}
}
}
pub fn trust_decision(&self) -> TrustDecision {
let state = self.trust_state();
let reason = match &state {
DeviceTrustState::Trusted => "attestation verified".to_string(),
DeviceTrustState::UntrustedNoAttestation => {
"device identity attestation missing".to_string()
}
DeviceTrustState::UntrustedNoRegistry => "attester registry not configured".to_string(),
DeviceTrustState::UntrustedInvalid(reason) => reason.clone(),
};
TrustDecision { state, reason }
}
pub fn protocol_report(&self) -> ProtocolNegotiationReport {
let client_version = alpine::messages::ALPINE_VERSION.to_string();
let device_version = self.reply.alpine_version.clone();
let compatible = client_version == device_version;
let note = if compatible {
"protocol versions match".to_string()
} else {
format!(
"client {} vs device {}; upgrade/downgrade may be required",
client_version, device_version
)
};
ProtocolNegotiationReport {
client_version,
device_version,
compatible,
note,
}
}
pub fn require_capabilities(
&self,
required: &alpine::messages::CapabilitySet,
) -> Result<(), AlpineSdkError> {
validate_capability_subset(required, &self.reply.capabilities)
}
}
pub struct DiscoveryClient {
socket: UdpSocket,
remote_addr: SocketAddr,
prefer_multicast: bool,
allow_broadcast: bool,
ipv6: bool,
interface: Option<String>,
attester_registry: Option<AttesterRegistry>,
}
impl DiscoveryClient {
pub fn new(options: DiscoveryClientOptions) -> Result<Self, DiscoveryError> {
let domain = if options.remote_addr.is_ipv4() {
Domain::IPV4
} else {
Domain::IPV6
};
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
if options.allow_broadcast && options.remote_addr.is_ipv4() {
socket.set_broadcast(true)?;
}
socket.bind(&options.local_addr.into())?;
let socket: UdpSocket = socket.into();
socket.set_read_timeout(Some(options.timeout))?;
if options.prefer_multicast || options.remote_addr.ip().is_multicast() {
let _ = socket.set_multicast_ttl_v4(4);
}
let local_addr = socket.local_addr().unwrap_or(options.local_addr);
info!(
"[ALPINE][DISCOVERY][SOCKET] discovery socket created local_addr={} remote_addr={} prefer_multicast={}",
local_addr,
options.remote_addr,
options.prefer_multicast
);
Ok(Self {
socket,
remote_addr: options.remote_addr,
prefer_multicast: options.prefer_multicast,
allow_broadcast: options.allow_broadcast,
ipv6: options.remote_addr.is_ipv6() || options.local_addr.is_ipv6(),
interface: options.interface.clone(),
attester_registry: options.attester_registry.clone(),
})
}
pub fn discover(&self, requested: &[String]) -> Result<DiscoveryOutcome, DiscoveryError> {
self.discover_with_report(requested).into_result()
}
pub fn discover_with_report(&self, requested: &[String]) -> DiscoveryResult {
let phase = current_phase();
if phase == Phase::Handshake {
warn!(
"[ALPINE][BUG] discovery attempted during handshake phase (no sends expected); local_addr={}",
self.socket
.local_addr()
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)))
);
}
let mut diagnostics = DiscoveryDiagnostics::new();
let mut nonce = vec![0u8; 32];
OsRng.fill_bytes(&mut nonce);
let request = DiscoveryRequest::new(requested.to_vec(), nonce.clone());
let payload = match serde_cbor::to_vec(&request) {
Ok(payload) => payload,
Err(err) => {
return DiscoveryResult {
outcome: None,
diagnostics,
error: Some(DiscoveryError::from(err)),
};
}
};
let payload_len = payload.len();
debug_assert!(
payload_len > 8,
"discovery payload unexpectedly small; framing may have drifted"
);
if payload_len > DISCOVERY_SAFE_MTU {
warn!(
"[ALPINE][DISCOVERY][WARN] payload_len={} exceeds safe MTU {}; fragmentation likely",
payload_len, DISCOVERY_SAFE_MTU
);
}
let mut send_error: Option<DiscoveryError> = None;
let mut sent = false;
for target in self.discovery_targets() {
let mode = classify_target(target, self.remote_addr);
let local_bind_addr = self
.socket
.local_addr()
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
info!(
"[ALPINE][DISCOVERY][TX][attempt] target={} mode={} local_bind={} payload_len={}",
target,
mode.as_str(),
local_bind_addr,
payload.len()
);
match self.socket.send_to(&payload, target) {
Ok(bytes) => {
info!(
"[ALPINE][DISCOVERY][TX][result] target={} mode={} local_bind={} bytes_sent={}",
target,
mode.as_str(),
local_bind_addr,
bytes
);
diagnostics.attempts.push(DiscoveryAttempt {
target,
mode,
local_bind: local_bind_addr,
payload_len,
bytes_sent: Some(bytes),
error: None,
});
sent = true;
}
Err(err) => {
let kind = err.kind();
let message = err.to_string();
let mapped = self.map_send_error(err, target);
warn!(
"[ALPINE][DISCOVERY][TX][result] target={} mode={} local_bind={} phase={} error={}",
target,
mode.as_str(),
local_bind_addr,
phase.label(),
mapped
);
diagnostics.attempts.push(DiscoveryAttempt {
target,
mode,
local_bind: local_bind_addr,
payload_len,
bytes_sent: None,
error: Some(DiscoveryErrorDetails {
label: mapped.label(),
hint: mapped.hint(),
kind: Some(kind),
message: Some(message),
}),
});
send_error = Some(mapped);
if !self.should_continue_after_error(&kind) {
return DiscoveryResult {
outcome: None,
diagnostics,
error: send_error,
};
}
}
}
}
if !sent {
let error = send_error.unwrap_or(DiscoveryError::PermissionDenied);
warn!(
"[ALPINE][DISCOVERY][WARN] no discovery payloads sent; possible UDP egress block"
);
return DiscoveryResult {
outcome: None,
diagnostics,
error: Some(error),
};
}
let timeout_ms = self
.socket
.read_timeout()
.ok()
.flatten()
.map(|d| d.as_millis())
.unwrap_or_default();
let local_port = self
.socket
.local_addr()
.map(|addr| addr.port())
.unwrap_or_default();
info!(
"[ALPINE][DISCOVERY] awaiting reply timeout_ms={} local_port={} remote_hint={}",
timeout_ms, local_port, self.remote_addr
);
let mut buf = vec![0u8; 2048];
let (len, peer) = match self.socket.recv_from(&mut buf) {
Ok(res) => res,
Err(err) => {
let kind = err.kind();
let message = err.to_string();
let mapped = self.map_recv_error(err);
if matches!(mapped, DiscoveryError::Timeout) && sent {
warn!(
"[ALPINE][DISCOVERY][WARN] discovery sends ok but no replies; inbound UDP may be blocked"
);
}
diagnostics.recv_error = Some(DiscoveryErrorDetails {
label: mapped.label(),
hint: mapped.hint(),
kind: Some(kind),
message: Some(message),
});
return DiscoveryResult {
outcome: None,
diagnostics,
error: Some(mapped),
};
}
};
let reply: DiscoveryReply = match serde_cbor::from_slice(&buf[..len]) {
Ok(reply) => reply,
Err(err) => {
return DiscoveryResult {
outcome: None,
diagnostics,
error: Some(DiscoveryError::from(err)),
};
}
};
let local_addr = match self.socket.local_addr() {
Ok(local_addr) => local_addr,
Err(err) => {
return DiscoveryResult {
outcome: None,
diagnostics,
error: Some(DiscoveryError::from(err)),
};
}
};
info!(
"[ALPINE][DISCOVERY][RX] reply received peer={} local_addr={} iface={:?} bytes={}",
peer, local_addr, self.interface, len
);
let (device_identity_trusted, device_identity_attestation_error) =
self.verify_attestation(&reply);
if !reply.device_identity_pubkey.is_empty() {
let peer_ip = peer.ip();
if let Some(reason) = record_identity_change(peer_ip, &reply.device_identity_pubkey) {
warn!(
"[ALPINE][DISCOVERY][WARN] identity change detected peer={} reason={}",
peer, reason
);
let _ = mark_quarantine(peer_ip, reason);
}
}
let outcome = DiscoveryOutcome {
reply: reply.clone(),
peer,
client_nonce: nonce,
local_addr,
device_identity_pubkey: if reply.device_identity_pubkey.is_empty() {
None
} else {
Some(reply.device_identity_pubkey.clone())
},
device_identity_trusted,
device_identity_attestation_error,
interface: self.interface.clone(),
run_id: Uuid::new_v4().to_string(),
};
DiscoveryResult {
outcome: Some(outcome),
diagnostics,
error: None,
}
}
fn discovery_targets(&self) -> Vec<SocketAddr> {
let mut targets = Vec::new();
if self.prefer_multicast {
if !self.ipv6 {
if let Ok(addr) = DEFAULT_MULTICAST_IPV4.parse() {
push_if_unique(&mut targets, addr);
}
}
if self.ipv6 {
if let Ok(addr) = DEFAULT_MULTICAST_IPV6.parse() {
push_if_unique(&mut targets, addr);
}
}
}
push_if_unique(&mut targets, self.remote_addr);
if self.allow_broadcast && self.remote_addr.ip().is_ipv4() && !self.ipv6 {
if let Ok(addr) = DEFAULT_BROADCAST_IPV4.parse() {
push_if_unique(&mut targets, addr);
}
}
targets
}
fn map_send_error(&self, err: io::Error, target: SocketAddr) -> DiscoveryError {
match err.kind() {
io::ErrorKind::PermissionDenied => {
if target.ip().is_multicast() {
DiscoveryError::MulticastUnavailable
} else if is_broadcast_addr(target.ip()) {
DiscoveryError::BroadcastBlocked
} else {
DiscoveryError::PermissionDenied
}
}
io::ErrorKind::ConnectionReset | io::ErrorKind::WouldBlock => {
DiscoveryError::PermissionDenied
}
_ => DiscoveryError::Io(err),
}
}
fn map_recv_error(&self, err: io::Error) -> DiscoveryError {
match err.kind() {
io::ErrorKind::TimedOut => DiscoveryError::Timeout,
io::ErrorKind::PermissionDenied | io::ErrorKind::ConnectionReset => {
DiscoveryError::PermissionDenied
}
io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
_ => DiscoveryError::Io(err),
}
}
fn should_continue_after_error(&self, kind: &io::ErrorKind) -> bool {
matches!(
kind,
io::ErrorKind::PermissionDenied
| io::ErrorKind::WouldBlock
| io::ErrorKind::ConnectionReset
)
}
fn verify_attestation(&self, reply: &DiscoveryReply) -> (bool, Option<String>) {
if reply.device_identity_attestation.is_empty() {
return (false, Some("device identity attestation missing".into()));
}
let Some(registry) = &self.attester_registry else {
return (false, Some("attester registry not configured".into()));
};
match verify_device_identity_attestation(reply, registry, std::time::SystemTime::now()) {
Ok(_) => (true, None),
Err(err) => {
warn!(
"[ALPINE][DISCOVERY][TRUST] attestation verification failed device_id={} err={}",
reply.device_id,
err
);
(false, Some(err.to_string()))
}
}
}
}
fn record_identity_change(peer: IpAddr, pubkey: &[u8]) -> Option<String> {
let mut guard = identity_map()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(existing) = guard.get(&peer) {
if existing != pubkey {
guard.insert(peer, pubkey.to_vec());
return Some("device identity pubkey changed for peer".to_string());
}
return None;
}
guard.insert(peer, pubkey.to_vec());
None
}
impl Drop for DiscoveryClient {
fn drop(&mut self) {
if let Ok(local) = self.socket.local_addr() {
info!(
"[ALPINE][DISCOVERY][SOCKET] discovery socket dropped local_addr={} remote_addr={}",
local, self.remote_addr
);
} else {
info!(
"[ALPINE][DISCOVERY][SOCKET] discovery socket dropped remote_addr={}",
self.remote_addr
);
}
}
}
fn is_broadcast_addr(ip: IpAddr) -> bool {
matches!(ip, IpAddr::V4(addr) if addr.is_broadcast())
}
fn push_if_unique(targets: &mut Vec<SocketAddr>, candidate: SocketAddr) {
if !targets.contains(&candidate) {
targets.push(candidate);
}
}
fn classify_target(target: SocketAddr, configured: SocketAddr) -> DiscoveryTargetKind {
if target.ip().is_multicast() {
DiscoveryTargetKind::Multicast
} else if is_broadcast_addr(target.ip()) {
DiscoveryTargetKind::Broadcast
} else if target == configured {
DiscoveryTargetKind::UnicastConfigured
} else {
DiscoveryTargetKind::UnicastFallback
}
}
fn validate_capability_subset(
required: &alpine::messages::CapabilitySet,
device: &alpine::messages::CapabilitySet,
) -> Result<(), AlpineSdkError> {
for format in required.channel_formats.iter() {
if !device.channel_formats.contains(format) {
return Err(AlpineSdkError::InvalidCapabilities(format!(
"channel format {:?} not supported by device",
format
)));
}
}
if required.max_channels > device.max_channels {
return Err(AlpineSdkError::InvalidCapabilities(format!(
"max channels {} exceeds device max {}",
required.max_channels, device.max_channels
)));
}
if required.grouping_supported && !device.grouping_supported {
return Err(AlpineSdkError::InvalidCapabilities(
"grouping not supported by device".into(),
));
}
if required.streaming_supported && !device.streaming_supported {
return Err(AlpineSdkError::InvalidCapabilities(
"streaming not supported by device".into(),
));
}
if required.encryption_supported && !device.encryption_supported {
return Err(AlpineSdkError::InvalidCapabilities(
"encryption not supported by device".into(),
));
}
if let Some(extensions) = required.vendor_extensions.as_ref() {
if let Some(device_extensions) = device.vendor_extensions.as_ref() {
for key in extensions.keys() {
if !device_extensions.contains_key(key) {
return Err(AlpineSdkError::InvalidCapabilities(format!(
"vendor extension {} not supported by device",
key
)));
}
}
} else {
return Err(AlpineSdkError::InvalidCapabilities(
"vendor extensions not supported by device".into(),
));
}
}
Ok(())
}