1use std::collections::HashMap;
27use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
28use std::time::Instant;
29
30use std::sync::Arc;
31
32use socket2::SockRef;
33use tokio::net::{TcpListener, UdpSocket};
34use tokio::sync::mpsc;
35use tokio::task::JoinHandle;
36use tokio_util::sync::CancellationToken;
37
38use arcbox_packet::ethernet::{ETH_HEADER_LEN, build_udp_ip_ethernet};
39
40const INBOUND_TCP_BUF_SIZE: usize = 4 * 1024 * 1024;
50
51const EPHEMERAL_START: u16 = 61000;
57const EPHEMERAL_END: u16 = 65535;
59
60pub(crate) struct EphemeralPorts {
62 next: u16,
63}
64
65impl EphemeralPorts {
66 pub(crate) fn new() -> Self {
67 Self {
68 next: EPHEMERAL_START,
69 }
70 }
71
72 pub(crate) fn allocate(&mut self) -> u16 {
74 let port = self.next;
75 self.next = if self.next == EPHEMERAL_END {
76 EPHEMERAL_START
77 } else {
78 self.next + 1
79 };
80 port
81 }
82
83 #[inline]
85 pub(crate) fn in_range(port: u16) -> bool {
86 (EPHEMERAL_START..=EPHEMERAL_END).contains(&port)
87 }
88}
89
90pub enum InboundCommand {
96 TcpAccepted {
98 host_port: u16,
99 container_port: u16,
100 stream: tokio::net::TcpStream,
101 },
102 UdpReceived {
104 host_port: u16,
105 container_port: u16,
106 data: Vec<u8>,
107 reply_tx: mpsc::Sender<Vec<u8>>,
109 client_addr: SocketAddr,
110 },
111}
112
113struct InboundUdpFlow {
119 client_tx: mpsc::Sender<Vec<u8>>,
121 last_active: Instant,
123}
124
125pub(crate) struct InboundRelay {
132 udp_flows: HashMap<(Ipv4Addr, u16, Ipv4Addr, u16), InboundUdpFlow>,
134 reply_tx: mpsc::Sender<Vec<u8>>,
136 gateway_mac: [u8; 6],
137 gateway_ip: Ipv4Addr,
138 guest_ip: Ipv4Addr,
139 mtu: usize,
141 ephemeral_ports: EphemeralPorts,
142}
143
144impl InboundRelay {
145 pub(crate) fn new(
146 reply_tx: mpsc::Sender<Vec<u8>>,
147 gateway_mac: [u8; 6],
148 gateway_ip: Ipv4Addr,
149 guest_ip: Ipv4Addr,
150 mtu: usize,
151 ) -> Self {
152 Self {
153 udp_flows: HashMap::new(),
154 reply_tx,
155 gateway_mac,
156 gateway_ip,
157 guest_ip,
158 mtu,
159 ephemeral_ports: EphemeralPorts::new(),
160 }
161 }
162
163 pub(crate) fn try_handle_reply(&mut self, frame: &[u8], _guest_mac: [u8; 6]) -> bool {
173 if frame.len() < ETH_HEADER_LEN + 20 {
174 return false;
175 }
176
177 let ip_start = ETH_HEADER_LEN;
178 let protocol = frame[ip_start + 9];
179
180 let ihl = ((frame[ip_start] & 0x0F) as usize) * 4;
181 let l4_start = ip_start + ihl;
182
183 match protocol {
184 6 => false, 17 => self.try_handle_udp_reply(frame, ip_start, l4_start),
186 _ => false,
187 }
188 }
189
190 fn try_handle_udp_reply(&mut self, frame: &[u8], ip_start: usize, udp_start: usize) -> bool {
192 if frame.len() < udp_start + 8 {
193 return false;
194 }
195
196 let dst_port = u16::from_be_bytes([frame[udp_start + 2], frame[udp_start + 3]]);
197 if !EphemeralPorts::in_range(dst_port) {
198 return false;
199 }
200
201 let src_ip = Ipv4Addr::new(
202 frame[ip_start + 12],
203 frame[ip_start + 13],
204 frame[ip_start + 14],
205 frame[ip_start + 15],
206 );
207 let dst_ip = Ipv4Addr::new(
208 frame[ip_start + 16],
209 frame[ip_start + 17],
210 frame[ip_start + 18],
211 frame[ip_start + 19],
212 );
213 let src_port = u16::from_be_bytes([frame[udp_start], frame[udp_start + 1]]);
214
215 let key = (dst_ip, dst_port, src_ip, src_port);
216
217 if let Some(flow) = self.udp_flows.get_mut(&key) {
218 let udp_len = u16::from_be_bytes([frame[udp_start + 4], frame[udp_start + 5]]) as usize;
219 if udp_len >= 8 && udp_start + udp_len <= frame.len() {
220 let payload = frame[udp_start + 8..udp_start + udp_len].to_vec();
221 flow.last_active = Instant::now();
222 let _ = flow.client_tx.try_send(payload);
223 }
224 return true;
225 }
226
227 false
228 }
229
230 pub(crate) fn inject_udp(
236 &mut self,
237 container_port: u16,
238 data: &[u8],
239 client_tx: mpsc::Sender<Vec<u8>>,
240 guest_mac: [u8; 6],
241 ) {
242 let ephemeral_port = self.ephemeral_ports.allocate();
243 let key = (
244 self.gateway_ip,
245 ephemeral_port,
246 self.guest_ip,
247 container_port,
248 );
249
250 self.udp_flows.insert(
251 key,
252 InboundUdpFlow {
253 client_tx,
254 last_active: Instant::now(),
255 },
256 );
257
258 let frames = build_udp_ip_ethernet(
259 self.gateway_ip,
260 self.guest_ip,
261 ephemeral_port,
262 container_port,
263 data,
264 self.gateway_mac,
265 guest_mac,
266 self.mtu,
267 );
268
269 for frame in frames {
270 if self.reply_tx.try_send(frame).is_err() {
271 break;
273 }
274 }
275
276 tracing::debug!(
277 "Inbound UDP: injected {} bytes gw:{} → guest:{}",
278 data.len(),
279 ephemeral_port,
280 container_port,
281 );
282 }
283
284 pub(crate) fn cleanup(&mut self) {
290 let now = Instant::now();
291 self.udp_flows
292 .retain(|_, flow| now.duration_since(flow.last_active).as_secs() < 60);
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302pub enum InboundProtocol {
303 Tcp,
304 Udp,
305}
306
307type ListenerKey = (Ipv4Addr, u16, InboundProtocol);
312
313type ListenerEntry = (JoinHandle<()>, CancellationToken, u16);
316
317pub struct InboundListenerManager {
320 cmd_tx: mpsc::Sender<InboundCommand>,
321 listeners: HashMap<ListenerKey, ListenerEntry>,
322}
323
324impl InboundListenerManager {
325 #[must_use]
327 pub fn new(cmd_tx: mpsc::Sender<InboundCommand>) -> Self {
328 Self {
329 cmd_tx,
330 listeners: HashMap::new(),
331 }
332 }
333
334 pub async fn add_rule(
345 &mut self,
346 host_ip: Ipv4Addr,
347 host_port: u16,
348 container_port: u16,
349 protocol: InboundProtocol,
350 ) -> std::io::Result<u16> {
351 let key = (host_ip, host_port, protocol);
352 if self.listeners.contains_key(&key) {
353 return Err(std::io::Error::new(
354 std::io::ErrorKind::AddrInUse,
355 format!("inbound listener already exists on {host_ip}:{host_port}"),
356 ));
357 }
358
359 let cancel = CancellationToken::new();
360 let cmd_tx = self.cmd_tx.clone();
361
362 let (handle, bound_port) = match protocol {
363 InboundProtocol::Tcp => {
364 let listener =
365 TcpListener::bind(SocketAddr::V4(SocketAddrV4::new(host_ip, host_port)))
366 .await?;
367 let bound = listener.local_addr()?.port();
368 tracing::info!(
369 "Inbound listener: TCP {}:{} → container :{}",
370 host_ip,
371 bound,
372 container_port,
373 );
374 let cancel_clone = cancel.clone();
375 let handle = tokio::spawn(async move {
376 tcp_listener_task(listener, container_port, cmd_tx, cancel_clone).await;
377 });
378 (handle, bound)
379 }
380 InboundProtocol::Udp => {
381 let socket =
382 UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new(host_ip, host_port))).await?;
383 let bound = socket.local_addr()?.port();
384 tracing::info!(
385 "Inbound listener: UDP {}:{} → container :{}",
386 host_ip,
387 bound,
388 container_port,
389 );
390 let cancel_clone = cancel.clone();
391 let handle = tokio::spawn(async move {
392 udp_listener_task(socket, container_port, cmd_tx, cancel_clone).await;
393 });
394 (handle, bound)
395 }
396 };
397
398 self.listeners.insert(key, (handle, cancel, bound_port));
399 Ok(bound_port)
400 }
401
402 pub async fn remove_rule(
405 &mut self,
406 host_ip: Ipv4Addr,
407 host_port: u16,
408 protocol: InboundProtocol,
409 ) {
410 let key = (host_ip, host_port, protocol);
411 if let Some((handle, cancel, bound)) = self.listeners.remove(&key) {
412 cancel.cancel();
413 handle.abort();
414 let _ = handle.await;
415 tracing::debug!(
416 "Inbound listener removed: {:?} {}:{}",
417 protocol,
418 host_ip,
419 bound
420 );
421 }
422 }
423
424 pub async fn stop_all(&mut self) {
426 let keys: Vec<_> = self.listeners.keys().copied().collect();
427 for (ip, port, protocol) in keys {
428 self.remove_rule(ip, port, protocol).await;
429 }
430 }
431
432 #[must_use]
434 pub fn len(&self) -> usize {
435 self.listeners.len()
436 }
437
438 #[must_use]
440 pub fn is_empty(&self) -> bool {
441 self.listeners.is_empty()
442 }
443}
444
445async fn tcp_listener_task(
451 listener: TcpListener,
452 container_port: u16,
453 cmd_tx: mpsc::Sender<InboundCommand>,
454 cancel: CancellationToken,
455) {
456 let host_port = listener.local_addr().map_or(0, |a| a.port());
457 loop {
458 tokio::select! {
459 biased;
460 () = cancel.cancelled() => break,
461 result = listener.accept() => {
462 match result {
463 Ok((stream, peer)) => {
464 tracing::debug!(
465 "Inbound TCP accept: {} → host:{} → container:{}",
466 peer, host_port, container_port,
467 );
468 let sock = SockRef::from(&stream);
472 if let Err(e) = sock.set_recv_buffer_size(INBOUND_TCP_BUF_SIZE) {
473 tracing::warn!("Failed to set SO_RCVBUF on inbound stream: {e}");
474 }
475 if let Err(e) = sock.set_send_buffer_size(INBOUND_TCP_BUF_SIZE) {
476 tracing::warn!("Failed to set SO_SNDBUF on inbound stream: {e}");
477 }
478 let cmd = InboundCommand::TcpAccepted {
479 host_port,
480 container_port,
481 stream,
482 };
483 if cmd_tx.send(cmd).await.is_err() {
484 break;
485 }
486 }
487 Err(e) => {
488 tracing::warn!("Inbound TCP accept error on :{}: {}", host_port, e);
489 }
490 }
491 }
492 }
493 }
494}
495
496async fn udp_listener_task(
498 socket: UdpSocket,
499 container_port: u16,
500 cmd_tx: mpsc::Sender<InboundCommand>,
501 cancel: CancellationToken,
502) {
503 let host_port = socket.local_addr().map_or(0, |a| a.port());
504 let socket = Arc::new(socket);
505 let mut reply_flows: HashMap<SocketAddr, mpsc::Sender<Vec<u8>>> = HashMap::new();
506 let mut buf = vec![0u8; 65535];
507
508 loop {
509 tokio::select! {
510 biased;
511 () = cancel.cancelled() => break,
512 result = socket.recv_from(&mut buf) => {
513 match result {
514 Ok((n, client_addr)) => {
515 let reply_tx = if let Some(tx) = reply_flows.get(&client_addr) {
516 if tx.is_closed() {
517 reply_flows.remove(&client_addr);
518 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
519 } else {
520 tx.clone()
521 }
522 } else {
523 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
524 };
525
526 let cmd = InboundCommand::UdpReceived {
527 host_port,
528 container_port,
529 data: buf[..n].to_vec(),
530 reply_tx,
531 client_addr,
532 };
533 if cmd_tx.send(cmd).await.is_err() {
534 break;
535 }
536 }
537 Err(e) => {
538 tracing::warn!("Inbound UDP recv error on :{}: {}", host_port, e);
539 }
540 }
541 }
542 }
543 }
544}
545
546fn create_udp_reply_flow(
547 client_addr: SocketAddr,
548 socket: &Arc<UdpSocket>,
549 cancel: &CancellationToken,
550 reply_flows: &mut HashMap<SocketAddr, mpsc::Sender<Vec<u8>>>,
551) -> mpsc::Sender<Vec<u8>> {
552 let (reply_tx, mut reply_rx) = mpsc::channel::<Vec<u8>>(16);
553 let reply_sock = Arc::clone(socket);
554 let flow_cancel = cancel.clone();
555 tokio::spawn(async move {
556 loop {
557 tokio::select! {
558 biased;
559 () = flow_cancel.cancelled() => break,
560 maybe_data = reply_rx.recv() => {
561 let Some(data) = maybe_data else {
562 break;
563 };
564 let _ = reply_sock.send_to(&data, client_addr).await;
565 }
566 }
567 }
568 });
569 reply_flows.insert(client_addr, reply_tx.clone());
570 reply_tx
571}
572
573#[cfg(test)]
578mod tests {
579 use std::time::Duration;
580
581 use super::*;
582
583 const GW_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 1);
584 const GUEST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 2);
585 const GW_MAC: [u8; 6] = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
586 const GUEST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x99];
587
588 #[test]
589 fn ephemeral_ports_allocation() {
590 let mut ep = EphemeralPorts::new();
591 assert_eq!(ep.allocate(), 61000);
592 assert_eq!(ep.allocate(), 61001);
593 }
594
595 #[test]
596 fn ephemeral_ports_wrap_around() {
597 let mut ep = EphemeralPorts::new();
598 ep.next = EPHEMERAL_END;
599 assert_eq!(ep.allocate(), EPHEMERAL_END);
600 assert_eq!(ep.allocate(), EPHEMERAL_START);
601 }
602
603 #[test]
604 fn ephemeral_ports_in_range() {
605 assert!(EphemeralPorts::in_range(61000));
606 assert!(EphemeralPorts::in_range(65535));
607 assert!(EphemeralPorts::in_range(63000));
608 assert!(!EphemeralPorts::in_range(60999));
609 assert!(!EphemeralPorts::in_range(32768));
610 assert!(!EphemeralPorts::in_range(80));
611 }
612
613 #[test]
614 fn inbound_relay_rejects_non_ephemeral() {
615 let (tx, _rx) = mpsc::channel(16);
616 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
617
618 let mut frame = vec![0u8; ETH_HEADER_LEN + 40];
620 frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
621 let ip = &mut frame[ETH_HEADER_LEN..];
622 ip[0] = 0x45;
623 ip[9] = 6; ip[12..16].copy_from_slice(&GUEST_IP.octets());
625 ip[16..20].copy_from_slice(&GW_IP.octets());
626 let tcp = &mut frame[ETH_HEADER_LEN + 20..];
628 tcp[0..2].copy_from_slice(&8080u16.to_be_bytes());
629 tcp[2..4].copy_from_slice(&80u16.to_be_bytes());
630 tcp[12] = 0x50; assert!(!relay.try_handle_reply(&frame, GUEST_MAC));
633 }
634
635 #[tokio::test]
636 async fn inject_udp_sends_frame_and_tracks_flow() {
637 let (tx, mut rx) = mpsc::channel(16);
638 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
639
640 let (client_tx, _client_rx) = mpsc::channel(16);
641 relay.inject_udp(53, b"dns query", client_tx, GUEST_MAC);
642
643 let key = (GW_IP, EPHEMERAL_START, GUEST_IP, 53);
645 assert!(relay.udp_flows.contains_key(&key));
646
647 let frame = rx.recv().await.expect("should receive UDP frame");
649 assert!(frame.len() >= ETH_HEADER_LEN + 28, "UDP frame too short");
650
651 assert_eq!(frame[ETH_HEADER_LEN + 9], 17);
653
654 let udp_start = ETH_HEADER_LEN + 20;
656 let src_port = u16::from_be_bytes([frame[udp_start], frame[udp_start + 1]]);
657 let dst_port = u16::from_be_bytes([frame[udp_start + 2], frame[udp_start + 3]]);
658 assert_eq!(src_port, EPHEMERAL_START);
659 assert_eq!(dst_port, 53);
660 }
661
662 #[test]
663 fn cleanup_removes_expired_udp_flows() {
664 let (tx, _rx) = mpsc::channel(16);
665 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
666
667 let (client_tx, _client_rx) = mpsc::channel(16);
668 let key = (GW_IP, 61000, GUEST_IP, 53);
669 relay.udp_flows.insert(
670 key,
671 InboundUdpFlow {
672 client_tx,
673 last_active: Instant::now()
674 .checked_sub(std::time::Duration::from_secs(120))
675 .unwrap(),
676 },
677 );
678 assert_eq!(relay.udp_flows.len(), 1);
679
680 relay.cleanup();
681 assert_eq!(
682 relay.udp_flows.len(),
683 0,
684 "expired UDP flow should be removed"
685 );
686 }
687
688 #[tokio::test]
689 async fn listener_manager_add_and_remove_rule() {
690 let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
691 let mut manager = InboundListenerManager::new(cmd_tx);
692
693 manager
695 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
696 .await
697 .expect("should bind to port 0 (OS-assigned)");
698
699 manager
701 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
702 .await;
703
704 assert!(cmd_rx.try_recv().is_err(), "no commands expected yet");
706 }
707
708 #[tokio::test]
720 async fn host_connection_produces_a_tcp_accepted_command() {
721 let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
722 let mut manager = InboundListenerManager::new(cmd_tx);
723
724 let host_port = manager
725 .add_rule(Ipv4Addr::LOCALHOST, 0, 8080, InboundProtocol::Tcp)
726 .await
727 .expect("rule should bind an OS-assigned port");
728 assert_ne!(host_port, 0, "add_rule must report the port it bound");
729
730 let _client = tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port))
731 .await
732 .expect("host should be able to connect to a registered rule");
733
734 let cmd = tokio::time::timeout(Duration::from_secs(5), cmd_rx.recv())
735 .await
736 .expect("a TcpAccepted command should arrive within 5s")
737 .expect("command channel stayed open");
738
739 match cmd {
740 InboundCommand::TcpAccepted {
741 host_port: got_host,
742 container_port,
743 ..
744 } => {
745 assert_eq!(got_host, host_port, "command reports the wrong host port");
746 assert_eq!(
747 container_port, 8080,
748 "command must carry the container port the rule was created with"
749 );
750 }
751 InboundCommand::UdpReceived { .. } => {
755 panic!("a TCP rule produced UdpReceived instead of TcpAccepted")
756 }
757 }
758 }
759
760 #[tokio::test]
763 async fn removing_a_rule_closes_the_listener() {
764 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
765 let mut manager = InboundListenerManager::new(cmd_tx);
766
767 let host_port = manager
768 .add_rule(Ipv4Addr::LOCALHOST, 0, 8080, InboundProtocol::Tcp)
769 .await
770 .expect("rule should bind an OS-assigned port");
771 tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port))
772 .await
773 .expect("connect should succeed while the rule exists");
774
775 manager
778 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
779 .await;
780
781 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
783 loop {
784 match tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port)).await {
785 Err(_) => break,
786 Ok(_) if tokio::time::Instant::now() >= deadline => {
787 panic!("port {host_port} still accepts connections after remove_rule")
788 }
789 Ok(_) => tokio::time::sleep(Duration::from_millis(50)).await,
790 }
791 }
792 }
793
794 #[tokio::test]
795 async fn listener_manager_stop_all() {
796 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
797 let mut manager = InboundListenerManager::new(cmd_tx);
798
799 manager
800 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
801 .await
802 .unwrap();
803 manager
804 .add_rule(Ipv4Addr::LOCALHOST, 0, 53, InboundProtocol::Udp)
805 .await
806 .unwrap();
807
808 manager.stop_all().await;
809 manager
813 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
814 .await
815 .unwrap();
816 }
817
818 #[tokio::test]
819 async fn listener_manager_rejects_duplicate_host_endpoint() {
820 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
821 let mut manager = InboundListenerManager::new(cmd_tx);
822 manager
823 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
824 .await
825 .unwrap();
826
827 let error = manager
828 .add_rule(Ipv4Addr::LOCALHOST, 0, 81, InboundProtocol::Tcp)
829 .await
830 .expect_err("the existing listener must not be reused for another destination");
831
832 assert_eq!(error.kind(), std::io::ErrorKind::AddrInUse);
833 assert_eq!(manager.listeners.len(), 1);
834 }
835
836 #[tokio::test]
837 async fn listener_remove_waits_until_socket_is_reusable() {
838 let reservation = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
839 let port = reservation.local_addr().unwrap().port();
840 drop(reservation);
841
842 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
843 let mut manager = InboundListenerManager::new(cmd_tx);
844 manager
845 .add_rule(Ipv4Addr::LOCALHOST, port, 80, InboundProtocol::Tcp)
846 .await
847 .unwrap();
848 manager
849 .remove_rule(Ipv4Addr::LOCALHOST, port, InboundProtocol::Tcp)
850 .await;
851
852 std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, port))
853 .expect("remove_rule must release the socket before returning");
854 }
855
856 #[tokio::test]
857 async fn same_port_different_ip_coexist() {
858 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
859 let mut manager = InboundListenerManager::new(cmd_tx);
860
861 manager
863 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
864 .await
865 .unwrap();
866 manager
867 .add_rule(Ipv4Addr::UNSPECIFIED, 0, 80, InboundProtocol::Tcp)
868 .await
869 .unwrap();
870
871 manager
873 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
874 .await;
875 manager
876 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
877 .await
878 .unwrap();
879 }
880
881 #[test]
882 fn invalid_host_ip_is_rejected() {
883 assert!(
886 "::1".parse::<Ipv4Addr>().is_err(),
887 "IPv6 should fail Ipv4Addr parse"
888 );
889 assert!("not-an-ip".parse::<Ipv4Addr>().is_err());
890 assert!("".parse::<Ipv4Addr>().is_err());
891 assert!("127.0.0.1".parse::<Ipv4Addr>().is_ok());
893 assert!("0.0.0.0".parse::<Ipv4Addr>().is_ok());
894 }
895}