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
433async fn tcp_listener_task(
439 listener: TcpListener,
440 container_port: u16,
441 cmd_tx: mpsc::Sender<InboundCommand>,
442 cancel: CancellationToken,
443) {
444 let host_port = listener.local_addr().map_or(0, |a| a.port());
445 loop {
446 tokio::select! {
447 biased;
448 () = cancel.cancelled() => break,
449 result = listener.accept() => {
450 match result {
451 Ok((stream, peer)) => {
452 tracing::debug!(
453 "Inbound TCP accept: {} → host:{} → container:{}",
454 peer, host_port, container_port,
455 );
456 let sock = SockRef::from(&stream);
460 if let Err(e) = sock.set_recv_buffer_size(INBOUND_TCP_BUF_SIZE) {
461 tracing::warn!("Failed to set SO_RCVBUF on inbound stream: {e}");
462 }
463 if let Err(e) = sock.set_send_buffer_size(INBOUND_TCP_BUF_SIZE) {
464 tracing::warn!("Failed to set SO_SNDBUF on inbound stream: {e}");
465 }
466 let cmd = InboundCommand::TcpAccepted {
467 host_port,
468 container_port,
469 stream,
470 };
471 if cmd_tx.send(cmd).await.is_err() {
472 break;
473 }
474 }
475 Err(e) => {
476 tracing::warn!("Inbound TCP accept error on :{}: {}", host_port, e);
477 }
478 }
479 }
480 }
481 }
482}
483
484async fn udp_listener_task(
486 socket: UdpSocket,
487 container_port: u16,
488 cmd_tx: mpsc::Sender<InboundCommand>,
489 cancel: CancellationToken,
490) {
491 let host_port = socket.local_addr().map_or(0, |a| a.port());
492 let socket = Arc::new(socket);
493 let mut reply_flows: HashMap<SocketAddr, mpsc::Sender<Vec<u8>>> = HashMap::new();
494 let mut buf = vec![0u8; 65535];
495
496 loop {
497 tokio::select! {
498 biased;
499 () = cancel.cancelled() => break,
500 result = socket.recv_from(&mut buf) => {
501 match result {
502 Ok((n, client_addr)) => {
503 let reply_tx = if let Some(tx) = reply_flows.get(&client_addr) {
504 if tx.is_closed() {
505 reply_flows.remove(&client_addr);
506 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
507 } else {
508 tx.clone()
509 }
510 } else {
511 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
512 };
513
514 let cmd = InboundCommand::UdpReceived {
515 host_port,
516 container_port,
517 data: buf[..n].to_vec(),
518 reply_tx,
519 client_addr,
520 };
521 if cmd_tx.send(cmd).await.is_err() {
522 break;
523 }
524 }
525 Err(e) => {
526 tracing::warn!("Inbound UDP recv error on :{}: {}", host_port, e);
527 }
528 }
529 }
530 }
531 }
532}
533
534fn create_udp_reply_flow(
535 client_addr: SocketAddr,
536 socket: &Arc<UdpSocket>,
537 cancel: &CancellationToken,
538 reply_flows: &mut HashMap<SocketAddr, mpsc::Sender<Vec<u8>>>,
539) -> mpsc::Sender<Vec<u8>> {
540 let (reply_tx, mut reply_rx) = mpsc::channel::<Vec<u8>>(16);
541 let reply_sock = Arc::clone(socket);
542 let flow_cancel = cancel.clone();
543 tokio::spawn(async move {
544 loop {
545 tokio::select! {
546 biased;
547 () = flow_cancel.cancelled() => break,
548 maybe_data = reply_rx.recv() => {
549 let Some(data) = maybe_data else {
550 break;
551 };
552 let _ = reply_sock.send_to(&data, client_addr).await;
553 }
554 }
555 }
556 });
557 reply_flows.insert(client_addr, reply_tx.clone());
558 reply_tx
559}
560
561#[cfg(test)]
566mod tests {
567 use std::time::Duration;
568
569 use super::*;
570
571 const GW_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 1);
572 const GUEST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 2);
573 const GW_MAC: [u8; 6] = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
574 const GUEST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x99];
575
576 #[test]
577 fn ephemeral_ports_allocation() {
578 let mut ep = EphemeralPorts::new();
579 assert_eq!(ep.allocate(), 61000);
580 assert_eq!(ep.allocate(), 61001);
581 }
582
583 #[test]
584 fn ephemeral_ports_wrap_around() {
585 let mut ep = EphemeralPorts::new();
586 ep.next = EPHEMERAL_END;
587 assert_eq!(ep.allocate(), EPHEMERAL_END);
588 assert_eq!(ep.allocate(), EPHEMERAL_START);
589 }
590
591 #[test]
592 fn ephemeral_ports_in_range() {
593 assert!(EphemeralPorts::in_range(61000));
594 assert!(EphemeralPorts::in_range(65535));
595 assert!(EphemeralPorts::in_range(63000));
596 assert!(!EphemeralPorts::in_range(60999));
597 assert!(!EphemeralPorts::in_range(32768));
598 assert!(!EphemeralPorts::in_range(80));
599 }
600
601 #[test]
602 fn inbound_relay_rejects_non_ephemeral() {
603 let (tx, _rx) = mpsc::channel(16);
604 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
605
606 let mut frame = vec![0u8; ETH_HEADER_LEN + 40];
608 frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
609 let ip = &mut frame[ETH_HEADER_LEN..];
610 ip[0] = 0x45;
611 ip[9] = 6; ip[12..16].copy_from_slice(&GUEST_IP.octets());
613 ip[16..20].copy_from_slice(&GW_IP.octets());
614 let tcp = &mut frame[ETH_HEADER_LEN + 20..];
616 tcp[0..2].copy_from_slice(&8080u16.to_be_bytes());
617 tcp[2..4].copy_from_slice(&80u16.to_be_bytes());
618 tcp[12] = 0x50; assert!(!relay.try_handle_reply(&frame, GUEST_MAC));
621 }
622
623 #[tokio::test]
624 async fn inject_udp_sends_frame_and_tracks_flow() {
625 let (tx, mut rx) = mpsc::channel(16);
626 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
627
628 let (client_tx, _client_rx) = mpsc::channel(16);
629 relay.inject_udp(53, b"dns query", client_tx, GUEST_MAC);
630
631 let key = (GW_IP, EPHEMERAL_START, GUEST_IP, 53);
633 assert!(relay.udp_flows.contains_key(&key));
634
635 let frame = rx.recv().await.expect("should receive UDP frame");
637 assert!(frame.len() >= ETH_HEADER_LEN + 28, "UDP frame too short");
638
639 assert_eq!(frame[ETH_HEADER_LEN + 9], 17);
641
642 let udp_start = ETH_HEADER_LEN + 20;
644 let src_port = u16::from_be_bytes([frame[udp_start], frame[udp_start + 1]]);
645 let dst_port = u16::from_be_bytes([frame[udp_start + 2], frame[udp_start + 3]]);
646 assert_eq!(src_port, EPHEMERAL_START);
647 assert_eq!(dst_port, 53);
648 }
649
650 #[test]
651 fn cleanup_removes_expired_udp_flows() {
652 let (tx, _rx) = mpsc::channel(16);
653 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
654
655 let (client_tx, _client_rx) = mpsc::channel(16);
656 let key = (GW_IP, 61000, GUEST_IP, 53);
657 relay.udp_flows.insert(
658 key,
659 InboundUdpFlow {
660 client_tx,
661 last_active: Instant::now()
662 .checked_sub(std::time::Duration::from_secs(120))
663 .unwrap(),
664 },
665 );
666 assert_eq!(relay.udp_flows.len(), 1);
667
668 relay.cleanup();
669 assert_eq!(
670 relay.udp_flows.len(),
671 0,
672 "expired UDP flow should be removed"
673 );
674 }
675
676 #[tokio::test]
677 async fn listener_manager_add_and_remove_rule() {
678 let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
679 let mut manager = InboundListenerManager::new(cmd_tx);
680
681 manager
683 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
684 .await
685 .expect("should bind to port 0 (OS-assigned)");
686
687 manager
689 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
690 .await;
691
692 assert!(cmd_rx.try_recv().is_err(), "no commands expected yet");
694 }
695
696 #[tokio::test]
708 async fn host_connection_produces_a_tcp_accepted_command() {
709 let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
710 let mut manager = InboundListenerManager::new(cmd_tx);
711
712 let host_port = manager
713 .add_rule(Ipv4Addr::LOCALHOST, 0, 8080, InboundProtocol::Tcp)
714 .await
715 .expect("rule should bind an OS-assigned port");
716 assert_ne!(host_port, 0, "add_rule must report the port it bound");
717
718 let _client = tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port))
719 .await
720 .expect("host should be able to connect to a registered rule");
721
722 let cmd = tokio::time::timeout(Duration::from_secs(5), cmd_rx.recv())
723 .await
724 .expect("a TcpAccepted command should arrive within 5s")
725 .expect("command channel stayed open");
726
727 match cmd {
728 InboundCommand::TcpAccepted {
729 host_port: got_host,
730 container_port,
731 ..
732 } => {
733 assert_eq!(got_host, host_port, "command reports the wrong host port");
734 assert_eq!(
735 container_port, 8080,
736 "command must carry the container port the rule was created with"
737 );
738 }
739 InboundCommand::UdpReceived { .. } => {
743 panic!("a TCP rule produced UdpReceived instead of TcpAccepted")
744 }
745 }
746 }
747
748 #[tokio::test]
751 async fn removing_a_rule_closes_the_listener() {
752 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
753 let mut manager = InboundListenerManager::new(cmd_tx);
754
755 let host_port = manager
756 .add_rule(Ipv4Addr::LOCALHOST, 0, 8080, InboundProtocol::Tcp)
757 .await
758 .expect("rule should bind an OS-assigned port");
759 tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port))
760 .await
761 .expect("connect should succeed while the rule exists");
762
763 manager
766 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
767 .await;
768
769 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
771 loop {
772 match tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, host_port)).await {
773 Err(_) => break,
774 Ok(_) if tokio::time::Instant::now() >= deadline => {
775 panic!("port {host_port} still accepts connections after remove_rule")
776 }
777 Ok(_) => tokio::time::sleep(Duration::from_millis(50)).await,
778 }
779 }
780 }
781
782 #[tokio::test]
783 async fn listener_manager_stop_all() {
784 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
785 let mut manager = InboundListenerManager::new(cmd_tx);
786
787 manager
788 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
789 .await
790 .unwrap();
791 manager
792 .add_rule(Ipv4Addr::LOCALHOST, 0, 53, InboundProtocol::Udp)
793 .await
794 .unwrap();
795
796 manager.stop_all().await;
797 manager
801 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
802 .await
803 .unwrap();
804 }
805
806 #[tokio::test]
807 async fn listener_manager_rejects_duplicate_host_endpoint() {
808 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
809 let mut manager = InboundListenerManager::new(cmd_tx);
810 manager
811 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
812 .await
813 .unwrap();
814
815 let error = manager
816 .add_rule(Ipv4Addr::LOCALHOST, 0, 81, InboundProtocol::Tcp)
817 .await
818 .expect_err("the existing listener must not be reused for another destination");
819
820 assert_eq!(error.kind(), std::io::ErrorKind::AddrInUse);
821 assert_eq!(manager.listeners.len(), 1);
822 }
823
824 #[tokio::test]
825 async fn listener_remove_waits_until_socket_is_reusable() {
826 let reservation = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
827 let port = reservation.local_addr().unwrap().port();
828 drop(reservation);
829
830 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
831 let mut manager = InboundListenerManager::new(cmd_tx);
832 manager
833 .add_rule(Ipv4Addr::LOCALHOST, port, 80, InboundProtocol::Tcp)
834 .await
835 .unwrap();
836 manager
837 .remove_rule(Ipv4Addr::LOCALHOST, port, InboundProtocol::Tcp)
838 .await;
839
840 std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, port))
841 .expect("remove_rule must release the socket before returning");
842 }
843
844 #[tokio::test]
845 async fn same_port_different_ip_coexist() {
846 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
847 let mut manager = InboundListenerManager::new(cmd_tx);
848
849 manager
851 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
852 .await
853 .unwrap();
854 manager
855 .add_rule(Ipv4Addr::UNSPECIFIED, 0, 80, InboundProtocol::Tcp)
856 .await
857 .unwrap();
858
859 manager
861 .remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp)
862 .await;
863 manager
864 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
865 .await
866 .unwrap();
867 }
868
869 #[test]
870 fn invalid_host_ip_is_rejected() {
871 assert!(
874 "::1".parse::<Ipv4Addr>().is_err(),
875 "IPv6 should fail Ipv4Addr parse"
876 );
877 assert!("not-an-ip".parse::<Ipv4Addr>().is_err());
878 assert!("".parse::<Ipv4Addr>().is_err());
879 assert!("127.0.0.1".parse::<Ipv4Addr>().is_ok());
881 assert!("0.0.0.0".parse::<Ipv4Addr>().is_ok());
882 }
883}