use core::fmt;
use crate::v2gtp::{self, PayloadType, V2gtpError};
pub const REQUEST_LEN: usize = 2;
pub const RESPONSE_LEN: usize = 20;
pub const MULTICAST_ADDR: [u8; 16] = [0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Security {
Tls,
None,
}
impl Security {
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::Tls => 0x00,
Self::None => 0x10,
}
}
pub const fn from_u8(value: u8) -> Result<Self, SdpError> {
match value {
0x00 => Ok(Self::Tls),
0x10 => Ok(Self::None),
other => Err(SdpError::UnknownSecurity(other)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TransportProtocol {
Tcp,
Udp,
}
impl TransportProtocol {
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::Tcp => 0x00,
Self::Udp => 0x10,
}
}
pub const fn from_u8(value: u8) -> Result<Self, SdpError> {
match value {
0x00 => Ok(Self::Tcp),
0x10 => Ok(Self::Udp),
other => Err(SdpError::UnknownTransport(other)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Request {
pub security: Security,
pub transport: TransportProtocol,
}
impl Request {
pub const TLS: Self = Self { security: Security::Tls, transport: TransportProtocol::Tcp };
pub const PLAIN: Self = Self { security: Security::None, transport: TransportProtocol::Tcp };
#[must_use]
pub const fn to_payload(self) -> [u8; REQUEST_LEN] {
[self.security.as_u8(), self.transport.as_u8()]
}
pub const fn from_payload(payload: &[u8]) -> Result<Self, SdpError> {
if payload.len() != REQUEST_LEN {
return Err(SdpError::BadLength { expected: REQUEST_LEN, actual: payload.len() });
}
Ok(Self {
security: match Security::from_u8(payload[0]) {
Ok(s) => s,
Err(e) => return Err(e),
},
transport: match TransportProtocol::from_u8(payload[1]) {
Ok(t) => t,
Err(e) => return Err(e),
},
})
}
pub fn write_frame(self, out: &mut [u8], wireless: bool) -> Result<usize, SdpError> {
let ty = if wireless { PayloadType::SdpWirelessRequest } else { PayloadType::SdpRequest };
Ok(v2gtp::write_frame(ty, &self.to_payload(), out)?)
}
pub fn from_frame(frame: &[u8]) -> Result<Self, SdpError> {
let (header, payload, rest) = v2gtp::split_frame(frame, RESPONSE_LEN.max(REQUEST_LEN))?;
if !header.payload_type.is_sdp_request() {
return Err(SdpError::WrongPayloadType(header.payload_type));
}
if !rest.is_empty() {
return Err(SdpError::TrailingData);
}
Self::from_payload(payload)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
pub address: [u8; 16],
pub port: u16,
pub security: Security,
pub transport: TransportProtocol,
}
impl Response {
#[must_use]
pub const fn to_payload(self) -> [u8; RESPONSE_LEN] {
let mut out = [0u8; RESPONSE_LEN];
let mut i = 0;
while i < 16 {
out[i] = self.address[i];
i += 1;
}
let port = self.port.to_be_bytes();
out[16] = port[0];
out[17] = port[1];
out[18] = self.security.as_u8();
out[19] = self.transport.as_u8();
out
}
pub fn from_payload(payload: &[u8]) -> Result<Self, SdpError> {
let payload: &[u8; RESPONSE_LEN] = payload
.try_into()
.map_err(|_| SdpError::BadLength { expected: RESPONSE_LEN, actual: payload.len() })?;
let mut address = [0u8; 16];
address.copy_from_slice(&payload[..16]);
let port = u16::from_be_bytes([payload[16], payload[17]]);
if port == 0 {
return Err(SdpError::InvalidPort);
}
Ok(Self {
address,
port,
security: Security::from_u8(payload[18])?,
transport: TransportProtocol::from_u8(payload[19])?,
})
}
pub fn write_frame(self, out: &mut [u8], wireless: bool) -> Result<usize, SdpError> {
let ty = if wireless { PayloadType::SdpWirelessResponse } else { PayloadType::SdpResponse };
Ok(v2gtp::write_frame(ty, &self.to_payload(), out)?)
}
pub fn from_frame(frame: &[u8]) -> Result<Self, SdpError> {
let (header, payload, rest) = v2gtp::split_frame(frame, RESPONSE_LEN)?;
if !header.payload_type.is_sdp_response() {
return Err(SdpError::WrongPayloadType(header.payload_type));
}
if !rest.is_empty() {
return Err(SdpError::TrailingData);
}
Self::from_payload(payload)
}
#[must_use]
pub const fn satisfies(&self, request: &Request) -> bool {
if self.transport as u8 != request.transport as u8 {
return false;
}
!matches!((request.security, self.security), (Security::Tls, Security::None))
}
#[must_use]
pub const fn is_link_local(&self) -> bool {
self.address[0] == 0xfe && (self.address[1] & 0xc0) == 0x80
}
}
#[cfg(feature = "std")]
impl Response {
#[must_use]
pub const fn ipv6(&self) -> std::net::Ipv6Addr {
std::net::Ipv6Addr::new(
u16::from_be_bytes([self.address[0], self.address[1]]),
u16::from_be_bytes([self.address[2], self.address[3]]),
u16::from_be_bytes([self.address[4], self.address[5]]),
u16::from_be_bytes([self.address[6], self.address[7]]),
u16::from_be_bytes([self.address[8], self.address[9]]),
u16::from_be_bytes([self.address[10], self.address[11]]),
u16::from_be_bytes([self.address[12], self.address[13]]),
u16::from_be_bytes([self.address[14], self.address[15]]),
)
}
#[must_use]
pub const fn from_ipv6(
addr: std::net::Ipv6Addr,
port: u16,
security: Security,
transport: TransportProtocol,
) -> Self {
Self { address: addr.octets(), port, security, transport }
}
}
#[derive(Debug, Clone)]
pub struct Discovery {
request: Request,
wireless: bool,
interval: crate::session::Millis,
max_attempts: u32,
attempts: u32,
deadline: Option<crate::session::Instant>,
pending: Option<[u8; v2gtp::HEADER_LEN + REQUEST_LEN]>,
event: Option<Event>,
done: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Event {
Found(Response),
Refused(Response),
GaveUp {
attempts: u32,
},
}
impl Discovery {
#[must_use]
pub const fn new(request: Request) -> Self {
Self {
request,
wireless: false,
interval: crate::session::timers::sdp::RESPONSE_TIMEOUT,
max_attempts: crate::session::timers::sdp::MAX_REQUESTS,
attempts: 0,
deadline: None,
pending: None,
event: None,
done: false,
}
}
#[must_use]
pub const fn wireless(mut self, wireless: bool) -> Self {
self.wireless = wireless;
self
}
#[must_use]
pub const fn with_interval(mut self, interval: crate::session::Millis) -> Self {
self.interval = interval;
self
}
#[must_use]
pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
self.max_attempts = max_attempts;
self
}
pub fn start(&mut self, now: crate::session::Instant) {
self.attempts = 0;
self.done = false;
self.event = None;
self.send(now);
}
#[must_use]
pub const fn attempts(&self) -> u32 {
self.attempts
}
#[must_use]
pub const fn is_finished(&self) -> bool {
self.done
}
#[must_use]
pub fn poll_transmit(&mut self) -> Option<alloc::vec::Vec<u8>> {
self.pending.take().map(|frame| frame.to_vec())
}
#[must_use]
pub const fn poll_timeout(&self) -> Option<crate::session::Instant> {
self.deadline
}
pub fn handle_timeout(&mut self, now: crate::session::Instant) {
if self.done || self.deadline.is_none_or(|d| d > now) {
return;
}
self.deadline = None;
if self.attempts >= self.max_attempts {
self.done = true;
self.event = Some(Event::GaveUp { attempts: self.attempts });
return;
}
self.send(now);
}
pub fn handle_datagram(
&mut self,
_now: crate::session::Instant,
frame: &[u8],
) -> Result<(), SdpError> {
if self.done {
return Ok(());
}
let response = Response::from_frame(frame)?;
self.done = true;
self.deadline = None;
self.pending = None;
self.event = Some(if response.satisfies(&self.request) {
Event::Found(response)
} else {
Event::Refused(response)
});
Ok(())
}
pub const fn poll_event(&mut self) -> Option<Event> {
self.event.take()
}
fn send(&mut self, now: crate::session::Instant) {
let mut frame = [0u8; v2gtp::HEADER_LEN + REQUEST_LEN];
if self.request.write_frame(&mut frame, self.wireless).is_ok() {
self.pending = Some(frame);
}
self.attempts += 1;
self.deadline = Some(now.saturating_add(self.interval));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SdpError {
BadLength {
expected: usize,
actual: usize,
},
UnknownSecurity(u8),
UnknownTransport(u8),
InvalidPort,
WrongPayloadType(PayloadType),
TrailingData,
Framing(V2gtpError),
}
impl From<V2gtpError> for SdpError {
fn from(e: V2gtpError) -> Self {
Self::Framing(e)
}
}
impl fmt::Display for SdpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadLength { expected, actual } => {
write!(f, "SDP payload is {actual} bytes, expected {expected}")
}
Self::UnknownSecurity(v) => write!(f, "unknown SDP security value {v:#04x}"),
Self::UnknownTransport(v) => write!(f, "unknown SDP transport value {v:#04x}"),
Self::InvalidPort => f.write_str("SDP response advertises port 0"),
Self::WrongPayloadType(t) => write!(f, "unexpected payload type {t:?} for SDP message"),
Self::TrailingData => f.write_str("trailing data after SDP datagram"),
Self::Framing(e) => write!(f, "V2GTP framing: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SdpError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Framing(e) => Some(e),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const LINK_LOCAL: [u8; 16] =
[0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x1a, 0x2b, 0xff, 0xfe, 0x3c, 0x4d, 0x5e];
#[test]
fn request_frame_matches_the_spec_layout() {
let mut buf = [0u8; 16];
let n = Request::TLS.write_frame(&mut buf, false).unwrap();
assert_eq!(n, 10);
assert_eq!(&buf[..n], &[0x01, 0xFE, 0x90, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00]);
}
#[test]
fn plain_request_frame() {
let mut buf = [0u8; 16];
let n = Request::PLAIN.write_frame(&mut buf, false).unwrap();
assert_eq!(&buf[8..n], &[0x10, 0x00]);
}
#[test]
fn request_roundtrips_through_a_frame() {
for req in [Request::TLS, Request::PLAIN] {
let mut buf = [0u8; 16];
let n = req.write_frame(&mut buf, false).unwrap();
assert_eq!(Request::from_frame(&buf[..n]).unwrap(), req);
}
}
#[test]
fn wireless_request_uses_its_own_payload_type() {
let mut buf = [0u8; 16];
let n = Request::TLS.write_frame(&mut buf, true).unwrap();
assert_eq!(&buf[2..4], &[0x90, 0x02]);
assert_eq!(Request::from_frame(&buf[..n]).unwrap(), Request::TLS);
}
#[test]
fn response_roundtrips_through_a_frame() {
let res = Response {
address: LINK_LOCAL,
port: 49152,
security: Security::Tls,
transport: TransportProtocol::Tcp,
};
let mut buf = [0u8; 64];
let n = res.write_frame(&mut buf, false).unwrap();
assert_eq!(n, 28);
assert_eq!(Response::from_frame(&buf[..n]).unwrap(), res);
assert!(res.is_link_local());
}
#[test]
fn a_response_may_not_downgrade_a_tls_request() {
let res = Response {
address: LINK_LOCAL,
port: 15118,
security: Security::None,
transport: TransportProtocol::Tcp,
};
assert!(!res.satisfies(&Request::TLS), "TLS request answered without TLS must be refused");
assert!(res.satisfies(&Request::PLAIN));
}
#[test]
fn a_response_may_upgrade_a_plaintext_request() {
let res = Response {
address: LINK_LOCAL,
port: 15118,
security: Security::Tls,
transport: TransportProtocol::Tcp,
};
assert!(res.satisfies(&Request::PLAIN));
assert!(res.satisfies(&Request::TLS));
}
#[test]
fn a_transport_mismatch_never_satisfies() {
let res = Response {
address: LINK_LOCAL,
port: 15118,
security: Security::Tls,
transport: TransportProtocol::Udp,
};
assert!(!res.satisfies(&Request::TLS));
}
#[test]
fn port_zero_is_rejected() {
let mut payload = [0u8; RESPONSE_LEN];
payload[..16].copy_from_slice(&LINK_LOCAL);
assert_eq!(Response::from_payload(&payload), Err(SdpError::InvalidPort));
}
#[test]
fn unknown_enum_bytes_are_rejected() {
assert_eq!(Security::from_u8(0x01), Err(SdpError::UnknownSecurity(0x01)));
assert_eq!(TransportProtocol::from_u8(0xFF), Err(SdpError::UnknownTransport(0xFF)));
}
#[test]
fn a_response_frame_is_not_a_request() {
let mut buf = [0u8; 64];
let n = Response {
address: LINK_LOCAL,
port: 15118,
security: Security::Tls,
transport: TransportProtocol::Tcp,
}
.write_frame(&mut buf, false)
.unwrap();
assert!(matches!(Request::from_frame(&buf[..n]), Err(SdpError::WrongPayloadType(_))));
}
#[test]
fn short_payloads_are_rejected() {
assert_eq!(
Request::from_payload(&[0x00]),
Err(SdpError::BadLength { expected: 2, actual: 1 })
);
assert_eq!(
Response::from_payload(&[0x00; 19]),
Err(SdpError::BadLength { expected: 20, actual: 19 })
);
}
#[test]
fn discovery_repeats_until_the_attempt_ceiling() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS).with_max_attempts(3);
let mut now = Instant::ZERO;
d.start(now);
assert!(d.poll_transmit().is_some());
for expected in 2..=3 {
now = d.poll_timeout().expect("a retry deadline");
d.handle_timeout(now);
assert!(d.poll_transmit().is_some(), "attempt {expected}");
assert_eq!(d.attempts(), expected);
}
now = d.poll_timeout().expect("one last deadline");
d.handle_timeout(now);
assert_eq!(d.poll_transmit(), None, "the ceiling is a ceiling");
assert_eq!(d.poll_event(), Some(Event::GaveUp { attempts: 3 }));
assert!(d.is_finished());
assert_eq!(d.poll_timeout(), None, "nothing more is pending");
}
#[test]
fn discovery_waits_the_specified_interval() {
use crate::session::Instant;
let mut d = Discovery::new(Request::PLAIN);
d.start(Instant::ZERO);
assert_eq!(
d.poll_timeout(),
Some(Instant::ZERO + crate::session::timers::sdp::RESPONSE_TIMEOUT)
);
d.handle_timeout(Instant::from_millis(1));
assert_eq!(d.attempts(), 1);
}
#[test]
fn a_downgrade_is_surfaced_as_a_refusal_not_a_find() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
let res = Response {
address: LINK_LOCAL,
port: 15118,
security: Security::None,
transport: TransportProtocol::Tcp,
};
let mut frame = [0u8; 64];
let n = res.write_frame(&mut frame, false).unwrap();
d.handle_datagram(Instant::ZERO, &frame[..n]).unwrap();
assert_eq!(d.poll_event(), Some(Event::Refused(res)));
}
#[test]
fn a_malformed_answer_does_not_end_discovery() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
let _ = d.poll_transmit();
assert!(d.handle_datagram(Instant::ZERO, &[0xDE, 0xAD]).is_err());
assert!(!d.is_finished());
assert!(d.poll_timeout().is_some(), "the retry is still armed");
}
#[cfg(feature = "std")]
#[test]
fn ipv6_conversion_roundtrips() {
let addr: std::net::Ipv6Addr = "fe80::21a:2bff:fe3c:4d5e".parse().unwrap();
let res = Response::from_ipv6(addr, 15118, Security::Tls, TransportProtocol::Tcp);
assert_eq!(res.ipv6(), addr);
assert_eq!(res.address, LINK_LOCAL);
}
}