Skip to content
File

Blob: firmware/vendor/sctp-proto/src/endpoint/mod.rs

rust707 lines
1#[cfg(test)]
2mod endpoint_test;
3 
4use alloc::boxed::Box;
5use alloc::collections::VecDeque;
6use alloc::string::{String, ToString};
7use alloc::sync::Arc;
8use core::fmt;
9use core::iter;
10use core::net::{IpAddr, SocketAddr};
11use core::ops::{Index, IndexMut};
12use std::collections::HashMap;
13use std::time::Instant;
14 
15use crate::chunk::{chunk_init::ChunkInit, chunk_type::CT_INIT};
16use crate::config::MAX_SNAP_INIT_BYTES;
17use crate::config::{ClientConfig, EndpointConfig, ServerConfig, TransportConfig};
18use crate::packet::PartialDecode;
19use crate::shared::AssociationEvent;
20use crate::shared::{AssociationEventInner, AssociationId};
21use crate::shared::{EndpointEvent, EndpointEventInner};
22use crate::util::{AssociationIdGenerator, RandomAssociationIdGenerator};
23use crate::{EcnCodepoint, Payload, Transmit};
24use crate::{association::Association, chunk::Chunk};
25 
26use bytes::Bytes;
27use log::{debug, trace, warn};
28use rand::{SeedableRng, rngs::StdRng};
29use rustc_hash::FxHashMap;
30use slab::Slab;
31use thiserror::Error;
32 
33/// The main entry point to the library
34///
35/// This object performs no I/O whatsoever. Instead, it generates a stream of packets to send via
36/// `poll_transmit`, and consumes incoming packets and association-generated events via `handle` and
37/// `handle_event`.
38pub struct Endpoint {
39 rng: StdRng,
40 transmits: VecDeque<Transmit>,
41 /// Identifies associations based on the INIT Dst AID the peer utilized
42 ///
43 /// Uses a standard `HashMap` to protect against hash collision attacks.
44 association_ids_init: HashMap<AssociationId, AssociationHandle>,
45 /// Identifies associations based on locally created CIDs
46 ///
47 /// Uses a cheaper hash function since keys are locally created
48 association_ids: FxHashMap<AssociationId, AssociationHandle>,
49 
50 associations: Slab<AssociationMeta>,
51 local_cid_generator: Box<dyn AssociationIdGenerator>,
52 config: Arc<EndpointConfig>,
53 server_config: Option<Arc<ServerConfig>>,
54 /// Whether incoming associations should be unconditionally rejected by a server
55 ///
56 /// Equivalent to a `ServerConfig.accept_buffer` of `0`, but can
57 /// be changed after the endpoint is constructed.
58 reject_new_associations: bool,
59}
60 
61impl fmt::Debug for Endpoint {
62 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
63 fmt.debug_struct("Endpoint<T>")
64 .field("rng", &self.rng)
65 .field("transmits", &self.transmits)
66 .field("association_ids_initial", &self.association_ids_init)
67 .field("association_ids", &self.association_ids)
68 .field("associations", &self.associations)
69 .field("config", &self.config)
70 .field("server_config", &self.server_config)
71 .field("reject_new_associations", &self.reject_new_associations)
72 .finish()
73 }
74}
75 
76impl Endpoint {
77 /// Create a new endpoint
78 ///
79 /// Returns `Err` if the configuration is invalid.
80 pub fn new(config: Arc<EndpointConfig>, server_config: Option<Arc<ServerConfig>>) -> Self {
81 let rng = {
82 let mut base = rand::rng();
83 StdRng::from_rng(&mut base)
84 };
85 Self {
86 rng,
87 transmits: VecDeque::new(),
88 association_ids_init: HashMap::default(),
89 association_ids: FxHashMap::default(),
90 associations: Slab::new(),
91 local_cid_generator: (config.aid_generator_factory.as_ref())(),
92 reject_new_associations: false,
93 config,
94 server_config,
95 }
96 }
97 
98 /// Get the next packet to transmit
99 #[must_use]
100 pub fn poll_transmit(&mut self) -> Option<Transmit> {
101 self.transmits.pop_front()
102 }
103 
104 /// Replace the server configuration, affecting new incoming associations only
105 pub fn set_server_config(&mut self, server_config: Option<Arc<ServerConfig>>) {
106 self.server_config = server_config;
107 }
108 
109 /// Process `EndpointEvent`s emitted from related `Association`s
110 ///
111 /// In turn, processing this event may return a `AssociationEvent` for the same `Association`.
112 pub fn handle_event(
113 &mut self,
114 ch: AssociationHandle,
115 event: EndpointEvent,
116 ) -> Option<AssociationEvent> {
117 match event.0 {
118 EndpointEventInner::Drained => {
119 let conn = self.associations.remove(ch.0);
120 self.association_ids_init.remove(&conn.init_cid);
121 for cid in conn.loc_cids.values() {
122 self.association_ids.remove(cid);
123 }
124 }
125 }
126 None
127 }
128 
129 /// Process an incoming UDP datagram
130 pub fn handle(
131 &mut self,
132 now: Instant,
133 remote: SocketAddr,
134 local_ip: Option<IpAddr>,
135 ecn: Option<EcnCodepoint>,
136 data: Bytes,
137 ) -> Option<(AssociationHandle, DatagramEvent)> {
138 let partial_decode = match PartialDecode::unmarshal(&data) {
139 Ok(x) => x,
140 Err(err) => {
141 trace!("malformed header: {}", err);
142 return None;
143 }
144 };
145 
146 //
147 // Handle packet on existing association, if any
148 //
149 let dst_cid = partial_decode.common_header.verification_tag;
150 let known_ch = if dst_cid > 0 {
151 self.association_ids.get(&dst_cid).cloned()
152 } else {
153 //TODO: improve INIT handling for DoS attack
154 if partial_decode.first_chunk_type == CT_INIT {
155 if let Some(dst_cid) = partial_decode.initiate_tag {
156 self.association_ids_init.get(&dst_cid).cloned()
157 } else {
158 None
159 }
160 } else {
161 None
162 }
163 };
164 
165 if let Some(ch) = known_ch {
166 return Some((
167 ch,
168 DatagramEvent::AssociationEvent(AssociationEvent(AssociationEventInner::Datagram(
169 Transmit {
170 now,
171 remote,
172 ecn,
173 payload: Payload::PartialDecode(partial_decode),
174 local_ip,
175 },
176 ))),
177 ));
178 }
179 
180 //
181 // Potentially create a new association
182 //
183 self.handle_first_packet(now, remote, local_ip, ecn, partial_decode)
184 .map(|(ch, a)| (ch, DatagramEvent::NewAssociation(a)))
185 }
186 
187 /// Initiate an association.
188 ///
189 /// If SNAP tokens (INIT chunks) are provided via [`ClientConfig::with_snap`],
190 /// the association will skip the SCTP 4-way handshake (RFC 4960 Section 5.1)
191 /// and immediately transition to the ESTABLISHED state.
192 ///
193 /// Both `local_sctp_init` and `remote_sctp_init` must be provided for SNAP;
194 /// use [`generate_snap_token`](crate::generate_snap_token) to create the
195 /// local token.
196 ///
197 /// If only one side is set (e.g. the peer does not support SNAP), the
198 /// association falls back to the normal SCTP handshake.
199 ///
200 /// **Note:** When using SNAP, **both** peers must call [`Endpoint::connect`]
201 /// (there is no server-side SNAP via [`Endpoint::handle`]). Each peer
202 /// generates its own token via [`generate_snap_token`](crate::generate_snap_token),
203 /// exchanges it with the remote peer through a signaling channel (e.g.,
204 /// SDP `a=sctp-init`), and then calls `connect` with `with_snap(local, remote)`.
205 ///
206 /// See [draft-hancke-tsvwg-snap-01](https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/).
207 pub fn connect(
208 &mut self,
209 config: ClientConfig,
210 remote: SocketAddr,
211 ) -> Result<(AssociationHandle, Association), ConnectError> {
212 if self.is_full() {
213 return Err(ConnectError::TooManyAssociations);
214 }
215 if remote.port() == 0 {
216 return Err(ConnectError::InvalidRemoteAddress(remote));
217 }
218 
219 match (config.local_sctp_init, config.remote_sctp_init) {
220 (Some(local_init), Some(remote_init)) => {
221 self.connect_with_snap(config.transport, remote, local_init, remote_init)
222 }
223 (partial_local, partial_remote) => {
224 if partial_local.is_some() || partial_remote.is_some() {
225 warn!(
226 "partial SNAP config: both local_sctp_init and \
227 remote_sctp_init must be set; falling back to normal handshake"
228 );
229 }
230 let remote_aid = RandomAssociationIdGenerator::new().generate_aid();
231 let local_aid = self.new_aid();
232 
233 Ok(self.add_association(
234 remote_aid,
235 local_aid,
236 remote,
237 None,
238 Instant::now(),
239 None,
240 config.transport,
241 ))
242 }
243 }
244 }
245 
246 /// Create an association using SNAP (out-of-band exchanged INIT chunks).
247 fn connect_with_snap(
248 &mut self,
249 transport: Arc<TransportConfig>,
250 remote: SocketAddr,
251 local_snap_bytes: Bytes,
252 remote_snap_bytes: Bytes,
253 ) -> Result<(AssociationHandle, Association), ConnectError> {
254 // Enforce a size ceiling on raw INIT bytes before parsing.
255 if local_snap_bytes.len() > MAX_SNAP_INIT_BYTES {
256 return Err(ConnectError::Snap(SnapError::OversizedInit {
257 side: SnapSide::Local,
258 len: local_snap_bytes.len(),
259 }));
260 }
261 if remote_snap_bytes.len() > MAX_SNAP_INIT_BYTES {
262 return Err(ConnectError::Snap(SnapError::OversizedInit {
263 side: SnapSide::Remote,
264 len: remote_snap_bytes.len(),
265 }));
266 }
267 
268 let local_init = ChunkInit::unmarshal(&local_snap_bytes).map_err(|err| {
269 ConnectError::Snap(SnapError::ParseFailed {
270 side: SnapSide::Local,
271 reason: err.to_string(),
272 })
273 })?;
274 let remote_init = ChunkInit::unmarshal(&remote_snap_bytes).map_err(|err| {
275 ConnectError::Snap(SnapError::ParseFailed {
276 side: SnapSide::Remote,
277 reason: err.to_string(),
278 })
279 })?;
280 
281 // Validate both chunks are INIT (not INIT-ACK)
282 if local_init.is_ack {
283 return Err(ConnectError::Snap(SnapError::InvalidInitAck {
284 side: SnapSide::Local,
285 }));
286 }
287 if remote_init.is_ack {
288 return Err(ConnectError::Snap(SnapError::InvalidInitAck {
289 side: SnapSide::Remote,
290 }));
291 }
292 
293 let local_aid = local_init.initiate_tag;
294 let remote_aid = remote_init.initiate_tag;
295 
296 // RFC 4960 Section 3.3.2: The Initiate Tag MUST NOT take the value 0.
297 if local_aid == 0 {
298 return Err(ConnectError::Snap(SnapError::ZeroInitiateTag {
299 side: SnapSide::Local,
300 }));
301 }
302 if remote_aid == 0 {
303 return Err(ConnectError::Snap(SnapError::ZeroInitiateTag {
304 side: SnapSide::Remote,
305 }));
306 }
307 
308 // initial_tsn=0 is degenerate: wrapping_sub(1) yields u32::MAX for
309 // peer_last_tsn, which would accept nearly all incoming TSNs.
310 if local_init.initial_tsn == 0 {
311 return Err(ConnectError::Snap(SnapError::ZeroInitialTsn {
312 side: SnapSide::Local,
313 }));
314 }
315 if remote_init.initial_tsn == 0 {
316 return Err(ConnectError::Snap(SnapError::ZeroInitialTsn {
317 side: SnapSide::Remote,
318 }));
319 }
320 
321 // RFC 4960 §3.3.2 / §5.1: validate stream counts, a_rwnd, etc.
322 local_init.check().map_err(|err| {
323 ConnectError::Snap(SnapError::InvalidInit {
324 side: SnapSide::Local,
325 reason: err.to_string(),
326 })
327 })?;
328 remote_init.check().map_err(|err| {
329 ConnectError::Snap(SnapError::InvalidInit {
330 side: SnapSide::Remote,
331 reason: err.to_string(),
332 })
333 })?;
334 
335 // Check for collision BEFORE allocating any resources.
336 if local_aid == remote_aid {
337 return Err(ConnectError::Snap(SnapError::AidCollision {
338 kind: AidCollisionKind::LocalEqualsRemote,
339 aid: local_aid,
340 }));
341 }
342 if self.association_ids.contains_key(&local_aid) {
343 return Err(ConnectError::Snap(SnapError::AidCollision {
344 kind: AidCollisionKind::LocalInAssociationIds,
345 aid: local_aid,
346 }));
347 }
348 if self.association_ids.contains_key(&remote_aid) {
349 return Err(ConnectError::Snap(SnapError::AidCollision {
350 kind: AidCollisionKind::RemoteInAssociationIds,
351 aid: remote_aid,
352 }));
353 }
354 if self.association_ids_init.contains_key(&remote_aid) {
355 return Err(ConnectError::Snap(SnapError::AidCollision {
356 kind: AidCollisionKind::RemoteInAssociationIdsInit,
357 aid: remote_aid,
358 }));
359 }
360 if self.association_ids_init.contains_key(&local_aid) {
361 return Err(ConnectError::Snap(SnapError::AidCollision {
362 kind: AidCollisionKind::LocalInAssociationIdsInit,
363 aid: local_aid,
364 }));
365 }
366 
367 let conn = Association::new_with_out_of_band_init(
368 transport,
369 self.config.get_max_payload_size(),
370 remote,
371 None,
372 local_init,
373 remote_init,
374 )
375 .map_err(|err| ConnectError::Snap(SnapError::AssociationFailed(err.to_string())))?;
376 
377 let id = self.associations.insert(AssociationMeta {
378 init_cid: remote_aid,
379 cids_issued: 1,
380 loc_cids: iter::once((0, local_aid)).collect(),
381 initial_remote: remote,
382 });
383 
384 let ch = AssociationHandle(id);
385 self.association_ids.insert(local_aid, ch);
386 self.association_ids_init.insert(remote_aid, ch);
387 
388 debug!(
389 "Created SNAP association: local_aid={:#x} remote_aid={:#x}",
390 local_aid, remote_aid
391 );
392 
393 Ok((ch, conn))
394 }
395 
396 fn new_aid(&mut self) -> AssociationId {
397 loop {
398 let aid = self.local_cid_generator.generate_aid();
399 if !self.association_ids.contains_key(&aid) {
400 break aid;
401 }
402 }
403 }
404 
405 fn handle_first_packet(
406 &mut self,
407 now: Instant,
408 remote: SocketAddr,
409 local_ip: Option<IpAddr>,
410 ecn: Option<EcnCodepoint>,
411 partial_decode: PartialDecode,
412 ) -> Option<(AssociationHandle, Association)> {
413 if partial_decode.first_chunk_type != CT_INIT
414 || (partial_decode.first_chunk_type == CT_INIT && partial_decode.initiate_tag.is_none())
415 {
416 debug!("refusing first packet with Non-INIT or emtpy initial_tag INIT");
417 return None;
418 }
419 
420 let server_config = self.server_config.as_ref().unwrap();
421 
422 if self.associations.len() >= server_config.concurrent_associations as usize
423 || self.reject_new_associations
424 || self.is_full()
425 {
426 debug!("refusing association");
427 //TODO: self.initial_close();
428 return None;
429 }
430 
431 let server_config = server_config.clone();
432 let transport_config = server_config.transport.clone();
433 
434 let remote_aid = *partial_decode.initiate_tag.as_ref().unwrap();
435 let local_aid = self.new_aid();
436 
437 let (ch, mut conn) = self.add_association(
438 remote_aid,
439 local_aid,
440 remote,
441 local_ip,
442 now,
443 Some(server_config),
444 transport_config,
445 );
446 
447 // Map the peer's INIT Initiate Tag so that retransmitted INITs (which use
448 // verification_tag=0) can be routed to the association created for the first INIT.
449 self.association_ids_init.insert(remote_aid, ch);
450 
451 conn.handle_event(AssociationEvent(AssociationEventInner::Datagram(
452 Transmit {
453 now,
454 remote,
455 ecn,
456 payload: Payload::PartialDecode(partial_decode),
457 local_ip,
458 },
459 )));
460 
461 Some((ch, conn))
462 }
463 
464 #[allow(clippy::too_many_arguments)]
465 fn add_association(
466 &mut self,
467 remote_aid: AssociationId,
468 local_aid: AssociationId,
469 remote_addr: SocketAddr,
470 local_ip: Option<IpAddr>,
471 now: Instant,
472 server_config: Option<Arc<ServerConfig>>,
473 transport_config: Arc<TransportConfig>,
474 ) -> (AssociationHandle, Association) {
475 let conn = Association::new(
476 server_config,
477 transport_config,
478 self.config.get_max_payload_size(),
479 local_aid,
480 remote_addr,
481 local_ip,
482 now,
483 );
484 
485 let id = self.associations.insert(AssociationMeta {
486 init_cid: remote_aid,
487 cids_issued: 1,
488 loc_cids: iter::once((0, local_aid)).collect(),
489 initial_remote: remote_addr,
490 });
491 
492 let ch = AssociationHandle(id);
493 self.association_ids.insert(local_aid, ch);
494 
495 (ch, conn)
496 }
497 
498 /// Unconditionally reject future incoming associations
499 pub fn reject_new_associations(&mut self) {
500 self.reject_new_associations = true;
501 }
502 
503 /// Access the configuration used by this endpoint
504 pub fn config(&self) -> &EndpointConfig {
505 &self.config
506 }
507 
508 /// Whether we've used up 3/4 of the available AID space
509 fn is_full(&self) -> bool {
510 (((u32::MAX >> 1) + (u32::MAX >> 2)) as usize) < self.association_ids.len()
511 }
512}
513 
514#[derive(Debug)]
515pub(crate) struct AssociationMeta {
516 init_cid: AssociationId,
517 /// Number of local association IDs.
518 cids_issued: u64,
519 loc_cids: FxHashMap<u64, AssociationId>,
520 /// Remote address the association began with
521 ///
522 /// Only needed to support associations with zero-length AIDs, which cannot migrate, so we don't
523 /// bother keeping it up to date.
524 initial_remote: SocketAddr,
525}
526 
527/// Internal identifier for an `Association` currently associated with an endpoint
528#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
529pub struct AssociationHandle(pub usize);
530 
531impl From<AssociationHandle> for usize {
532 fn from(x: AssociationHandle) -> usize {
533 x.0
534 }
535}
536 
537impl Index<AssociationHandle> for Slab<AssociationMeta> {
538 type Output = AssociationMeta;
539 fn index(&self, ch: AssociationHandle) -> &AssociationMeta {
540 &self[ch.0]
541 }
542}
543 
544impl IndexMut<AssociationHandle> for Slab<AssociationMeta> {
545 fn index_mut(&mut self, ch: AssociationHandle) -> &mut AssociationMeta {
546 &mut self[ch.0]
547 }
548}
549 
550/// Event resulting from processing a single datagram
551#[allow(clippy::large_enum_variant)] // Not passed around extensively
552pub enum DatagramEvent {
553 /// The datagram is redirected to its `Association`
554 AssociationEvent(AssociationEvent),
555 /// The datagram has resulted in starting a new `Association`
556 NewAssociation(Association),
557}
558 
559/// Errors in the parameters being used to create a new association
560///
561/// These arise before any I/O has been performed.
562#[non_exhaustive]
563#[derive(Debug, Error, Clone, PartialEq, Eq)]
564pub enum ConnectError {
565 /// The endpoint can no longer create new associations
566 ///
567 /// Indicates that a necessary component of the endpoint has been dropped or otherwise disabled.
568 #[error("endpoint stopping")]
569 EndpointStopping,
570 /// The number of active associations on the local endpoint is at the limit
571 ///
572 /// Try using longer association IDs.
573 #[error("too many associations")]
574 TooManyAssociations,
575 /// The domain name supplied was malformed
576 #[error("invalid DNS name: {0}")]
577 InvalidDnsName(String),
578 /// The remote [`SocketAddr`] supplied was malformed
579 ///
580 /// Examples include attempting to connect to port 0, or using an inappropriate address family.
581 #[error("invalid remote address: {0}")]
582 InvalidRemoteAddress(SocketAddr),
583 /// No default client configuration was set up
584 ///
585 /// Use `Endpoint::connect_with` to specify a client configuration.
586 #[error("no default client config")]
587 NoDefaultClientConfig,
588 /// Out-of-band SNAP setup error.
589 #[error("SNAP error: {0}")]
590 Snap(#[from] SnapError),
591}
592 
593/// Which peer's token (INIT chunk) caused a [`SnapError`].
594#[non_exhaustive]
595#[derive(Debug, Clone, Copy, PartialEq, Eq)]
596pub enum SnapSide {
597 /// The locally-generated token.
598 Local,
599 /// The remote peer's token.
600 Remote,
601}
602 
603impl fmt::Display for SnapSide {
604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605 match self {
606 SnapSide::Local => f.write_str("local"),
607 SnapSide::Remote => f.write_str("remote"),
608 }
609 }
610}
611 
612/// Which routing table detected an association-ID collision in [`SnapError::AidCollision`].
613#[non_exhaustive]
614#[derive(Debug, Clone, Copy, PartialEq, Eq)]
615pub enum AidCollisionKind {
616 /// The local and remote association IDs are equal.
617 LocalEqualsRemote,
618 /// The local AID is already present in `association_ids`.
619 LocalInAssociationIds,
620 /// The remote AID is already present in `association_ids`.
621 RemoteInAssociationIds,
622 /// The remote AID is already present in `association_ids_init`.
623 RemoteInAssociationIdsInit,
624 /// The local AID is already present in `association_ids_init`.
625 LocalInAssociationIdsInit,
626}
627 
628impl fmt::Display for AidCollisionKind {
629 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630 match self {
631 AidCollisionKind::LocalEqualsRemote => f.write_str("local_aid equals remote_aid"),
632 AidCollisionKind::LocalInAssociationIds => f.write_str("local_aid in association_ids"),
633 AidCollisionKind::RemoteInAssociationIds => {
634 f.write_str("remote_aid in association_ids")
635 }
636 AidCollisionKind::RemoteInAssociationIdsInit => {
637 f.write_str("remote_aid in association_ids_init")
638 }
639 AidCollisionKind::LocalInAssociationIdsInit => {
640 f.write_str("local_aid in association_ids_init")
641 }
642 }
643 }
644}
645 
646/// Specific errors that can occur during SNAP (out-of-band token) setup.
647///
648/// These are not part of the stable API — match on [`ConnectError::Snap`]
649/// and use the `Display` impl for user-facing messages.
650#[non_exhaustive]
651#[derive(Debug, Error, Clone, PartialEq, Eq)]
652pub enum SnapError {
653 /// Failed to parse token (INIT chunk) bytes.
654 #[error("failed to parse {side} SNAP token: {reason}")]
655 ParseFailed {
656 /// Which side's token failed to parse.
657 side: SnapSide,
658 /// The underlying parse error message.
659 reason: String,
660 },
661 /// The provided bytes were an INIT-ACK, not an INIT.
662 #[error("invalid {side} SNAP token: expected INIT, got INIT-ACK")]
663 InvalidInitAck {
664 /// Which side provided an INIT-ACK instead of an INIT.
665 side: SnapSide,
666 },
667 /// The token's `initiate_tag` is zero (RFC 4960 §3.3.2 violation).
668 #[error("{side} SNAP token has zero initiate_tag")]
669 ZeroInitiateTag {
670 /// Which side has the zero tag.
671 side: SnapSide,
672 },
673 /// The token's `initial_tsn` is zero.
674 #[error("{side} SNAP token has zero initial_tsn")]
675 ZeroInitialTsn {
676 /// Which side has the zero TSN.
677 side: SnapSide,
678 },
679 /// An association ID collision was detected.
680 #[error("SNAP collision: {kind} {aid:#x} already in use")]
681 AidCollision {
682 /// Which routing table collided.
683 kind: AidCollisionKind,
684 /// The colliding association ID.
685 aid: u32,
686 },
687 /// The token (INIT chunk) bytes exceed the maximum allowed size.
688 #[error("{side} SNAP token too large: {len} bytes (max {max})", max = MAX_SNAP_INIT_BYTES)]
689 OversizedInit {
690 /// Which side sent oversized bytes.
691 side: SnapSide,
692 /// Actual byte length.
693 len: usize,
694 },
695 /// The parsed token (INIT chunk) failed RFC 4960 validation.
696 #[error("invalid {side} SNAP token: {reason}")]
697 InvalidInit {
698 /// Which side has the invalid token.
699 side: SnapSide,
700 /// The underlying validation error message.
701 reason: String,
702 },
703 /// Association creation failed.
704 #[error("failed to create SNAP association: {0}")]
705 AssociationFailed(String),
706}