1use crate::config::SubscriptionConfig;
2use crate::error::McrxError;
3#[cfg(feature = "metrics")]
4use crate::metrics::SubscriptionMetricsSnapshot;
5use crate::packet::{Packet, PacketWithMetadata};
6use crate::platform::{ReceiveSocket, recv_packet, recv_packet_with_metadata, socket_local_addr};
7use socket2::Socket;
8use std::net::SocketAddr;
9#[cfg(feature = "metrics")]
10use std::sync::Mutex;
11#[cfg(feature = "metrics")]
12use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
13#[cfg(feature = "metrics")]
14use std::time::SystemTime;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct SubscriptionId(pub u64);
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SubscriptionState {
26 Bound,
28 Joined,
30}
31
32#[derive(Debug)]
35pub struct SubscriptionParts {
36 pub id: SubscriptionId,
38 pub config: SubscriptionConfig,
40 pub socket: Socket,
42 pub state: SubscriptionState,
44}
45
46#[cfg(feature = "metrics")]
47#[derive(Debug)]
48struct LastPacketMetrics {
49 source: Option<SocketAddr>,
50 receive_at: Option<SystemTime>,
51}
52
53#[cfg(feature = "metrics")]
54#[derive(Debug)]
55struct SubscriptionMetricsInner {
56 packets_received: AtomicU64,
57 bytes_received: AtomicU64,
58 would_block_count: AtomicU64,
59 receive_errors: AtomicU64,
60 join_count: AtomicU64,
61 leave_count: AtomicU64,
62 last_payload_len: AtomicUsize,
63 last_packet: Mutex<LastPacketMetrics>,
64}
65
66#[cfg(feature = "metrics")]
67impl Default for SubscriptionMetricsInner {
68 fn default() -> Self {
69 Self {
70 packets_received: AtomicU64::new(0),
71 bytes_received: AtomicU64::new(0),
72 would_block_count: AtomicU64::new(0),
73 receive_errors: AtomicU64::new(0),
74 join_count: AtomicU64::new(0),
75 leave_count: AtomicU64::new(0),
76 last_payload_len: AtomicUsize::new(usize::MAX),
77 last_packet: Mutex::new(LastPacketMetrics {
78 source: None,
79 receive_at: None,
80 }),
81 }
82 }
83}
84
85#[derive(Debug)]
87pub struct Subscription {
88 id: SubscriptionId,
89 config: SubscriptionConfig,
90 socket: ReceiveSocket,
91 state: SubscriptionState,
92 #[cfg(feature = "metrics")]
93 metrics: SubscriptionMetricsInner,
94}
95
96impl Subscription {
97 #[cfg(feature = "metrics")]
98 fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
99 match mutex.lock() {
100 Ok(guard) => guard,
101 Err(poisoned) => poisoned.into_inner(),
102 }
103 }
104
105 fn with_socket(id: SubscriptionId, config: SubscriptionConfig, socket: ReceiveSocket) -> Self {
106 Self {
107 id,
108 config,
109 socket,
110 state: SubscriptionState::Bound,
111 #[cfg(feature = "metrics")]
112 metrics: SubscriptionMetricsInner::default(),
113 }
114 }
115
116 #[cfg(feature = "metrics")]
117 fn record_received_packet(&self, packet: &Packet) {
118 self.metrics
119 .packets_received
120 .fetch_add(1, Ordering::Relaxed);
121 self.metrics
122 .bytes_received
123 .fetch_add(packet.payload.len() as u64, Ordering::Relaxed);
124 self.metrics
125 .last_payload_len
126 .store(packet.payload.len(), Ordering::Relaxed);
127 let mut last_packet = Self::lock_unpoisoned(&self.metrics.last_packet);
128 last_packet.source = Some(packet.source);
129 last_packet.receive_at = Some(SystemTime::now());
130 }
131
132 #[cfg(feature = "metrics")]
133 fn record_would_block(&self) {
134 self.metrics
135 .would_block_count
136 .fetch_add(1, Ordering::Relaxed);
137 }
138
139 #[cfg(feature = "metrics")]
140 fn record_receive_error(&self) {
141 self.metrics.receive_errors.fetch_add(1, Ordering::Relaxed);
142 }
143
144 pub fn new(id: SubscriptionId, config: SubscriptionConfig, socket: Socket) -> Self {
152 let receive_socket = ReceiveSocket::adopt(socket, &config);
153 Self::with_socket(id, config, receive_socket)
154 }
155
156 pub(crate) fn from_receive_socket(
157 id: SubscriptionId,
158 config: SubscriptionConfig,
159 socket: ReceiveSocket,
160 ) -> Self {
161 Self::with_socket(id, config, socket)
162 }
163
164 pub fn id(&self) -> SubscriptionId {
166 self.id
167 }
168
169 pub fn config(&self) -> &SubscriptionConfig {
171 &self.config
172 }
173
174 pub fn socket(&self) -> &Socket {
176 self.socket.socket()
177 }
178
179 pub fn socket_mut(&mut self) -> &mut Socket {
184 self.socket.socket_mut()
185 }
186
187 pub fn try_recv(&self) -> Result<Option<Packet>, McrxError> {
194 if !self.is_joined() {
195 return Err(McrxError::SubscriptionNotJoined);
196 }
197 match recv_packet(&self.socket, self.id, &self.config) {
198 Ok(Some(packet)) => {
199 #[cfg(feature = "metrics")]
200 self.record_received_packet(&packet);
201
202 Ok(Some(packet))
203 }
204 Ok(None) => {
205 #[cfg(feature = "metrics")]
206 self.record_would_block();
207
208 Ok(None)
209 }
210 Err(err) => {
211 #[cfg(feature = "metrics")]
212 self.record_receive_error();
213
214 Err(err)
215 }
216 }
217 }
218
219 pub fn try_recv_with_metadata(&self) -> Result<Option<PacketWithMetadata>, McrxError> {
222 if !self.is_joined() {
223 return Err(McrxError::SubscriptionNotJoined);
224 }
225
226 match recv_packet_with_metadata(&self.socket, self.id, &self.config) {
227 Ok(Some(packet)) => {
228 #[cfg(feature = "metrics")]
229 self.record_received_packet(&packet.packet);
230
231 Ok(Some(packet))
232 }
233 Ok(None) => {
234 #[cfg(feature = "metrics")]
235 self.record_would_block();
236
237 Ok(None)
238 }
239 Err(err) => {
240 #[cfg(feature = "metrics")]
241 self.record_receive_error();
242
243 Err(err)
244 }
245 }
246 }
247
248 #[cfg(unix)]
252 pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
253 use std::os::fd::AsRawFd;
254 self.socket().as_raw_fd()
255 }
256
257 #[cfg(windows)]
261 pub fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
262 use std::os::windows::io::AsRawSocket;
263 self.socket().as_raw_socket()
264 }
265
266 pub fn local_addr(&self) -> Result<SocketAddr, McrxError> {
268 self.socket
269 .local_addr()
270 .or_else(|_| socket_local_addr(self.socket()))
271 }
272
273 pub fn into_socket(self) -> Socket {
278 self.socket.into_socket()
279 }
280
281 pub fn into_parts(self) -> SubscriptionParts {
283 SubscriptionParts {
284 id: self.id,
285 config: self.config,
286 socket: self.socket.into_socket(),
287 state: self.state,
288 }
289 }
290
291 pub fn state(&self) -> SubscriptionState {
296 self.state
297 }
298
299 #[cfg(feature = "metrics")]
304 pub fn metrics_snapshot(&self) -> SubscriptionMetricsSnapshot {
305 let last_payload_len = match self.metrics.last_payload_len.load(Ordering::Relaxed) {
306 usize::MAX => None,
307 payload_len => Some(payload_len),
308 };
309 let last_packet = Self::lock_unpoisoned(&self.metrics.last_packet);
310
311 SubscriptionMetricsSnapshot {
312 packets_received: self.metrics.packets_received.load(Ordering::Relaxed),
313 bytes_received: self.metrics.bytes_received.load(Ordering::Relaxed),
314 would_block_count: self.metrics.would_block_count.load(Ordering::Relaxed),
315 receive_errors: self.metrics.receive_errors.load(Ordering::Relaxed),
316 join_count: self.metrics.join_count.load(Ordering::Relaxed),
317 leave_count: self.metrics.leave_count.load(Ordering::Relaxed),
318 last_payload_len,
319 last_source: last_packet.source,
320 last_receive_at: last_packet.receive_at,
321 captured_at: SystemTime::now(),
322 }
323 }
324
325 pub fn is_joined(&self) -> bool {
330 matches!(self.state, SubscriptionState::Joined)
331 }
332
333 pub fn mark_joined(&mut self) -> Result<(), McrxError> {
340 if self.state == SubscriptionState::Joined {
341 return Err(McrxError::SubscriptionAlreadyJoined);
342 }
343
344 self.state = SubscriptionState::Joined;
345
346 #[cfg(feature = "metrics")]
347 self.metrics.join_count.fetch_add(1, Ordering::Relaxed);
348
349 Ok(())
350 }
351
352 pub fn mark_bound(&mut self) -> Result<(), McrxError> {
359 if self.state == SubscriptionState::Bound {
360 return Err(McrxError::SubscriptionNotJoined);
361 }
362
363 self.state = SubscriptionState::Bound;
364
365 #[cfg(feature = "metrics")]
366 self.metrics.leave_count.fetch_add(1, Ordering::Relaxed);
367
368 Ok(())
369 }
370}
371
372#[cfg(unix)]
373impl std::os::fd::AsFd for Subscription {
374 fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
375 self.socket().as_fd()
376 }
377}
378
379#[cfg(unix)]
380impl std::os::fd::AsRawFd for Subscription {
381 fn as_raw_fd(&self) -> std::os::fd::RawFd {
382 self.socket().as_raw_fd()
383 }
384}
385
386#[cfg(windows)]
387impl std::os::windows::io::AsSocket for Subscription {
388 fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
389 self.socket().as_socket()
390 }
391}
392
393#[cfg(windows)]
394impl std::os::windows::io::AsRawSocket for Subscription {
395 fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
396 self.socket().as_raw_socket()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use crate::config::{SourceFilter, SubscriptionConfig};
404 use crate::platform;
405 use crate::test_support::{
406 ipv6_group_socket_addr, ipv6_multicast_loopback_available, make_multicast_sender,
407 make_multicast_sender_v6, make_multicast_sender_v6_for_source,
408 sample_config_on_unused_port, sample_config_v6_on_unused_port,
409 sample_ssm_receive_config_v6_on_unused_port, unused_udp_port_v4,
410 };
411 use socket2::SockRef;
412 use std::io::ErrorKind;
413 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket};
414 use std::time::{Duration, Instant};
415
416 fn test_ssm_config(port: u16, interface: Ipv4Addr) -> SubscriptionConfig {
417 SubscriptionConfig {
418 group: IpAddr::V4(Ipv4Addr::new(232, 1, 2, 3)),
419 source: SourceFilter::Source(IpAddr::V4(interface)),
420 dst_port: port,
421 interface: Some(IpAddr::V4(interface)),
422 interface_index: None,
423 }
424 }
425
426 fn ipv4_group(config: &SubscriptionConfig) -> Ipv4Addr {
427 config.ipv4_membership().unwrap().group
428 }
429
430 fn ipv4_group_socket_addr(config: &SubscriptionConfig) -> SocketAddrV4 {
431 SocketAddrV4::new(ipv4_group(config), config.dst_port)
432 }
433
434 fn primary_ipv4() -> Ipv4Addr {
435 let probe = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).unwrap();
436 probe
437 .connect(SocketAddrV4::new(Ipv4Addr::new(8, 8, 8, 8), 9))
438 .unwrap();
439
440 match probe.local_addr().unwrap() {
441 SocketAddr::V4(addr) => *addr.ip(),
442 SocketAddr::V6(_) => panic!("expected an IPv4 local address for SSM test"),
443 }
444 }
445
446 fn recv_next_subscription_packet(subscription: &Subscription, deadline: Instant) -> Packet {
447 loop {
448 match subscription.try_recv().unwrap() {
449 Some(packet) => return packet,
450 None if Instant::now() < deadline => {
451 std::thread::sleep(Duration::from_millis(10));
452 }
453 None => panic!("timed out waiting for packet"),
454 }
455 }
456 }
457
458 fn assert_pktinfo_metadata(packet: &PacketWithMetadata, expected_destination: IpAddr) {
459 #[cfg(any(
460 target_os = "linux",
461 target_os = "android",
462 windows,
463 target_vendor = "apple",
464 target_os = "freebsd",
465 target_os = "dragonfly",
466 target_os = "netbsd",
467 target_os = "openbsd"
468 ))]
469 {
470 assert_eq!(
471 packet.metadata.destination_local_ip,
472 Some(expected_destination)
473 );
474 assert!(packet.metadata.ingress_interface_index.is_some());
475 }
476
477 #[cfg(not(any(
478 target_os = "linux",
479 target_os = "android",
480 windows,
481 target_vendor = "apple",
482 target_os = "freebsd",
483 target_os = "dragonfly",
484 target_os = "netbsd",
485 target_os = "openbsd"
486 )))]
487 {
488 let _ = expected_destination;
489 assert_eq!(packet.metadata.destination_local_ip, None);
490 assert_eq!(packet.metadata.ingress_interface_index, None);
491 }
492 }
493
494 #[test]
495 fn try_recv_returns_none_when_no_packet_is_available() {
496 let config = sample_config_on_unused_port();
497 let socket = platform::open_bound_socket(&config).unwrap();
498 let mut subscription = Subscription::from_receive_socket(SubscriptionId(1), config, socket);
499 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
500 subscription.mark_joined().unwrap();
501
502 let result = subscription.try_recv().unwrap();
503
504 assert!(result.is_none());
505 }
506
507 #[test]
508 fn low_level_constructor_switches_socket_to_nonblocking() {
509 let config = sample_config_on_unused_port();
510 let socket = Socket::from(
511 UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, config.dst_port)).unwrap(),
512 );
513 let mut subscription = Subscription::new(SubscriptionId(1), config, socket);
514 subscription.mark_joined().unwrap();
515
516 let started = Instant::now();
517 assert!(subscription.try_recv().unwrap().is_none());
518 assert!(started.elapsed() < Duration::from_millis(100));
519 }
520
521 #[test]
522 fn try_recv_rejects_unicast_sent_to_the_bound_port() {
523 let config = sample_config_on_unused_port();
524 let socket = platform::open_bound_socket(&config).unwrap();
525 let mut subscription =
526 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
527 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
528 subscription.mark_joined().unwrap();
529
530 let sender = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap();
531 let payload = b"hello multicast core";
532
533 sender
534 .send_to(
535 payload,
536 SocketAddrV4::new(Ipv4Addr::LOCALHOST, config.dst_port),
537 )
538 .unwrap();
539
540 let deadline = Instant::now() + Duration::from_millis(200);
541 while Instant::now() < deadline {
542 assert!(subscription.try_recv().unwrap().is_none());
543 std::thread::sleep(Duration::from_millis(5));
544 }
545
546 let multicast_sender = make_multicast_sender();
547 multicast_sender
548 .send_to(payload, ipv4_group_socket_addr(&config))
549 .unwrap();
550
551 let packet =
552 recv_next_subscription_packet(&subscription, Instant::now() + Duration::from_secs(1));
553 assert_eq!(packet.group, config.group);
554 assert_eq!(&packet.payload[..], payload);
555 }
556
557 #[test]
558 fn try_recv_with_metadata_exposes_current_socket_context() {
559 let config = sample_config_on_unused_port();
560 let socket = platform::open_bound_socket(&config).unwrap();
561 let mut subscription =
562 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
563 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
564 subscription.mark_joined().unwrap();
565
566 let sender = make_multicast_sender();
567 let payload = b"hello detailed receive";
568
569 sender
570 .send_to(payload, ipv4_group_socket_addr(&config))
571 .unwrap();
572
573 let deadline = Instant::now() + Duration::from_secs(1);
574 let packet = loop {
575 match subscription.try_recv_with_metadata().unwrap() {
576 Some(packet) => break packet,
577 None if Instant::now() < deadline => {
578 std::thread::sleep(Duration::from_millis(10));
579 }
580 None => panic!("timed out waiting for packet with metadata"),
581 }
582 };
583
584 assert_eq!(packet.packet.subscription_id, SubscriptionId(1));
585 assert_eq!(packet.packet.group, IpAddr::V4(ipv4_group(&config)));
586 assert_eq!(packet.packet.dst_port, config.dst_port);
587 assert_eq!(&packet.packet.payload[..], payload);
588 assert_pktinfo_metadata(&packet, IpAddr::V4(ipv4_group(&config)));
589 assert_eq!(
590 packet.metadata.socket_local_addr,
591 Some(SocketAddr::V4(SocketAddrV4::new(
592 Ipv4Addr::UNSPECIFIED,
593 config.dst_port,
594 )))
595 );
596 assert_eq!(packet.metadata.configured_interface, config.interface);
597 }
598
599 #[test]
600 fn try_recv_with_metadata_exposes_current_ipv6_socket_context() {
601 if !ipv6_multicast_loopback_available() {
602 return;
603 }
604
605 let config = sample_config_v6_on_unused_port();
606 let socket = platform::open_bound_socket(&config).unwrap();
607 let mut subscription =
608 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
609 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
610 subscription.mark_joined().unwrap();
611
612 let sender = make_multicast_sender_v6(Ipv6Addr::LOCALHOST);
613 let payload = b"hello detailed receive ipv6";
614
615 sender
616 .send_to(payload, ipv6_group_socket_addr(&config))
617 .unwrap();
618
619 let deadline = Instant::now() + Duration::from_secs(1);
620 let packet = loop {
621 match subscription.try_recv_with_metadata().unwrap() {
622 Some(packet) => break packet,
623 None if Instant::now() < deadline => {
624 std::thread::sleep(Duration::from_millis(10));
625 }
626 None => panic!("timed out waiting for IPv6 packet with metadata"),
627 }
628 };
629
630 assert_eq!(packet.packet.subscription_id, SubscriptionId(1));
631 assert_eq!(packet.packet.group, config.group);
632 assert_eq!(packet.packet.dst_port, config.dst_port);
633 assert_eq!(&packet.packet.payload[..], payload);
634 assert_pktinfo_metadata(&packet, config.group);
635 assert_eq!(
636 packet.metadata.socket_local_addr,
637 Some(SocketAddr::V6(SocketAddrV6::new(
638 Ipv6Addr::UNSPECIFIED,
639 config.dst_port,
640 0,
641 0,
642 )))
643 );
644 assert_eq!(packet.metadata.configured_interface, config.interface);
645 assert_eq!(
646 packet.metadata.configured_interface_index,
647 config.interface_index
648 );
649 }
650
651 #[test]
652 fn try_recv_receives_multicast_packet_from_joined_group() {
653 let config = sample_config_on_unused_port();
654 let socket = platform::open_bound_socket(&config).unwrap();
655 let mut subscription =
656 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
657 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
658 subscription.mark_joined().unwrap();
659
660 let sender = make_multicast_sender();
661
662 let sender_port = sender.local_addr().unwrap().port();
663 let payload = b"hello real asm multicast";
664
665 sender
666 .send_to(payload, ipv4_group_socket_addr(&config))
667 .unwrap();
668
669 let deadline = Instant::now() + Duration::from_secs(1);
670 let packet = recv_next_subscription_packet(&subscription, deadline);
671
672 assert_eq!(packet.subscription_id, SubscriptionId(1));
673 assert_eq!(packet.group, IpAddr::V4(ipv4_group(&config)));
674 assert_eq!(packet.dst_port, config.dst_port);
675 assert_eq!(&packet.payload[..], payload);
676 assert_eq!(packet.source.port(), sender_port);
677 }
678
679 #[test]
680 fn try_recv_receives_ssm_packet_from_allowed_source() {
681 let interface = primary_ipv4();
682 let config = test_ssm_config(unused_udp_port_v4(), interface);
683 let socket = platform::open_bound_socket(&config).unwrap();
684 let mut subscription =
685 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
686 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
687 subscription.mark_joined().unwrap();
688
689 let sender = UdpSocket::bind(SocketAddrV4::new(interface, 0)).unwrap();
690 sender.set_multicast_loop_v4(true).unwrap();
691 sender.set_multicast_ttl_v4(1).unwrap();
692 SockRef::from(&sender)
693 .set_multicast_if_v4(&interface)
694 .unwrap();
695
696 let sender_port = sender.local_addr().unwrap().port();
697 let payload = b"hello real ssm multicast";
698
699 match sender.send_to(payload, ipv4_group_socket_addr(&config)) {
700 Ok(_) => {}
701 Err(err)
702 if matches!(
703 err.kind(),
704 ErrorKind::HostUnreachable | ErrorKind::NetworkUnreachable
705 ) =>
706 {
707 return;
709 }
710 Err(err) => panic!("failed to send IPv4 SSM test packet: {err}"),
711 }
712
713 let deadline = Instant::now() + Duration::from_secs(1);
714 let packet = recv_next_subscription_packet(&subscription, deadline);
715
716 assert_eq!(packet.subscription_id, SubscriptionId(1));
717 assert_eq!(packet.group, IpAddr::V4(ipv4_group(&config)));
718 assert_eq!(packet.dst_port, config.dst_port);
719 assert_eq!(&packet.payload[..], payload);
720 assert_eq!(packet.source.port(), sender_port);
721 assert_eq!(packet.source.ip(), IpAddr::V4(interface));
722 }
723
724 #[test]
725 fn try_recv_receives_ipv6_ssm_packet_from_allowed_source() {
726 let Some(config) = sample_ssm_receive_config_v6_on_unused_port() else {
727 return;
728 };
729 let interface = match config.source_addr().unwrap() {
730 IpAddr::V6(source) => source,
731 IpAddr::V4(_) => panic!("expected an IPv6 source for IPv6 SSM test"),
732 };
733 let socket = platform::open_bound_socket(&config).unwrap();
734 let mut subscription =
735 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
736 platform::join_multicast_group(subscription.socket(), subscription.config()).unwrap();
737 subscription.mark_joined().unwrap();
738
739 let sender = make_multicast_sender_v6_for_source(interface);
740 let sender_port = sender.local_addr().unwrap().port();
741 let payload = b"hello real ipv6 ssm multicast";
742
743 sender
744 .send_to(payload, ipv6_group_socket_addr(&config))
745 .unwrap();
746
747 let deadline = Instant::now() + Duration::from_secs(1);
748 let packet = recv_next_subscription_packet(&subscription, deadline);
749
750 assert_eq!(packet.subscription_id, SubscriptionId(1));
751 assert_eq!(packet.group, config.group);
752 assert_eq!(packet.dst_port, config.dst_port);
753 assert_eq!(&packet.payload[..], payload);
754 assert_eq!(packet.source.port(), sender_port);
755 assert_eq!(packet.source.ip(), IpAddr::V6(interface));
756 }
757
758 #[test]
759 fn mark_joined_transitions_bound_to_joined_state() {
760 let config = sample_config_on_unused_port();
761 let socket = platform::open_bound_socket(&config).unwrap();
762 let mut subscription = Subscription::from_receive_socket(SubscriptionId(1), config, socket);
763
764 subscription.mark_joined().unwrap();
765
766 assert_eq!(subscription.state(), SubscriptionState::Joined);
767 }
768
769 #[test]
770 fn mark_joined_rejects_already_joined_subscription() {
771 let config = sample_config_on_unused_port();
772 let socket = platform::open_bound_socket(&config).unwrap();
773 let mut subscription = Subscription::from_receive_socket(SubscriptionId(1), config, socket);
774
775 subscription.mark_joined().unwrap();
776 let result = subscription.mark_joined();
777
778 assert!(matches!(result, Err(McrxError::SubscriptionAlreadyJoined)));
779 }
780
781 #[test]
782 fn mark_bound_transitions_joined_to_bound_state() {
783 let config = sample_config_on_unused_port();
784 let socket = platform::open_bound_socket(&config).unwrap();
785 let mut subscription = Subscription::from_receive_socket(SubscriptionId(1), config, socket);
786
787 subscription.mark_joined().unwrap();
788 subscription.mark_bound().unwrap();
789
790 assert_eq!(subscription.state(), SubscriptionState::Bound);
791 }
792
793 #[test]
794 fn mark_bound_rejects_already_bound_subscription() {
795 let config = sample_config_on_unused_port();
796 let socket = platform::open_bound_socket(&config).unwrap();
797 let mut subscription = Subscription::from_receive_socket(SubscriptionId(1), config, socket);
798
799 let result = subscription.mark_bound();
800
801 assert!(matches!(result, Err(McrxError::SubscriptionNotJoined)));
802 }
803
804 #[test]
805 fn local_addr_returns_bound_socket_address() {
806 let config = sample_config_on_unused_port();
807 let socket = platform::open_bound_socket(&config).unwrap();
808 let subscription =
809 Subscription::from_receive_socket(SubscriptionId(1), config.clone(), socket);
810
811 let local_addr = subscription.local_addr().unwrap();
812
813 assert_eq!(
814 local_addr,
815 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, config.dst_port))
816 );
817 }
818}