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];
pub use crate::session::Security;
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, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TlsPolicy {
Unsupported,
Supported,
Required,
}
#[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);
}
if address == [0u8; 16] || address[0] == 0xff {
return Err(SdpError::InvalidAddress(address));
}
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]
#[allow(
clippy::match_same_arms,
reason = "one arm per requirement; \\[V2G2-627\\]'s obligatory downgrade \
and an ordinary plaintext answer produce the same byte for \
entirely different reasons, and merging them would lose the \
citation that says so"
)]
pub const fn answering(request: Request, address: [u8; 16], port: u16, tls: TlsPolicy) -> Self {
let security = match (request.security, tls) {
(Security::Tls, TlsPolicy::Supported | TlsPolicy::Required)
| (Security::None, TlsPolicy::Required) => Security::Tls,
(Security::Tls, TlsPolicy::Unsupported) => Security::None,
(Security::None, TlsPolicy::Unsupported | TlsPolicy::Supported) => Security::None,
};
Self { address, port, security, transport: request.transport }
}
#[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,
allow_off_link: bool,
interval: crate::session::Millis,
max_attempts: u32,
attempts: u32,
deadline: Option<crate::session::Instant>,
pending: Option<[u8; v2gtp::HEADER_LEN + REQUEST_LEN]>,
outcome: Option<Event>,
notice: Option<Event>,
accepted: Option<Response>,
done: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Event {
Found(Response),
Refused {
response: Response,
reason: Refusal,
},
GaveUp {
attempts: u32,
},
Conflict {
accepted: Response,
other: Response,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Refusal {
SecurityDowngrade,
TransportMismatch,
OffLink,
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SecurityDowngrade => f.write_str("less transport security than requested"),
Self::TransportMismatch => f.write_str("a different transport protocol"),
Self::OffLink => f.write_str("an address that is not link-local"),
}
}
}
impl Discovery {
#[must_use]
pub const fn new(request: Request) -> Self {
Self {
request,
wireless: false,
allow_off_link: false,
interval: crate::session::timers::sdp::RESPONSE_TIMEOUT,
max_attempts: crate::session::timers::sdp::MAX_REQUESTS,
attempts: 0,
deadline: None,
pending: None,
outcome: None,
notice: None,
accepted: None,
done: false,
}
}
#[must_use]
pub const fn wireless(mut self, wireless: bool) -> Self {
self.wireless = wireless;
self
}
#[must_use]
pub const fn allow_off_link(mut self, allow: bool) -> Self {
self.allow_off_link = allow;
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.outcome = None;
self.notice = None;
self.accepted = 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.outcome = Some(Event::GaveUp { attempts: self.attempts });
return;
}
self.send(now);
}
pub fn handle_datagram(
&mut self,
_now: crate::session::Instant,
frame: &[u8],
) -> Result<(), SdpError> {
let response = Response::from_frame(frame)?;
if let Some(accepted) = self.accepted {
if response != accepted && self.refusal(&response).is_none() {
self.notice = Some(Event::Conflict { accepted, other: response });
}
return Ok(());
}
if self.done {
return Ok(());
}
if let Some(reason) = self.refusal(&response) {
self.notice = Some(Event::Refused { response, reason });
return Ok(());
}
self.done = true;
self.deadline = None;
self.pending = None;
self.accepted = Some(response);
self.outcome = Some(Event::Found(response));
Ok(())
}
fn refusal(&self, response: &Response) -> Option<Refusal> {
if response.transport as u8 != self.request.transport as u8 {
return Some(Refusal::TransportMismatch);
}
if matches!((self.request.security, response.security), (Security::Tls, Security::None)) {
return Some(Refusal::SecurityDowngrade);
}
if !self.allow_off_link && !response.is_link_local() {
return Some(Refusal::OffLink);
}
None
}
pub const fn poll_event(&mut self) -> Option<Event> {
match self.notice.take() {
Some(notice) => Some(notice),
None => self.outcome.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,
InvalidAddress([u8; 16]),
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::InvalidAddress(a) if *a == [0u8; 16] => {
f.write_str("SDP response advertises the unspecified address")
}
Self::InvalidAddress(_) => f.write_str("SDP response advertises a multicast address"),
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 answering_tests {
use super::{Request, Response, Security, TlsPolicy, TransportProtocol};
const STATION: [u8; 16] = [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
#[test]
fn the_answer_is_determined_by_the_request_and_the_policy() {
use Security::{None as Plain, Tls};
use TlsPolicy::{Required, Supported, Unsupported};
for (asked, policy, expected) in [
(Tls, Unsupported, Plain),
(Tls, Supported, Tls),
(Tls, Required, Tls),
(Plain, Unsupported, Plain),
(Plain, Supported, Plain),
(Plain, Required, Tls),
] {
let request = Request { security: asked, transport: TransportProtocol::Tcp };
let answer = Response::answering(request, STATION, 15118, policy);
assert_eq!(answer.security, expected, "{asked:?} + {policy:?}");
assert_eq!(answer.transport, TransportProtocol::Tcp, "\\[V2G2-625\\]");
assert_eq!(answer.address, STATION);
assert_eq!(answer.port, 15118);
}
}
#[test]
fn what_a_station_answers_is_what_a_vehicle_accepts_except_the_one_case() {
for policy in [TlsPolicy::Supported, TlsPolicy::Required] {
for request in [Request::TLS, Request::PLAIN] {
let answer = Response::answering(request, STATION, 15118, policy);
assert!(answer.satisfies(&request), "{request:?} + {policy:?}");
}
}
let answer = Response::answering(Request::TLS, STATION, 15118, TlsPolicy::Unsupported);
assert!(!answer.satisfies(&Request::TLS));
let answer = Response::answering(Request::PLAIN, STATION, 15118, TlsPolicy::Unsupported);
assert!(answer.satisfies(&Request::PLAIN));
}
#[test]
fn a_station_answering_with_its_link_local_address_is_accepted() {
let answer = Response::answering(Request::TLS, STATION, 15118, TlsPolicy::Supported);
assert!(answer.is_link_local());
}
#[test]
fn the_answer_survives_a_v2gtp_frame() {
let answer = Response::answering(Request::TLS, STATION, 15118, TlsPolicy::Supported);
let mut frame = [0u8; 64];
let n = answer.write_frame(&mut frame, false).unwrap();
assert_eq!(Response::from_frame(&frame[..n]).unwrap(), answer);
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
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);
}
fn answer(address: [u8; 16], security: Security, transport: TransportProtocol) -> Vec<u8> {
let res = Response { address, port: 15118, security, transport };
let mut frame = [0u8; 64];
let n = res.write_frame(&mut frame, false).unwrap();
frame[..n].to_vec()
}
#[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 wire = answer(LINK_LOCAL, Security::None, TransportProtocol::Tcp);
d.handle_datagram(Instant::ZERO, &wire).unwrap();
assert_eq!(
d.poll_event(),
Some(Event::Refused {
response: Response::from_frame(&wire).unwrap(),
reason: Refusal::SecurityDowngrade,
})
);
}
#[test]
fn a_refused_answer_does_not_end_discovery() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
let _ = d.poll_transmit();
d.handle_datagram(
Instant::ZERO,
&answer(LINK_LOCAL, Security::None, TransportProtocol::Tcp),
)
.unwrap();
assert!(matches!(d.poll_event(), Some(Event::Refused { .. })));
assert!(!d.is_finished(), "one spoofed packet must not end the run");
assert!(d.poll_timeout().is_some(), "the retry is still armed");
assert_eq!(d.attempts(), 1, "a refused answer does not consume an attempt");
let good = answer(LINK_LOCAL, Security::Tls, TransportProtocol::Tcp);
d.handle_datagram(Instant::ZERO, &good).unwrap();
assert_eq!(d.poll_event(), Some(Event::Found(Response::from_frame(&good).unwrap())));
assert!(d.is_finished());
}
#[test]
fn a_flood_of_refusals_cannot_hide_the_outcome() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
let bad = answer(LINK_LOCAL, Security::None, TransportProtocol::Tcp);
for _ in 0..100 {
d.handle_datagram(Instant::ZERO, &bad).unwrap();
}
let good = answer(LINK_LOCAL, Security::Tls, TransportProtocol::Tcp);
d.handle_datagram(Instant::ZERO, &good).unwrap();
assert!(matches!(d.poll_event(), Some(Event::Refused { .. })));
assert_eq!(d.poll_event(), Some(Event::Found(Response::from_frame(&good).unwrap())));
assert_eq!(d.poll_event(), None, "two unread events, not a hundred and one");
}
#[test]
fn an_answer_after_the_attempt_ceiling_does_not_undo_giving_up() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS).with_max_attempts(1);
let mut now = Instant::ZERO;
d.start(now);
now = d.poll_timeout().unwrap();
d.handle_timeout(now);
assert_eq!(d.poll_event(), Some(Event::GaveUp { attempts: 1 }));
d.handle_datagram(now, &answer(LINK_LOCAL, Security::Tls, TransportProtocol::Tcp)).unwrap();
assert_eq!(d.poll_event(), None);
assert!(d.is_finished());
}
#[test]
fn a_second_station_answering_is_reported_rather_than_ignored() {
use crate::session::Instant;
const OTHER: [u8; 16] = [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9];
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
let first = answer(LINK_LOCAL, Security::Tls, TransportProtocol::Tcp);
d.handle_datagram(Instant::ZERO, &first).unwrap();
let accepted = Response::from_frame(&first).unwrap();
assert_eq!(d.poll_event(), Some(Event::Found(accepted)));
assert!(d.is_finished());
let second = answer(OTHER, Security::Tls, TransportProtocol::Tcp);
d.handle_datagram(Instant::ZERO, &second).unwrap();
assert_eq!(
d.poll_event(),
Some(Event::Conflict { accepted, other: Response::from_frame(&second).unwrap() }),
);
d.handle_datagram(Instant::ZERO, &first).unwrap();
assert_eq!(d.poll_event(), None);
d.handle_datagram(Instant::ZERO, &answer(OTHER, Security::None, TransportProtocol::Tcp))
.unwrap();
assert_eq!(d.poll_event(), None);
}
#[test]
fn an_off_link_address_is_refused_by_default() {
use crate::session::Instant;
const GLOBAL: [u8; 16] = [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
let wire = answer(GLOBAL, Security::Tls, TransportProtocol::Tcp);
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
d.handle_datagram(Instant::ZERO, &wire).unwrap();
assert_eq!(
d.poll_event(),
Some(Event::Refused {
response: Response::from_frame(&wire).unwrap(),
reason: Refusal::OffLink,
})
);
assert!(!d.is_finished());
let mut d = Discovery::new(Request::TLS).allow_off_link(true);
d.start(Instant::ZERO);
d.handle_datagram(Instant::ZERO, &wire).unwrap();
assert!(matches!(d.poll_event(), Some(Event::Found(_))));
}
#[test]
fn a_transport_mismatch_is_named_as_one() {
use crate::session::Instant;
let mut d = Discovery::new(Request::TLS);
d.start(Instant::ZERO);
d.handle_datagram(
Instant::ZERO,
&answer(LINK_LOCAL, Security::Tls, TransportProtocol::Udp),
)
.unwrap();
assert!(matches!(
d.poll_event(),
Some(Event::Refused { reason: Refusal::TransportMismatch, .. })
));
}
#[test]
fn an_address_that_is_not_an_endpoint_is_refused_by_the_codec() {
let mut payload = [0u8; RESPONSE_LEN];
payload[16] = 0x3B; payload[17] = 0x0E;
assert_eq!(
Response::from_payload(&payload),
Err(SdpError::InvalidAddress([0u8; 16])),
"the unspecified address"
);
payload[0] = 0xff;
payload[15] = 0x01; assert!(matches!(Response::from_payload(&payload), Err(SdpError::InvalidAddress(_))));
payload[0] = 0xfe;
payload[1] = 0x80;
assert!(Response::from_payload(&payload).is_ok(), "a link-local address is fine");
}
#[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);
}
}