mod error;
mod event_publisher;
mod runtime;
mod sd_state;
mod service_info;
mod subscription_manager;
pub use error::Error;
pub use event_publisher::EventPublisher;
pub use service_info::Subscriber;
#[cfg(feature = "std")]
pub use service_info::{EventGroupInfo, ServiceInfo};
#[cfg(feature = "bare_metal")]
pub use subscription_manager::{StaticSubscriptionHandle, StaticSubscriptionStorage};
pub use subscription_manager::{SubscribeError, SubscriptionHandle, SubscriptionManager};
pub use sd_state::SdStateManager;
use core::sync::atomic::{AtomicBool, Ordering};
use crate::Timer;
use crate::e2e::{E2EKey, E2EProfile};
#[cfg(feature = "_alloc")]
use crate::protocol::sd;
#[cfg(test)]
use crate::protocol::sd::{Entry, Flags, ServiceEntry};
#[cfg(feature = "_alloc")]
use crate::transport::SocketOptions;
#[cfg(feature = "_alloc")]
use crate::transport::WrappableSharedHandle;
use crate::transport::{E2ERegistryHandle, SharedHandle, TransportFactory, TransportSocket};
#[cfg(feature = "_alloc")]
use alloc::sync::Arc;
use core::net::Ipv4Addr;
#[cfg(feature = "_alloc")]
use core::net::SocketAddrV4;
#[cfg(test)]
use std::vec::Vec;
#[cfg(feature = "server-tokio")]
use crate::e2e::E2ERegistry;
#[cfg(feature = "server-tokio")]
use std::sync::Mutex;
#[cfg(feature = "server-tokio")]
use tokio::sync::RwLock;
#[cfg(feature = "bare_metal")]
const _DEFAULT_EVENT_GROUP_IDS: usize = 1;
#[cfg(not(feature = "bare_metal"))]
const _DEFAULT_EVENT_GROUP_IDS: usize = 32;
#[cfg(feature = "bare_metal")]
const _DEFAULT_ACCEPTED_OFFERS: usize = 4;
#[cfg(not(feature = "bare_metal"))]
const _DEFAULT_ACCEPTED_OFFERS: usize = 16;
const _SERVER_EVENT_GROUP_IDS_CAP: usize = crate::from_env_or(
option_env!("SIMPLE_SOMEIP_MAX_SUBS"),
_DEFAULT_EVENT_GROUP_IDS,
);
const _SERVER_ACCEPTED_OFFERS_CAP: usize = crate::from_env_or(
option_env!("SIMPLE_SOMEIP_MAX_OFFERS"),
_DEFAULT_ACCEPTED_OFFERS,
);
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub interface: Ipv4Addr,
pub local_port: u16,
pub service_id: u16,
pub instance_id: u16,
pub major_version: u8,
pub minor_version: u32,
pub ttl: u32,
pub event_group_ids: heapless::Vec<u16, { ServerConfig::EVENT_GROUP_IDS_CAP }>,
pub announce: bool,
pub accepted_offers: heapless::Vec<AcceptedOffer, { ServerConfig::ACCEPTED_OFFERS_CAP }>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AcceptedOffer {
pub service_id: u16,
pub instance_id: u16,
pub major_version: u8,
pub event_group_id: u16,
}
impl ServerConfig {
pub const EVENT_GROUP_IDS_CAP: usize = _SERVER_EVENT_GROUP_IDS_CAP;
pub const ACCEPTED_OFFERS_CAP: usize = _SERVER_ACCEPTED_OFFERS_CAP;
pub const SUBSCRIBERS_PER_GROUP_CAP: usize = subscription_manager::SUBSCRIBERS_PER_GROUP;
#[must_use]
pub fn new(service_id: u16, instance_id: u16) -> Self {
Self {
interface: Ipv4Addr::UNSPECIFIED,
local_port: 0,
service_id,
instance_id,
major_version: 1,
minor_version: 0,
ttl: 3, event_group_ids: heapless::Vec::new(),
announce: true,
accepted_offers: heapless::Vec::new(),
}
}
#[must_use]
pub fn with_interface(mut self, interface: Ipv4Addr) -> Self {
self.interface = interface;
self
}
#[must_use]
pub fn with_local_port(mut self, local_port: u16) -> Self {
self.local_port = local_port;
self
}
#[must_use]
pub fn accepts_event_group(&self, event_group_id: u16) -> bool {
self.event_group_ids.is_empty() || self.event_group_ids.contains(&event_group_id)
}
#[must_use]
pub fn with_accepted_offer(
mut self,
service_id: u16,
instance_id: u16,
major_version: u8,
event_group_id: u16,
) -> Self {
self.accepted_offers
.push(AcceptedOffer {
service_id,
instance_id,
major_version,
event_group_id,
})
.expect("accepted_offers capacity exceeded");
self
}
#[allow(clippy::result_large_err)]
#[must_use = "the returned `Result` carries the (possibly-modified) config — drop is silent"]
pub fn try_with_accepted_offer(
mut self,
service_id: u16,
instance_id: u16,
major_version: u8,
event_group_id: u16,
) -> Result<Self, Self> {
if self
.accepted_offers
.push(AcceptedOffer {
service_id,
instance_id,
major_version,
event_group_id,
})
.is_ok()
{
Ok(self)
} else {
Err(self)
}
}
#[must_use]
pub fn accepts_offer(
&self,
service_id: u16,
instance_id: u16,
major_version: u8,
event_group_id: u16,
) -> bool {
self.accepted_offers.iter().any(|o| {
o.service_id == service_id
&& o.instance_id == instance_id
&& o.major_version == major_version
&& o.event_group_id == event_group_id
})
}
#[must_use]
pub fn with_major_version(mut self, major_version: u8) -> Self {
self.major_version = major_version;
self
}
#[must_use]
pub fn with_minor_version(mut self, minor_version: u32) -> Self {
self.minor_version = minor_version;
self
}
#[must_use]
pub fn with_ttl(mut self, ttl: core::time::Duration) -> Self {
self.ttl = u32::try_from(ttl.as_secs()).unwrap_or(u32::MAX);
self
}
#[must_use]
pub fn with_event_group(mut self, event_group_id: u16) -> Self {
self.event_group_ids
.push(event_group_id)
.expect("event_group_ids capacity exceeded");
self
}
#[allow(clippy::result_large_err)]
#[must_use = "the returned `Result` carries the (possibly-modified) config — drop is silent"]
pub fn try_with_event_group(mut self, event_group_id: u16) -> Result<Self, Self> {
if self.event_group_ids.push(event_group_id).is_ok() {
Ok(self)
} else {
Err(self)
}
}
#[must_use]
pub fn with_announce(mut self, announce: bool) -> Self {
self.announce = announce;
self
}
}
pub struct ServerDeps<F, Tm, R, Sub>
where
F: TransportFactory,
Tm: Timer,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
{
pub factory: F,
pub timer: Tm,
pub e2e_registry: R,
pub subscriptions: Sub,
pub non_sd_observer: Option<(NonSdRequestCallback, usize)>,
}
#[cfg(feature = "server-tokio")]
impl
ServerDeps<
crate::tokio_transport::TokioTransport,
crate::tokio_transport::TokioTimer,
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
>
{
#[must_use]
pub fn tokio() -> Self {
Self {
factory: crate::tokio_transport::TokioTransport,
timer: crate::tokio_transport::TokioTimer,
e2e_registry: Arc::new(Mutex::new(E2ERegistry::new())),
subscriptions: Arc::new(RwLock::new(SubscriptionManager::new())),
non_sd_observer: None,
}
}
}
impl<F, Tm, R, Sub> ServerDeps<F, Tm, R, Sub>
where
F: TransportFactory,
Tm: Timer,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
{
pub fn with_factory<F2: TransportFactory>(self, factory: F2) -> ServerDeps<F2, Tm, R, Sub> {
ServerDeps {
factory,
timer: self.timer,
e2e_registry: self.e2e_registry,
subscriptions: self.subscriptions,
non_sd_observer: self.non_sd_observer,
}
}
pub fn with_timer<Tm2: Timer>(self, timer: Tm2) -> ServerDeps<F, Tm2, R, Sub> {
ServerDeps {
factory: self.factory,
timer,
e2e_registry: self.e2e_registry,
subscriptions: self.subscriptions,
non_sd_observer: self.non_sd_observer,
}
}
pub fn with_e2e_registry<R2: E2ERegistryHandle>(
self,
e2e_registry: R2,
) -> ServerDeps<F, Tm, R2, Sub> {
ServerDeps {
factory: self.factory,
timer: self.timer,
e2e_registry,
subscriptions: self.subscriptions,
non_sd_observer: self.non_sd_observer,
}
}
pub fn with_subscriptions<Sub2: SubscriptionHandle>(
self,
subscriptions: Sub2,
) -> ServerDeps<F, Tm, R, Sub2> {
ServerDeps {
factory: self.factory,
timer: self.timer,
e2e_registry: self.e2e_registry,
subscriptions,
non_sd_observer: self.non_sd_observer,
}
}
#[must_use]
pub fn with_non_sd_observer(mut self, observer: Option<(NonSdRequestCallback, usize)>) -> Self {
self.non_sd_observer = observer;
self
}
}
pub struct ServerHandles<Hep> {
pub publisher: Hep,
}
pub struct ServerStorage<F, Tm, R, Sub, H, Hsd, Hep>
where
F: TransportFactory + 'static,
Tm: Timer,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
H: SharedHandle<F::Socket>,
Hsd: SharedHandle<SdStateManager>,
Hep: SharedHandle<EventPublisher<R, Sub, H, F::Socket>>,
{
pub factory: F,
pub timer: Tm,
pub e2e_registry: R,
pub subscriptions: Sub,
pub unicast_socket: H,
pub sd_socket: H,
pub sd_state: Hsd,
pub publisher: Hep,
pub started: StartedLatch,
pub non_sd_observer: Option<(NonSdRequestCallback, usize)>,
}
#[cfg(feature = "_alloc")]
type DefaultSocketHandle<F> = Arc<<F as TransportFactory>::Socket>;
#[cfg(not(feature = "_alloc"))]
type DefaultSocketHandle<F> = &'static <F as TransportFactory>::Socket;
#[cfg(feature = "_alloc")]
type DefaultSdStateHandle = Arc<SdStateManager>;
#[cfg(not(feature = "_alloc"))]
type DefaultSdStateHandle = &'static SdStateManager;
#[cfg(feature = "_alloc")]
type DefaultEventPublisherHandle<R, Sub, H, T> = Arc<EventPublisher<R, Sub, H, T>>;
#[cfg(not(feature = "_alloc"))]
type DefaultEventPublisherHandle<R, Sub, H, T> = &'static EventPublisher<R, Sub, H, T>;
pub struct Server<
F,
Tm,
R,
Sub,
H = DefaultSocketHandle<F>,
Hsd = DefaultSdStateHandle,
Hep = DefaultEventPublisherHandle<R, Sub, H, <F as TransportFactory>::Socket>,
> where
F: TransportFactory + 'static,
F::Socket: 'static,
Tm: Timer + Clone + 'static,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
H: SharedHandle<F::Socket>,
Hsd: SharedHandle<SdStateManager>,
Hep: SharedHandle<EventPublisher<R, Sub, H, F::Socket>>,
{
config: ServerConfig,
unicast_socket: H,
sd_socket: H,
subscriptions: Sub,
publisher: Hep,
sd_state: Hsd,
e2e_registry: R,
#[allow(dead_code)]
factory: F,
timer: Tm,
is_passive: bool,
started: StartedLatch,
non_sd_observer: Option<(NonSdRequestCallback, usize)>,
}
pub type NonSdRequestCallback = fn(
ctx: usize,
source: core::net::SocketAddrV4,
service_id: u16,
method_id: u16,
payload: &[u8],
e2e_status: u8,
response_out: &mut [u8],
) -> i32;
#[cfg(feature = "_alloc")]
type StartedLatch = Arc<AtomicBool>;
#[cfg(not(feature = "_alloc"))]
type StartedLatch = &'static AtomicBool;
#[cfg(feature = "server-tokio")]
type DefaultTokioServerHep = Arc<
EventPublisher<
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
Arc<crate::tokio_transport::TokioSocket>,
crate::tokio_transport::TokioSocket,
>,
>;
#[cfg(feature = "server-tokio")]
impl
Server<
crate::tokio_transport::TokioTransport,
crate::tokio_transport::TokioTimer,
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
>
{
pub async fn new(
config: ServerConfig,
) -> Result<
(
Self,
ServerHandles<DefaultTokioServerHep>,
impl core::future::Future<Output = Result<(), Error>> + 'static,
),
Error,
> {
Self::new_with_loopback(config, false).await
}
pub async fn new_with_loopback(
config: ServerConfig,
multicast_loopback: bool,
) -> Result<
(
Self,
ServerHandles<DefaultTokioServerHep>,
impl core::future::Future<Output = Result<(), Error>> + 'static,
),
Error,
> {
let deps = ServerDeps {
factory: crate::tokio_transport::TokioTransport,
timer: crate::tokio_transport::TokioTimer,
e2e_registry: Arc::new(Mutex::new(E2ERegistry::new())),
subscriptions: Arc::new(RwLock::new(SubscriptionManager::new())),
non_sd_observer: None,
};
Self::new_with_deps(deps, config, multicast_loopback).await
}
pub async fn new_passive(
config: ServerConfig,
) -> Result<
(
Self,
ServerHandles<DefaultTokioServerHep>,
impl core::future::Future<Output = Result<(), Error>> + 'static,
),
Error,
> {
let deps = ServerDeps {
factory: crate::tokio_transport::TokioTransport,
timer: crate::tokio_transport::TokioTimer,
e2e_registry: Arc::new(Mutex::new(E2ERegistry::new())),
subscriptions: Arc::new(RwLock::new(SubscriptionManager::new())),
non_sd_observer: None,
};
Self::new_passive_with_deps(deps, config).await
}
}
#[cfg(feature = "_alloc")]
impl<F, Tm, R, Sub, H, Hsd, Hep> Server<F, Tm, R, Sub, H, Hsd, Hep>
where
F: TransportFactory + 'static,
F::Socket: 'static,
Tm: Timer + Clone + 'static,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
H: WrappableSharedHandle<F::Socket>,
Hsd: WrappableSharedHandle<SdStateManager>,
Hep: WrappableSharedHandle<EventPublisher<R, Sub, H, F::Socket>>,
{
pub async fn new_with_deps(
deps: ServerDeps<F, Tm, R, Sub>,
mut config: ServerConfig,
multicast_loopback: bool,
) -> Result<
(
Self,
ServerHandles<Hep>,
impl core::future::Future<Output = Result<(), Error>> + 'static,
),
Error,
> {
let ServerDeps {
factory,
timer,
e2e_registry,
subscriptions,
non_sd_observer: deps_non_sd_observer,
} = deps;
let unicast_addr = SocketAddrV4::new(config.interface, config.local_port);
let unicast_raw = factory.bind(unicast_addr, &SocketOptions::new()).await?;
let bound_port = unicast_raw.local_addr()?.port();
let unicast_socket: H = H::wrap(unicast_raw);
config.local_port = bound_port;
crate::log::info!(
"Server bound to {}:{} for service 0x{:04X}",
config.interface,
bound_port,
config.service_id
);
let mut sd_opts = SocketOptions::new();
sd_opts.reuse_address = true;
sd_opts.reuse_port = true;
sd_opts.multicast_if_v4 = Some(config.interface);
sd_opts.multicast_loop_v4 = Some(multicast_loopback);
let sd_addr = SocketAddrV4::new(config.interface, sd::MULTICAST_PORT);
let sd_raw = factory.bind(sd_addr, &sd_opts).await?;
sd_raw.join_multicast_v4(sd::MULTICAST_IP, config.interface)?;
let sd_socket: H = H::wrap(sd_raw);
crate::log::info!(
"Server SD socket bound to {} (expected port {}), joined multicast {}",
sd_addr,
sd::MULTICAST_PORT,
sd::MULTICAST_IP
);
let publisher = Hep::wrap(EventPublisher::new(
subscriptions.clone(),
unicast_socket.clone(),
e2e_registry.clone(),
));
let server = Self {
config,
unicast_socket,
sd_socket,
subscriptions,
publisher,
sd_state: Hsd::wrap(SdStateManager::new()),
e2e_registry,
factory,
timer,
is_passive: false,
started: Arc::new(AtomicBool::new(false)),
non_sd_observer: deps_non_sd_observer,
};
let handles = ServerHandles {
publisher: server.publisher(),
};
let run = server.run_inner();
Ok((server, handles, run))
}
pub async fn new_passive_with_deps(
deps: ServerDeps<F, Tm, R, Sub>,
mut config: ServerConfig,
) -> Result<
(
Self,
ServerHandles<Hep>,
impl core::future::Future<Output = Result<(), Error>> + 'static,
),
Error,
> {
let ServerDeps {
factory,
timer,
e2e_registry,
subscriptions,
non_sd_observer: deps_non_sd_observer,
} = deps;
let unicast_addr = SocketAddrV4::new(config.interface, config.local_port);
let unicast_raw = factory.bind(unicast_addr, &SocketOptions::new()).await?;
let bound_port = unicast_raw.local_addr()?.port();
let unicast_socket: H = H::wrap(unicast_raw);
config.local_port = bound_port;
crate::log::info!(
"Passive server bound to {}:{} for service 0x{:04X}",
config.interface,
bound_port,
config.service_id
);
let sd_placeholder_addr = SocketAddrV4::new(config.interface, 0);
let sd_socket: H = H::wrap(
factory
.bind(sd_placeholder_addr, &SocketOptions::new())
.await?,
);
crate::log::info!(
"Passive server SD placeholder socket bound near {} (not in SD reuseport group)",
sd_placeholder_addr
);
let publisher = Hep::wrap(EventPublisher::new(
subscriptions.clone(),
unicast_socket.clone(),
e2e_registry.clone(),
));
let server = Self {
config,
unicast_socket,
sd_socket,
subscriptions,
publisher,
sd_state: Hsd::wrap(SdStateManager::new()),
e2e_registry,
factory,
timer,
is_passive: true,
started: Arc::new(AtomicBool::new(false)),
non_sd_observer: deps_non_sd_observer,
};
let handles = ServerHandles {
publisher: server.publisher(),
};
let run = server.run_inner();
Ok((server, handles, run))
}
}
impl<F, Tm, R, Sub, H, Hsd, Hep> Server<F, Tm, R, Sub, H, Hsd, Hep>
where
F: TransportFactory + 'static,
F::Socket: 'static,
Tm: Timer + Clone + 'static,
R: E2ERegistryHandle,
Sub: SubscriptionHandle,
H: SharedHandle<F::Socket>,
Hsd: SharedHandle<SdStateManager>,
Hep: SharedHandle<EventPublisher<R, Sub, H, F::Socket>>,
{
pub fn new_with_handles(
deps: ServerStorage<F, Tm, R, Sub, H, Hsd, Hep>,
mut config: ServerConfig,
) -> Result<Self, Error> {
let bound_port = deps.unicast_socket.get().local_addr()?.port();
if config.local_port == 0 {
config.local_port = bound_port;
} else if config.local_port != bound_port {
crate::log::error!(
"ServerConfig.local_port ({}) does not match unicast socket's \
bound port ({}); SD offers would lie. Pass local_port = 0 to \
auto-fill from the bound port instead.",
config.local_port,
bound_port,
);
return Err(Error::InvalidUsage("new_with_handles_local_port_mismatch"));
}
crate::log::info!(
"Server (handles) bound to {}:{} for service 0x{:04X}",
config.interface,
bound_port,
config.service_id
);
Ok(Self {
config,
unicast_socket: deps.unicast_socket,
sd_socket: deps.sd_socket,
subscriptions: deps.subscriptions,
publisher: deps.publisher,
sd_state: deps.sd_state,
e2e_registry: deps.e2e_registry,
factory: deps.factory,
timer: deps.timer,
is_passive: false,
started: deps.started,
non_sd_observer: deps.non_sd_observer,
})
}
pub fn new_passive_with_handles(
deps: ServerStorage<F, Tm, R, Sub, H, Hsd, Hep>,
mut config: ServerConfig,
) -> Result<Self, Error> {
let bound_port = deps.unicast_socket.get().local_addr()?.port();
if config.local_port == 0 {
config.local_port = bound_port;
} else if config.local_port != bound_port {
crate::log::error!(
"ServerConfig.local_port ({}) does not match unicast socket's \
bound port ({}); event publishers would advertise a port \
nothing is listening on. Pass local_port = 0 to auto-fill.",
config.local_port,
bound_port,
);
return Err(Error::InvalidUsage(
"new_passive_with_handles_local_port_mismatch",
));
}
crate::log::info!(
"Passive server (handles) bound to {}:{} for service 0x{:04X}",
config.interface,
bound_port,
config.service_id
);
Ok(Self {
config,
unicast_socket: deps.unicast_socket,
sd_socket: deps.sd_socket,
subscriptions: deps.subscriptions,
publisher: deps.publisher,
sd_state: deps.sd_state,
e2e_registry: deps.e2e_registry,
factory: deps.factory,
timer: deps.timer,
is_passive: true,
started: deps.started,
non_sd_observer: deps.non_sd_observer,
})
}
#[must_use]
pub fn publisher(&self) -> Hep {
self.publisher.clone()
}
pub fn unicast_local_addr(&self) -> Result<core::net::SocketAddr, Error> {
match self.unicast_socket.get().local_addr() {
Ok(v4) => Ok(core::net::SocketAddr::V4(v4)),
Err(e) => Err(Error::Transport(e)),
}
}
pub fn register_e2e(
&self,
key: E2EKey,
profile: E2EProfile,
) -> Result<(), crate::e2e::E2ERegistryFull> {
self.e2e_registry.register(key, profile)
}
pub fn unregister_e2e(&self, key: &E2EKey) {
self.e2e_registry.unregister(key);
}
pub fn run_with_buffers<'a>(
&self,
unicast_buf: &'a mut [u8],
sd_buf: &'a mut [u8],
recv_send_buf: &'a mut [u8],
announce_send_buf: &'a mut [u8],
) -> impl core::future::Future<Output = Result<(), Error>> + 'a + use<'a, F, Tm, R, Sub, H, Hsd, Hep>
where
Tm: 'a,
Sub: 'a,
H: 'a,
Hsd: 'a,
{
let config = self.config.clone();
let unicast_socket = self.unicast_socket.clone();
let sd_socket = self.sd_socket.clone();
let subscriptions = self.subscriptions.clone();
let e2e_registry = self.e2e_registry.clone();
let sd_state = self.sd_state.clone();
let timer = self.timer.clone();
let is_passive = self.is_passive;
let non_sd_observer = self.non_sd_observer;
#[allow(noop_method_call)]
let started = self.started.clone();
async move {
if started
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
crate::log::warn!(
"Server::run_with_buffers already started for service 0x{:04X}; \
a second run-future cannot share the same sockets \
and session counter",
config.service_id
);
return Err(Error::InvalidUsage("server_already_running"));
}
runtime::run_combined::<H, F::Socket, Sub, Hsd, Tm, R>(
config,
unicast_socket,
sd_socket,
subscriptions,
sd_state,
e2e_registry,
timer,
is_passive,
unicast_buf,
sd_buf,
recv_send_buf,
announce_send_buf,
non_sd_observer,
)
.await
}
}
pub fn announce_only_with_buffer<'a>(
&self,
announce_send_buf: &'a mut [u8],
) -> impl core::future::Future<Output = ()> + 'a + use<'a, F, Tm, R, Sub, H, Hsd, Hep>
where
Tm: 'a,
Hsd: 'a,
H: 'a,
{
let config = self.config.clone();
let sd_socket = self.sd_socket.clone();
let sd_state = self.sd_state.clone();
let timer = self.timer.clone();
async move {
runtime::announce_loop(
&config,
sd_socket.get(),
sd_state.get(),
&timer,
announce_send_buf,
)
.await;
}
}
#[cfg(feature = "_alloc")]
pub fn announce_only_future<'a>(
&self,
) -> impl core::future::Future<Output = ()> + 'a + use<'a, F, Tm, R, Sub, H, Hsd, Hep>
where
Tm: 'a,
Hsd: 'a,
H: 'a,
{
let config = self.config.clone();
let sd_socket = self.sd_socket.clone();
let sd_state = self.sd_state.clone();
let timer = self.timer.clone();
async move {
let mut announce_send_buf = alloc::vec![0u8; crate::UDP_BUFFER_SIZE];
runtime::announce_loop(
&config,
sd_socket.get(),
sd_state.get(),
&timer,
&mut announce_send_buf,
)
.await;
}
}
#[cfg(feature = "_alloc")]
pub fn run(
&self,
) -> impl core::future::Future<Output = Result<(), Error>>
+ Send
+ 'static
+ use<F, Tm, R, Sub, H, Hsd, Hep>
where
F: Send + Sync,
F::Socket: Send + Sync,
for<'a> <F::Socket as TransportSocket>::SendFuture<'a>: Send,
for<'a> <F::Socket as TransportSocket>::RecvFuture<'a>: Send,
H: Send + Sync,
Sub: Send + Sync,
for<'a> Sub::SubscribeFuture<'a>: Send,
for<'a> Sub::UnsubscribeFuture<'a>: Send,
R: Send + Sync,
Tm: Send + Sync,
for<'a> Tm::SleepFuture<'a>: Send,
Hsd: Send + Sync,
Hep: Send + Sync,
{
self.run_inner()
}
#[cfg(feature = "_alloc")]
fn run_inner(
&self,
) -> impl core::future::Future<Output = Result<(), Error>> + 'static + use<F, Tm, R, Sub, H, Hsd, Hep>
{
let config = self.config.clone();
let unicast_socket = self.unicast_socket.clone();
let sd_socket = self.sd_socket.clone();
let subscriptions = self.subscriptions.clone();
let e2e_registry = self.e2e_registry.clone();
let sd_state = self.sd_state.clone();
let timer = self.timer.clone();
let is_passive = self.is_passive;
let non_sd_observer = self.non_sd_observer;
let started = self.started.clone();
async move {
if started
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
crate::log::warn!(
"Server::run already started for service 0x{:04X}; \
a second run-future cannot share the same sockets \
and session counter",
config.service_id
);
return Err(Error::InvalidUsage("server_already_running"));
}
let mut unicast_buf = alloc::vec![0u8; 65535];
let mut sd_buf = alloc::vec![0u8; 65535];
let mut recv_send_buf = alloc::vec![0u8; crate::UDP_BUFFER_SIZE];
let mut announce_send_buf = alloc::vec![0u8; crate::UDP_BUFFER_SIZE];
runtime::run_combined::<H, F::Socket, Sub, Hsd, Tm, R>(
config,
unicast_socket,
sd_socket,
subscriptions,
sd_state,
e2e_registry,
timer,
is_passive,
&mut unicast_buf,
&mut sd_buf,
&mut recv_send_buf,
&mut announce_send_buf,
non_sd_observer,
)
.await
}
}
}
#[cfg(all(test, feature = "server-tokio"))]
mod tests {
use super::*;
use crate::protocol::{
Header as SomeIpHeader, MessageType, MessageTypeField, MessageView, ReturnCode,
};
use crate::tokio_transport::{TokioTimer, TokioTransport};
use crate::traits::WireFormat;
use std::format;
use std::net::IpAddr;
use std::vec;
use tokio::net::UdpSocket;
type TestServer = Server<
TokioTransport,
TokioTimer,
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
>;
#[tokio::test]
async fn test_server_creation() {
let config = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30682);
let result = TestServer::new(config).await;
assert!(result.is_ok());
}
#[test]
fn server_config_builder_chain_overrides_each_field() {
let cfg = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30683)
.with_major_version(2)
.with_minor_version(7)
.with_ttl(core::time::Duration::from_secs(10))
.with_event_group(0x42)
.with_event_group(0x43);
assert_eq!(cfg.interface, Ipv4Addr::LOCALHOST);
assert_eq!(cfg.local_port, 30683);
assert_eq!(cfg.major_version, 2);
assert_eq!(cfg.minor_version, 7);
assert_eq!(cfg.ttl, 10);
assert!(cfg.accepts_event_group(0x42));
assert!(cfg.accepts_event_group(0x43));
assert!(!cfg.accepts_event_group(0x44));
}
#[test]
fn server_config_with_ttl_truncates_subsecond_precision() {
let cfg = ServerConfig::new(0x5B, 1).with_ttl(core::time::Duration::from_millis(2_999));
assert_eq!(cfg.ttl, 2, "sub-second is truncated, not rounded");
}
#[test]
fn server_config_with_announce_toggles_field() {
let default_cfg = ServerConfig::new(0x5B, 1);
assert!(
default_cfg.announce,
"announce must default to true so a fresh `ServerConfig` emits SD offers"
);
let suppressed = default_cfg.clone().with_announce(false);
assert!(
!suppressed.announce,
"with_announce(false) must clear the field"
);
let restored = suppressed.with_announce(true);
assert!(
restored.announce,
"with_announce(true) must re-enable after a previous suppression"
);
}
#[test]
fn server_config_with_ttl_saturates_overflow() {
let cfg = ServerConfig::new(0x5B, 1)
.with_ttl(core::time::Duration::from_secs(u64::from(u32::MAX) + 1));
assert_eq!(cfg.ttl, u32::MAX);
}
#[test]
fn server_config_try_with_event_group_rejects_at_capacity() {
let mut cfg = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30684);
for i in 0..u16::try_from(ServerConfig::EVENT_GROUP_IDS_CAP).unwrap() {
cfg = cfg.try_with_event_group(i).expect("under cap");
}
let cap = ServerConfig::EVENT_GROUP_IDS_CAP;
let result = cfg.try_with_event_group(0xFFFF);
let returned = result.expect_err("at-cap insert must fail");
assert_eq!(returned.event_group_ids.len(), cap);
assert!(!returned.accepts_event_group(0xFFFF));
}
async fn build_test_handles(
unicast_port: u16,
) -> (
ServerStorage<
TokioTransport,
TokioTimer,
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
Arc<crate::tokio_transport::TokioSocket>,
Arc<SdStateManager>,
Arc<
EventPublisher<
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
Arc<crate::tokio_transport::TokioSocket>,
crate::tokio_transport::TokioSocket,
>,
>,
>,
u16, // actual bound port (0 → ephemeral)
) {
let factory = TokioTransport;
let unicast_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, unicast_port);
let unicast_raw = factory
.bind(unicast_addr, &SocketOptions::new())
.await
.expect("bind unicast");
let bound_port = unicast_raw.local_addr().expect("local_addr").port();
let unicast_socket = Arc::new(unicast_raw);
let sd_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
let sd_socket = Arc::new(
factory
.bind(sd_addr, &SocketOptions::new())
.await
.expect("bind sd"),
);
let e2e_registry = Arc::new(Mutex::new(E2ERegistry::new()));
let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new()));
let publisher = Arc::new(EventPublisher::new(
subscriptions.clone(),
unicast_socket.clone(),
e2e_registry.clone(),
));
let handles = ServerStorage {
factory,
timer: TokioTimer,
e2e_registry,
subscriptions,
unicast_socket,
sd_socket,
sd_state: Arc::new(SdStateManager::new()),
publisher,
started: Arc::new(AtomicBool::new(false)),
non_sd_observer: None,
};
(handles, bound_port)
}
#[tokio::test]
async fn new_with_handles_back_fills_local_port_on_zero() {
let (handles, bound_port) = build_test_handles(0).await;
assert_ne!(
bound_port, 0,
"test precondition: kernel must assign a real ephemeral port",
);
let config = ServerConfig::new(0xFE10, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let server = TestServer::new_with_handles(handles, config)
.expect("new_with_handles must accept local_port = 0");
assert_eq!(
server.config.local_port, bound_port,
"config.local_port must be back-filled from the unicast socket's bound port",
);
}
#[tokio::test]
async fn new_with_handles_accepts_matching_local_port() {
let (handles, bound_port) = build_test_handles(0).await;
let config = ServerConfig::new(0xFE11, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(bound_port);
let server = TestServer::new_with_handles(handles, config)
.expect("matching local_port must be accepted");
assert_eq!(server.config.local_port, bound_port);
}
#[tokio::test]
async fn new_with_handles_rejects_local_port_mismatch() {
let (handles, bound_port) = build_test_handles(0).await;
let bogus_port = bound_port.wrapping_add(1);
assert_ne!(bogus_port, bound_port);
let config = ServerConfig::new(0xFE12, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(bogus_port);
let result = TestServer::new_with_handles(handles, config);
match result {
Err(Error::InvalidUsage(tag)) => {
assert_eq!(tag, "new_with_handles_local_port_mismatch");
}
Ok(_) => panic!("non-zero non-matching local_port must be rejected"),
Err(other) => {
panic!(
"expected Error::InvalidUsage(\"new_with_handles_local_port_mismatch\"), got {other:?}"
)
}
}
}
#[tokio::test]
async fn new_passive_with_handles_back_fills_local_port_on_zero() {
let (handles, bound_port) = build_test_handles(0).await;
let config = ServerConfig::new(0xFE13, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let server = TestServer::new_passive_with_handles(handles, config)
.expect("new_passive_with_handles must accept local_port = 0");
assert_eq!(server.config.local_port, bound_port);
assert!(server.is_passive, "passive constructor must set is_passive");
}
#[tokio::test]
async fn new_passive_with_handles_rejects_local_port_mismatch() {
let (handles, bound_port) = build_test_handles(0).await;
let bogus_port = bound_port.wrapping_add(1);
assert_ne!(bogus_port, bound_port);
let config = ServerConfig::new(0xFE14, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(bogus_port);
let result = TestServer::new_passive_with_handles(handles, config);
match result {
Err(Error::InvalidUsage(tag)) => {
assert_eq!(tag, "new_passive_with_handles_local_port_mismatch");
}
Ok(_) => panic!("non-zero non-matching local_port must be rejected"),
Err(other) => panic!("unexpected: {other:?}"),
}
}
#[tokio::test]
async fn passive_server_run_with_buffers_returns_invalid_usage() {
let (handles, _) = build_test_handles(0).await;
let config = ServerConfig::new(0xFE15, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let server = TestServer::new_passive_with_handles(handles, config).expect("passive ctor");
let mut unicast_buf = vec![0u8; 1500];
let mut sd_buf = vec![0u8; 1500];
let mut recv_send_buf = vec![0u8; 1500];
let mut announce_send_buf = vec![0u8; 1500];
let result = server
.run_with_buffers(
&mut unicast_buf,
&mut sd_buf,
&mut recv_send_buf,
&mut announce_send_buf,
)
.await;
match result {
Err(Error::InvalidUsage(tag)) => assert_eq!(tag, "passive_server_run"),
other => {
panic!("passive server's run_with_buffers must return InvalidUsage, got {other:?}",)
}
}
}
#[test]
fn server_config_accepts_event_group_empty_means_any() {
let config = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30490);
assert!(config.event_group_ids.is_empty());
assert!(config.accepts_event_group(0x0001));
assert!(config.accepts_event_group(0xBEEF));
assert!(config.accepts_event_group(0xFFFF));
}
#[test]
fn server_config_accepts_event_group_populated_validates() {
let mut config = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30490);
config.event_group_ids.push(0x0001).unwrap();
config.event_group_ids.push(0x0042).unwrap();
assert!(config.accepts_event_group(0x0001));
assert!(config.accepts_event_group(0x0042));
assert!(!config.accepts_event_group(0x0002));
assert!(!config.accepts_event_group(0xBEEF));
}
#[tokio::test]
async fn handle_sd_message_rolls_back_subscription_on_failed_ack_send() {
use crate::transport::{IoErrorKind, ReceivedDatagram, TransportError};
use core::future::{Future, Ready, ready};
use core::pin::Pin;
use core::task::{Context, Poll};
use std::pin::Pin as StdPin;
struct FailingSocket {
local: SocketAddrV4,
}
struct FailingSend;
impl Future for FailingSend {
type Output = Result<(), TransportError>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Err(TransportError::Io(IoErrorKind::NetworkUnreachable)))
}
}
impl TransportSocket for FailingSocket {
type SendFuture<'a> = FailingSend;
type RecvFuture<'a> = Ready<Result<ReceivedDatagram, TransportError>>;
fn send_to<'a>(&'a self, _b: &'a [u8], _t: SocketAddrV4) -> Self::SendFuture<'a> {
FailingSend
}
fn recv_from<'a>(&'a self, _b: &'a mut [u8]) -> Self::RecvFuture<'a> {
ready(Err(TransportError::Unsupported))
}
fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
Ok(self.local)
}
fn join_multicast_v4(&self, _g: Ipv4Addr, _i: Ipv4Addr) -> Result<(), TransportError> {
Ok(())
}
fn leave_multicast_v4(&self, _g: Ipv4Addr, _i: Ipv4Addr) -> Result<(), TransportError> {
Ok(())
}
}
struct FailingFactory {
next_port: Arc<Mutex<u16>>,
}
impl TransportFactory for FailingFactory {
type Socket = FailingSocket;
type BindFuture<'a> = StdPin<
std::boxed::Box<
dyn Future<Output = Result<Self::Socket, TransportError>> + Send + 'a,
>,
>;
fn bind<'a>(
&'a self,
addr: SocketAddrV4,
_options: &'a SocketOptions,
) -> Self::BindFuture<'a> {
let port = if addr.port() == 0 {
let mut p = self.next_port.lock().unwrap();
*p = p.saturating_add(1);
50000u16.saturating_add(*p)
} else {
addr.port()
};
let local = SocketAddrV4::new(*addr.ip(), port);
std::boxed::Box::pin(async move { Ok(FailingSocket { local }) })
}
}
let factory = FailingFactory {
next_port: Arc::new(Mutex::new(0)),
};
let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new()));
let deps = ServerDeps {
factory,
timer: TokioTimer,
e2e_registry: Arc::new(Mutex::new(E2ERegistry::new())),
subscriptions: subscriptions.clone(),
non_sd_observer: None,
};
let config = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let (server, _handles, _run): (Server<_, _, _, _, Arc<FailingSocket>>, _, _) =
Server::new_with_deps(deps, config, false)
.await
.expect("create failing-socket server");
let bytes = make_subscription_header(
0x5B,
1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
45000,
);
let view = MessageView::parse(&bytes).expect("parse Subscribe");
let sd_view = view.sd_header().expect("Subscribe has SD header");
let sender = core::net::SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 45000));
let result = runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
sender,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await;
assert!(
result.is_ok(),
"handle_sd_message must not propagate transient SD-socket I/O errors; got {result:?}"
);
let subs = subscriptions.read().await;
assert_eq!(
subs.subscription_count(),
0,
"subscription must be rolled back after failed ACK send"
);
}
#[tokio::test]
async fn test_server_creation_with_loopback_enabled() {
let config = ServerConfig::new(0x5C, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(30683);
let (server, _handles, _run) = TestServer::new_with_loopback(config, true)
.await
.expect("new_with_loopback(true) should succeed on localhost");
assert!(
server
.sd_socket
.multicast_loop_v4()
.expect("multicast_loop_v4 getter should succeed"),
"multicast loopback should be enabled on the SD socket",
);
}
fn build_sd_message(sd_header: &sd::Header<'_>) -> Vec<u8> {
let mut sd_data = Vec::new();
sd_header.encode(&mut sd_data).unwrap();
let someip_header = SomeIpHeader::new_sd(0x0001, sd_data.len());
let mut buffer = Vec::new();
someip_header.encode(&mut buffer).unwrap();
buffer.extend_from_slice(&sd_data);
buffer
}
fn parse_subscribe_ack_ttl(data: &[u8]) -> u32 {
let view = MessageView::parse(data).expect("Failed to parse SOME/IP message");
let sd_view = view.sd_header().expect("Failed to parse SD header");
let mut entries = sd_view.entries();
let entry = entries.next().expect("Expected at least 1 entry");
assert_eq!(
entry.entry_type().unwrap(),
sd::EntryType::SubscribeAck,
"Expected SubscribeAckEventGroup entry"
);
entry.ttl()
}
async fn create_test_server(service_id: u16, instance_id: u16) -> (TestServer, u16) {
let config = ServerConfig::new(service_id, instance_id)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let (server, _handles, _run) = TestServer::new(config)
.await
.expect("Failed to create server");
let port = match server.unicast_local_addr().unwrap() {
core::net::SocketAddr::V4(addr) => addr.port(),
core::net::SocketAddr::V6(_) => panic!("expected IPv4 address"),
};
(server, port)
}
#[allow(clippy::too_many_arguments)]
fn make_subscription_header(
service_id: u16,
instance_id: u16,
major_version: u8,
ttl: u32,
event_group_id: u16,
client_ip: Ipv4Addr,
protocol: sd::TransportProtocol,
client_port: u16,
) -> Vec<u8> {
let entry = Entry::SubscribeEventGroup(sd::EventGroupEntry::new(
service_id,
instance_id,
major_version,
ttl,
event_group_id,
));
let endpoint = sd::Options::IpV4Endpoint {
ip: client_ip,
protocol,
port: client_port,
};
let entries = [entry];
let options = [endpoint];
let sd_header = sd::Header::new(
Flags::new_sd(sd::RebootFlag::RecentlyRebooted),
&entries,
&options,
);
build_sd_message(&sd_header)
}
#[tokio::test]
async fn test_subscribe_ack_success() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let message = make_subscription_header(
0x5B,
1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
server_port,
);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
assert_eq!(subs.subscription_count(), 1);
let subscribers = subs.get_subscribers(0x5B, 1, 0x01);
assert_eq!(subscribers.len(), 1);
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeAck")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert!(ttl > 0, "Expected ACK (TTL > 0), got TTL={ttl}");
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_subscribe_nack_wrong_service() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let message = make_subscription_header(
0x99, 1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
server_port,
);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
assert_eq!(subs.subscription_count(), 0);
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeNack")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert_eq!(ttl, 0, "Expected NACK (TTL=0), got TTL={ttl}");
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_subscribe_nack_wrong_instance() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let message = make_subscription_header(
0x5B,
99, 1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
server_port,
);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
assert_eq!(subs.subscription_count(), 0);
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeNack")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert_eq!(ttl, 0, "Expected NACK (TTL=0), got TTL={ttl}");
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_find_service_sends_unicast_offer() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let find_entry = Entry::FindService(ServiceEntry::find(0x5B));
let find_entries = [find_entry];
let sd_header = sd::Header::new(
Flags::new_sd(sd::RebootFlag::RecentlyRebooted),
&find_entries,
&[],
);
let message = build_sd_message(&sd_header);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for unicast OfferService")
.unwrap();
let view = MessageView::parse(&resp_buf[..resp_len]).unwrap();
assert_eq!(view.header().message_id().service_id(), 0xFFFF);
let sd_view = view.sd_header().unwrap();
let mut entries = sd_view.entries();
let entry = entries.next().unwrap();
assert_eq!(entry.entry_type().unwrap(), sd::EntryType::OfferService);
assert_eq!(entry.service_id(), 0x5B);
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_find_service_wildcard() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let find_entry = Entry::FindService(ServiceEntry::find(0xFFFF));
let find_entries = [find_entry];
let sd_header = sd::Header::new(
Flags::new_sd(sd::RebootFlag::RecentlyRebooted),
&find_entries,
&[],
);
let message = build_sd_message(&sd_header);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for unicast OfferService")
.unwrap();
let view = MessageView::parse(&resp_buf[..resp_len]).unwrap();
let sd_view = view.sd_header().unwrap();
let mut entries = sd_view.entries();
let entry = entries.next().unwrap();
assert_eq!(entry.entry_type().unwrap(), sd::EntryType::OfferService);
assert_eq!(entry.service_id(), 0x5B);
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_find_service_wrong_service_ignored() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let find_entry = Entry::FindService(ServiceEntry::find(0x99));
let find_entries = [find_entry];
let sd_header = sd::Header::new(
Flags::new_sd(sd::RebootFlag::RecentlyRebooted),
&find_entries,
&[],
);
let message = build_sd_message(&sd_header);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
});
let mut resp_buf = vec![0u8; 65535];
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
client_socket.recv_from(&mut resp_buf),
)
.await;
assert!(
result.is_err(),
"Expected timeout (no response for wrong service)"
);
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_subscribe_nack_no_endpoint() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let entry = sd::Entry::SubscribeEventGroup(sd::EventGroupEntry::new(0x5B, 1, 1, 3, 0x01));
let sub_entries = [entry];
let sd_header = sd::Header::new(Flags::new(true, true), &sub_entries, &[]);
let message = build_sd_message(&sd_header);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
assert_eq!(subs.subscription_count(), 0);
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeNack")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert_eq!(ttl, 0, "Expected NACK (TTL=0), got TTL={ttl}");
server_handle.await.unwrap();
}
#[tokio::test]
async fn test_send_offer_service() {
let receiver = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let recv_addr = receiver.local_addr().unwrap();
let (server, _) = create_test_server(0x5B, 1).await;
runtime::send_unicast_offer(
&mut [0u8; crate::UDP_BUFFER_SIZE],
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
recv_addr,
)
.await
.expect("send_unicast_offer failed");
let mut buf = vec![0u8; 65535];
let (len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
receiver.recv_from(&mut buf),
)
.await
.expect("Timeout waiting for OfferService")
.unwrap();
let view = MessageView::parse(&buf[..len]).unwrap();
assert_eq!(view.header().message_id(), crate::protocol::MessageId::SD);
let sd_view = view.sd_header().unwrap();
let mut entries = sd_view.entries();
let entry = entries.next().unwrap();
assert_eq!(entry.entry_type().unwrap(), sd::EntryType::OfferService);
assert_eq!(entry.service_id(), 0x5B);
assert_eq!(entry.instance_id(), 1);
drop(server);
let (server2, _) = create_test_server(0x5B, 1).await;
let fut = server2.run();
drop(fut);
}
#[tokio::test]
async fn test_run_non_sd_message() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let client_port = match client_socket.local_addr().unwrap() {
core::net::SocketAddr::V4(a) => a.port(),
core::net::SocketAddr::V6(_) => panic!("expected v4 source address"),
};
let subscriptions = Arc::clone(&server.subscriptions);
let server_handle = tokio::spawn(async move {
server.run().await.ok();
});
let non_sd_header = SomeIpHeader::new(
crate::protocol::MessageId::new_from_service_and_method(0x1234, 0x0001),
0x0001,
0x01,
0x01,
MessageTypeField::new(MessageType::Request, false),
ReturnCode::Ok,
0,
);
let mut non_sd_buf = Vec::new();
non_sd_header.encode(&mut non_sd_buf).unwrap();
client_socket
.send_to(&non_sd_buf, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let message = make_subscription_header(
0x5B,
1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
client_port,
);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeAck")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert!(ttl > 0, "Expected ACK (TTL > 0), got TTL={ttl}");
let subs = subscriptions.read().await;
assert_eq!(subs.subscription_count(), 1);
server_handle.abort();
}
#[tokio::test]
async fn test_run_malformed_data() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let client_port = match client_socket.local_addr().unwrap() {
core::net::SocketAddr::V4(a) => a.port(),
core::net::SocketAddr::V6(_) => panic!("expected v4 source address"),
};
let subscriptions = Arc::clone(&server.subscriptions);
let server_handle = tokio::spawn(async move {
server.run().await.ok();
});
client_socket
.send_to(&[0xFF, 0xFE, 0xFD], format!("127.0.0.1:{server_port}"))
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let message = make_subscription_header(
0x5B,
1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
client_port,
);
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeAck")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert!(ttl > 0, "Expected ACK (TTL > 0), got TTL={ttl}");
let subs = subscriptions.read().await;
assert_eq!(subs.subscription_count(), 1);
server_handle.abort();
}
#[tokio::test]
async fn test_handle_sd_other_entry_type() {
let (server, _) = create_test_server(0x5B, 1).await;
let entry = sd::Entry::StopOfferService(sd::ServiceEntry {
index_first_options_run: 0,
index_second_options_run: 0,
options_count: sd::OptionsCount::new(0, 0),
service_id: 0x5B,
instance_id: 1,
major_version: 1,
ttl: 0,
minor_version: 0,
});
let stop_entries = [entry];
let sd_msg = sd::Header::new(Flags::new(true, true), &stop_entries, &[]);
let mut buf = [0u8; 64];
let n = sd_msg.encode(&mut buf.as_mut_slice()).unwrap();
let sd_view = sd::SdHeaderView::parse(&buf[..n]).unwrap();
let result = runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
"127.0.0.1:12345".parse().unwrap(),
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_subscribe_ack_different_endpoint_port() {
let (server, server_port) = create_test_server(0x5B, 1).await;
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let message = make_subscription_header(
0x5B,
1,
1,
3,
0x01,
Ipv4Addr::LOCALHOST,
sd::TransportProtocol::Udp,
server_port.wrapping_add(1), );
client_socket
.send_to(&message, format!("127.0.0.1:{server_port}"))
.await
.unwrap();
let server_handle = tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
let datagram = server.unicast_socket.recv_from(&mut buf).await.unwrap();
let len = datagram.bytes_received;
let addr = core::net::SocketAddr::V4(datagram.source);
let data = &buf[..len];
let view = MessageView::parse(data).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
addr,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
assert_eq!(subs.subscription_count(), 1);
});
let mut resp_buf = vec![0u8; 65535];
let (resp_len, _) = tokio::time::timeout(
std::time::Duration::from_secs(2),
client_socket.recv_from(&mut resp_buf),
)
.await
.expect("Timeout waiting for SubscribeAck")
.unwrap();
let ttl = parse_subscribe_ack_ttl(&resp_buf[..resp_len]);
assert!(ttl > 0, "Expected ACK (TTL > 0), got TTL={ttl}");
server_handle.await.unwrap();
}
fn write_ipv4_endpoint_option(
buf: &mut [u8],
ip: Ipv4Addr,
port: u16,
protocol: sd::TransportProtocol,
) -> usize {
let opt = sd::Options::IpV4Endpoint { ip, protocol, port };
let mut slot = buf;
opt.write(&mut slot).unwrap()
}
fn write_load_balancing_option(buf: &mut [u8], priority: u16, weight: u16) -> usize {
let opt = sd::Options::LoadBalancing { priority, weight };
let mut slot = buf;
opt.write(&mut slot).unwrap()
}
fn fill_ipv4_endpoints(buf: &mut [u8], count: usize, base_port: u16) -> usize {
let mut offset = 0;
for i in 0..count {
let port_offset = u16::try_from(i).expect("test fixture count fits in u16");
let n = write_ipv4_endpoint_option(
&mut buf[offset..],
Ipv4Addr::new(10, 0, 0, 1),
base_port + port_offset,
sd::TransportProtocol::Udp,
);
offset += n;
}
offset
}
#[test]
fn extract_endpoint_single_option_first_run() {
let mut buf = [0u8; 64];
let total = fill_ipv4_endpoints(&mut buf, 1, 30000);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 0, 1, 0, 0);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30000))
);
}
#[test]
fn extract_endpoint_zero_options_in_both_runs_returns_none() {
let iter = sd::OptionIter::new(&[]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter, 0, 0, 0, 0),
None
);
}
#[test]
fn extract_endpoint_count_zero_with_nonzero_index_returns_none() {
let mut buf = [0u8; 64];
let total = fill_ipv4_endpoints(&mut buf, 2, 30100);
let iter = sd::OptionIter::new(&buf[..total]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter, 1, 0, 0, 0),
None
);
}
#[test]
fn extract_endpoint_multi_option_first_run_returns_first() {
let mut buf = [0u8; 64];
let total = fill_ipv4_endpoints(&mut buf, 2, 30200);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 0, 2, 0, 0);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30200))
);
}
#[test]
fn extract_endpoint_split_across_first_and_second_runs() {
let mut buf = [0u8; 96];
let total = fill_ipv4_endpoints(&mut buf, 3, 30300);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 0, 1, 2, 1);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30300))
);
}
#[test]
fn extract_endpoint_honors_first_index_offset() {
let mut buf = [0u8; 128];
let total = fill_ipv4_endpoints(&mut buf, 4, 30400);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 2, 1, 0, 0);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30402))
);
}
#[test]
fn extract_endpoint_respects_first_count_cap() {
let mut buf = [0u8; 128];
let total = fill_ipv4_endpoints(&mut buf, 4, 30500);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 1, 1, 0, 0);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30501))
);
}
#[test]
fn extract_endpoint_skips_non_ipv4_options() {
let mut buf = [0u8; 64];
let mut offset = 0;
offset += write_load_balancing_option(&mut buf[offset..], 1, 2);
offset += write_ipv4_endpoint_option(
&mut buf[offset..],
Ipv4Addr::new(10, 0, 0, 1),
30600,
sd::TransportProtocol::Udp,
);
offset += write_load_balancing_option(&mut buf[offset..], 3, 4);
let iter = sd::OptionIter::new(&buf[..offset]);
let got = runtime::extract_subscriber_endpoint(&iter, 0, 3, 0, 0);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30600))
);
}
#[test]
fn extract_endpoint_all_non_ipv4_returns_none() {
let mut buf = [0u8; 32];
let mut offset = 0;
offset += write_load_balancing_option(&mut buf[offset..], 1, 2);
offset += write_load_balancing_option(&mut buf[offset..], 3, 4);
let iter = sd::OptionIter::new(&buf[..offset]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter, 0, 2, 0, 0),
None
);
}
#[test]
fn extract_endpoint_second_run_only() {
let mut buf = [0u8; 64];
let total = fill_ipv4_endpoints(&mut buf, 2, 30700);
let iter = sd::OptionIter::new(&buf[..total]);
let got = runtime::extract_subscriber_endpoint(&iter, 0, 0, 1, 1);
assert_eq!(
got,
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 30701))
);
}
#[tokio::test]
async fn combined_sd_subscribe_uses_its_own_options_run() {
let (server, _port) = create_test_server(0x5B, 1).await;
let offer_endpoint_port: u16 = 40_111;
let subscribe_endpoint_port: u16 = 40_222;
let offer_entry = Entry::OfferService(sd::ServiceEntry {
index_first_options_run: 0,
index_second_options_run: 0,
options_count: sd::OptionsCount::new(1, 0),
service_id: 0x5B,
instance_id: 1,
major_version: 1,
ttl: 3,
minor_version: 0,
});
let subscribe_entry = Entry::SubscribeEventGroup(sd::EventGroupEntry {
index_first_options_run: 1,
index_second_options_run: 0,
options_count: sd::OptionsCount::new(1, 0),
service_id: 0x5B,
instance_id: 1,
major_version: 1,
ttl: 3,
counter: 0,
event_group_id: 0x0001,
});
let entries = [offer_entry, subscribe_entry];
let options = [
sd::Options::IpV4Endpoint {
ip: Ipv4Addr::LOCALHOST,
protocol: sd::TransportProtocol::Udp,
port: offer_endpoint_port,
},
sd::Options::IpV4Endpoint {
ip: Ipv4Addr::LOCALHOST,
protocol: sd::TransportProtocol::Udp,
port: subscribe_endpoint_port,
},
];
let sd_header = sd::Header::new(
sd::Flags::new_sd(sd::RebootFlag::RecentlyRebooted),
&entries,
&options,
);
let message = build_sd_message(&sd_header);
let sender = core::net::SocketAddr::from((Ipv4Addr::LOCALHOST, 54_321));
let view = MessageView::parse(&message).unwrap();
let sd_view = view.sd_header().unwrap();
runtime::handle_sd_message(
&server.config,
server.sd_socket.get(),
server.sd_state.get(),
&server.subscriptions,
&sd_view,
sender,
&mut [0u8; crate::UDP_BUFFER_SIZE],
)
.await
.unwrap();
let subs = server.subscriptions.read().await;
let subscribers = subs.get_subscribers(0x5B, 1, 0x0001);
assert_eq!(
subscribers.len(),
1,
"combined SD packet must yield exactly one subscriber"
);
assert_eq!(
subscribers[0].address.port(),
subscribe_endpoint_port,
"subscription endpoint must come from the Subscribe entry's own \
options run (options[1]={subscribe_endpoint_port}), not from \
the Offer entry's options[0]={offer_endpoint_port}"
);
assert_ne!(
subscribers[0].address.port(),
offer_endpoint_port,
"regression: subscription picked up the OfferService endpoint \
instead of its own SubscribeEventGroup endpoint"
);
}
async fn make_passive_server(service_id: u16, instance_id: u16) -> TestServer {
let config = ServerConfig::new(service_id, instance_id)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let (server, _handles, _run) = TestServer::new_passive(config)
.await
.expect("new_passive should succeed");
server
}
#[tokio::test]
async fn new_passive_unicast_bound_to_requested_port() {
let server = make_passive_server(0x005C, 0x0001).await;
let local = server.unicast_local_addr().unwrap();
match local {
core::net::SocketAddr::V4(v4) => {
assert_ne!(
v4.port(),
0,
"kernel should assign an ephemeral port when local_port=0"
);
}
core::net::SocketAddr::V6(_) => panic!("expected IPv4 unicast address"),
}
}
#[tokio::test]
async fn new_passive_sd_socket_is_not_bound_to_30490() {
let server = make_passive_server(0x005C, 0x0001).await;
let sd_addr = server.sd_socket.local_addr().unwrap();
assert_ne!(
sd_addr.port(),
30490,
"passive SD socket must not bind the SOME/IP SD port"
);
}
#[tokio::test]
async fn new_passive_publisher_accepts_register_subscriber() {
let server = make_passive_server(0x005C, 0x0001).await;
let publisher = server.publisher();
assert!(!publisher.has_subscribers(0x005C, 0x0001, 0x0001).await);
let subscriber = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 2), 40_000);
publisher
.register_subscriber(0x005C, 0x0001, 0x0001, subscriber)
.await
.unwrap();
assert!(publisher.has_subscribers(0x005C, 0x0001, 0x0001).await);
assert_eq!(publisher.subscriber_count(0x005C, 0x0001, 0x0001).await, 1);
publisher
.remove_subscriber(0x005C, 0x0001, 0x0001, subscriber)
.await;
assert!(!publisher.has_subscribers(0x005C, 0x0001, 0x0001).await);
}
#[tokio::test]
async fn run_on_passive_returns_invalid_input() {
let server = make_passive_server(0x005C, 0x0001).await;
let err = server
.run()
.await
.expect_err("run on a passive server must fail");
match err {
Error::InvalidUsage(tag) => {
assert_eq!(tag, "passive_server_run");
}
other => panic!("expected Error::InvalidUsage(\"passive_server_run\"), got {other:?}"),
}
}
#[tokio::test]
async fn run_on_regular_server_builds_future_ok() {
let (server, _port) = create_test_server(0x005C, 0x0001).await;
let fut = server.run();
drop(fut);
}
#[tokio::test]
async fn second_run_future_returns_already_running() {
let (server, _port) = create_test_server(0x005D, 0x0001).await;
let first = tokio::spawn(server.run());
tokio::task::yield_now().await;
tokio::task::yield_now().await;
let second = server.run().await;
match second {
Err(Error::InvalidUsage(tag)) => {
assert_eq!(tag, "server_already_running");
}
other => panic!(
"second run-future must return InvalidUsage(\"server_already_running\"), got {other:?}"
),
}
let mut unicast_buf = vec![0u8; 1500];
let mut sd_buf = vec![0u8; 1500];
let mut recv_send_buf = vec![0u8; 1500];
let mut announce_send_buf = vec![0u8; 1500];
let third = server
.run_with_buffers(
&mut unicast_buf,
&mut sd_buf,
&mut recv_send_buf,
&mut announce_send_buf,
)
.await;
match third {
Err(Error::InvalidUsage(tag)) => {
assert_eq!(tag, "server_already_running");
}
other => panic!(
"second run_with_buffers must return InvalidUsage(\"server_already_running\"), got {other:?}"
),
}
first.abort();
let _ = first.await;
}
#[ignore = "requires MULTICAST on loopback; consistent with the \
#[ignore]-gated sd_state.rs tests. Runs in any environment \
where loopback multicast is available."]
#[tokio::test]
async fn announcement_loop_sends_offer_service_when_driven() {
use crate::protocol::MessageId;
const SID: u16 = 0xAA01;
const IID: u16 = 0xFF01;
let iface = std::net::Ipv4Addr::LOCALHOST;
let recv = {
let s = socket2::Socket::new(
socket2::Domain::IPV4,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.unwrap();
s.set_reuse_address(true).unwrap();
#[cfg(unix)]
s.set_reuse_port(true).unwrap();
s.bind(&core::net::SocketAddr::new(IpAddr::V4(iface), sd::MULTICAST_PORT).into())
.unwrap();
s.set_nonblocking(true).unwrap();
let std_s: std::net::UdpSocket = s.into();
let rs = tokio::net::UdpSocket::from_std(std_s).unwrap();
rs.join_multicast_v4(sd::MULTICAST_IP, iface).unwrap();
rs
};
let config = ServerConfig::new(SID, IID)
.with_interface(iface)
.with_local_port(30501);
let (_server, _handles, run) = TestServer::new_with_loopback(config, true).await.unwrap();
let handle = tokio::spawn(async move {
let _ = run.await;
});
let mut buf = [0u8; 1500];
let offer_fields = tokio::time::timeout(std::time::Duration::from_secs(3), async {
loop {
let (n, _src) = recv.recv_from(&mut buf).await.expect("recv failed");
let Ok(view) = crate::protocol::MessageView::parse(&buf[..n]) else {
continue;
};
if view.header().message_id() != MessageId::SD {
continue;
}
let Ok(sd_view) = view.sd_header() else {
continue;
};
let Some(entry) = sd_view.entries().next() else {
continue;
};
if !matches!(entry.entry_type(), Ok(sd::EntryType::OfferService)) {
continue;
}
if entry.service_id() != SID || entry.instance_id() != IID {
continue;
}
break (
entry.service_id(),
entry.instance_id(),
entry.major_version(),
entry.ttl(),
);
}
})
.await
.expect("timed out waiting for our OfferService");
let (svc, inst, major, ttl) = offer_fields;
assert_eq!(svc, SID, "emitted service_id must match server config");
assert_eq!(inst, IID, "emitted instance_id must match server config");
assert_eq!(major, 1, "default major_version from ServerConfig::new");
assert!(
ttl > 0,
"OfferService TTL must be non-zero (TTL=0 means StopOffering)",
);
handle.abort();
}
#[tokio::test]
async fn with_announce_false_suppresses_offer_service() {
use crate::protocol::MessageId;
const SID: u16 = 0xAA02;
const IID: u16 = 0xFF02;
let iface = std::net::Ipv4Addr::LOCALHOST;
let recv = {
let s = socket2::Socket::new(
socket2::Domain::IPV4,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.unwrap();
s.set_reuse_address(true).unwrap();
#[cfg(unix)]
s.set_reuse_port(true).unwrap();
s.bind(&core::net::SocketAddr::new(IpAddr::V4(iface), sd::MULTICAST_PORT).into())
.unwrap();
s.set_nonblocking(true).unwrap();
let std_s: std::net::UdpSocket = s.into();
let rs = tokio::net::UdpSocket::from_std(std_s).unwrap();
rs.join_multicast_v4(sd::MULTICAST_IP, iface).unwrap();
rs
};
let config = ServerConfig::new(SID, IID)
.with_interface(iface)
.with_local_port(30502)
.with_announce(false);
let (_server, _handles, run) = TestServer::new_with_loopback(config, true).await.unwrap();
let handle = tokio::spawn(async move {
let _ = run.await;
});
let saw_our_offer = tokio::time::timeout(std::time::Duration::from_millis(2_500), async {
let mut buf = [0u8; 1500];
loop {
let (n, _src) = recv.recv_from(&mut buf).await.expect("recv failed");
let Ok(view) = crate::protocol::MessageView::parse(&buf[..n]) else {
continue;
};
if view.header().message_id() != MessageId::SD {
continue;
}
let Ok(sd_view) = view.sd_header() else {
continue;
};
let Some(entry) = sd_view.entries().next() else {
continue;
};
if !matches!(entry.entry_type(), Ok(sd::EntryType::OfferService)) {
continue;
}
if entry.service_id() == SID && entry.instance_id() == IID {
break true;
}
}
})
.await
.unwrap_or(false);
handle.abort();
let _ = handle.await;
assert!(
!saw_our_offer,
"with_announce(false) must suppress OfferService emission for the configured \
service; observed an OfferService for (sid={SID:#06x}, iid={IID:#06x}) within \
the listen window. The dispatcher topology in examples/client_server depends \
on this suppression."
);
}
#[tokio::test]
async fn new_passive_two_instances_do_not_fight_over_sd_port() {
let a = make_passive_server(0x005B, 0x0002).await;
let b = make_passive_server(0x005C, 0x0001).await;
let addr_a = a.sd_socket.local_addr().unwrap();
let addr_b = b.sd_socket.local_addr().unwrap();
assert_ne!(addr_a, addr_b);
assert_ne!(addr_a.port(), 30490);
assert_ne!(addr_b.port(), 30490);
}
#[tokio::test]
async fn new_passive_returns_error_when_unicast_bind_fails() {
let blocker = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
.await
.expect("blocker bind should succeed");
let blocker_port = match blocker.local_addr().unwrap() {
core::net::SocketAddr::V4(v4) => v4.port(),
core::net::SocketAddr::V6(_) => panic!("expected IPv4"),
};
let config = ServerConfig::new(0x005C, 0x0001)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(blocker_port);
let result = TestServer::new_passive(config).await;
let Err(err) = result else {
panic!("new_passive must fail when the unicast port is taken");
};
match err {
Error::Transport(crate::transport::TransportError::AddressInUse) => {}
Error::Io(io_err) => {
assert!(
matches!(
io_err.kind(),
std::io::ErrorKind::AddrInUse | std::io::ErrorKind::PermissionDenied
),
"expected AddrInUse or PermissionDenied, got {:?}",
io_err.kind()
);
}
other => panic!("expected Error::Io or Error::Transport(AddressInUse), got {other:?}"),
}
drop(blocker);
}
#[tokio::test]
async fn new_passive_with_tracing_subscriber_evaluates_format_args() {
use tracing::subscriber::with_default;
use tracing_subscriber::fmt;
let subscriber = fmt()
.with_max_level(tracing::Level::TRACE)
.with_test_writer()
.finish();
let fut = async {
let _server = make_passive_server(0x00AA, 0x00BB).await;
};
let handle = std::thread::spawn(move || {
with_default(subscriber, || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(fut);
});
});
handle.join().expect("subscriber thread panicked");
}
#[test]
fn extract_subscriber_endpoint_with_tracing_evaluates_log_args() {
use tracing::subscriber::with_default;
use tracing_subscriber::fmt;
let subscriber = fmt()
.with_max_level(tracing::Level::TRACE)
.with_test_writer()
.finish();
with_default(subscriber, || {
let iter_empty = sd::OptionIter::new(&[]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter_empty, 0, 0, 0, 0),
None
);
let mut buf_one = [0u8; 32];
let len_one = fill_ipv4_endpoints(&mut buf_one, 1, 31000);
let iter_one = sd::OptionIter::new(&buf_one[..len_one]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter_one, 0, 1, 0, 0),
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 31000))
);
let mut buf_many = [0u8; 64];
let len_many = fill_ipv4_endpoints(&mut buf_many, 3, 31100);
let iter_many = sd::OptionIter::new(&buf_many[..len_many]);
assert_eq!(
runtime::extract_subscriber_endpoint(&iter_many, 0, 3, 0, 0),
Some(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 31100))
);
});
}
#[ignore = "requires loopback multicast support (MULTICAST on lo)"]
#[tokio::test]
async fn announcement_loop_emits_first_offer_within_timeout() {
use crate::protocol::MessageView;
use crate::protocol::sd::EntryType;
let interface = Ipv4Addr::LOCALHOST;
let service_id = 0xFE02;
let config = ServerConfig::new(service_id, 0x43)
.with_interface(interface)
.with_local_port(30684);
let raw_rx = socket2::Socket::new(
socket2::Domain::IPV4,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.unwrap();
raw_rx.set_reuse_address(true).unwrap();
#[cfg(unix)]
raw_rx.set_reuse_port(true).unwrap();
raw_rx.set_multicast_loop_v4(true).unwrap();
raw_rx
.bind(&core::net::SocketAddr::new(IpAddr::V4(interface), sd::MULTICAST_PORT).into())
.unwrap();
raw_rx.set_nonblocking(true).unwrap();
let rx: UdpSocket = UdpSocket::from_std(raw_rx.into()).unwrap();
rx.join_multicast_v4(sd::MULTICAST_IP, interface).unwrap();
let (_server, _handles, run_fut) = TestServer::new_with_loopback(config, true)
.await
.expect("server must bind with loopback enabled");
let announce_handle = tokio::spawn(async move {
let _ = run_fut.await;
});
let recv_loop = async {
let mut buf = [0u8; 2048];
loop {
let (len, _from) = rx.recv_from(&mut buf).await.expect("recv_from");
let Ok(view) = MessageView::parse(&buf[..len]) else {
continue;
};
if view.header().message_id().service_id() != 0xFFFF {
continue;
}
let Ok(sd_view) = view.sd_header() else {
continue;
};
let Some(entry) = sd_view.entries().next() else {
continue;
};
if !matches!(entry.entry_type(), Ok(EntryType::OfferService)) {
continue;
}
if entry.service_id() == service_id {
return;
}
}
};
tokio::time::timeout(std::time::Duration::from_secs(2), recv_loop)
.await
.expect("announcement_loop should emit at least one OfferService within 2s");
announce_handle.abort();
let _ = announce_handle.await;
}
const TOKIO_SERVER_RUN_FUTURE_BUDGET: usize = 9728;
#[tokio::test]
async fn future_size_witness_tokio_server() {
let config = ServerConfig::new(0x5B, 1)
.with_interface(Ipv4Addr::LOCALHOST)
.with_local_port(0);
let (_server, _handles, run) = TestServer::new(config).await.expect("Server::new");
let run_size = core::mem::size_of_val(&run);
std::println!("FUTURE_SIZE tokio_server_run_future {run_size}");
assert!(
run_size <= TOKIO_SERVER_RUN_FUTURE_BUDGET,
"server run future grew: {run_size} B > budget {TOKIO_SERVER_RUN_FUTURE_BUDGET} B"
);
}
}