Skip to main content

smolvm_network/
stack.rs

1//! Host-side smoltcp runtime for the virtio-net backend.
2//!
3//! Context
4//! =======
5//!
6//! This file is the in-process "gateway" that sits behind the guest's virtio
7//! NIC. It does not configure the guest interface; that already happened inside
8//! `smolvm-agent`. Instead, this module:
9//! - receives raw Ethernet frames coming out of libkrun
10//! - feeds them into smoltcp as if smolvm were the guest's next-hop gateway
11//! - forwards guest DNS queries to a host UDP socket
12//! - relays guest TCP streams to host `TcpStream`s
13//!
14//! Conceptually, it plays the role of a tiny virtual router/NAT-side gateway:
15//!
16//! ```text
17//! guest eth0
18//!   -> Ethernet frame
19//!   -> Frame queues
20//!   -> smoltcp Interface (gateway MAC/IP)
21//!   -> protocol-specific handling:
22//!        - TCP  -> host relay threads
23//!        - DNS  -> host UDP socket
24//!        - other supported egress -> future phases
25//!   -> outbound network
26//! ```
27//!
28//! Poll-loop-centric view:
29//!
30//! ```text
31//! guest_to_host queue
32//!   -> VirtioNetworkDevice::stage_next_frame()
33//!   -> classify_guest_frame()
34//!   -> smoltcp ingress
35//!   -> protocol-specific side effects
36//!        - TCP SYN  -> create relay/socket state
37//!        - DNS UDP  -> gateway UDP socket
38//!        - other UDP-> drop for now
39//!   -> smoltcp egress
40//!   -> host_to_guest queue
41//!   -> FrameStream writer
42//! ```
43//!
44//! Runtime control flow:
45//!
46//! ```text
47//! new guest frame         -> guest_wake  -> poll loop
48//! host relay has data     -> relay_wake  -> poll loop
49//! published host connect  -> relay_wake  -> poll loop
50//! smoltcp emitted frames  -> host_wake   -> frame writer
51//! ```
52
53use crate::device::VirtioNetworkDevice;
54use crate::dns;
55use crate::egress::EgressPolicy;
56use crate::queues::NetworkFrameQueues;
57use crate::tcp_listeners::AcceptedTcpConnection;
58use crate::tcp_relay::{spawn_tcp_relay, TcpRelayTable};
59use crate::{virtio_net_log, DEFAULT_DNS_ADDR};
60use smoltcp::iface::{
61    Config, Interface, PollIngressSingleResult, PollResult, SocketHandle, SocketSet,
62};
63use smoltcp::socket::udp::{PacketBuffer, PacketMetadata, Socket as UdpSocket, UdpMetadata};
64use smoltcp::time::Instant;
65use smoltcp::wire::{
66    EthernetAddress, EthernetFrame, EthernetProtocol, HardwareAddress, IpAddress, IpCidr,
67    Ipv4Packet, Ipv6Packet, TcpPacket, UdpPacket,
68};
69use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as HostUdpSocket};
70use std::sync::atomic::Ordering;
71use std::sync::mpsc::{Receiver, TryRecvError};
72use std::sync::Arc;
73use std::thread::{self, JoinHandle};
74use std::time::{Duration, Instant as StdInstant};
75
76const DNS_SOCKET_PORT: u16 = 53;
77const DNS_PACKET_SLOTS: usize = 8;
78const DNS_BUFFER_BYTES: usize = 2048;
79const DEFAULT_IDLE_TIMEOUT_MS: i32 = 100;
80
81/// Resolved network parameters for one guest NIC.
82///
83/// These are the host-side parameters for the virtual link. Note that the
84/// smoltcp interface is configured with the *gateway* MAC/IP, because the host
85/// runtime is acting as the guest-visible gateway endpoint.
86#[derive(Debug, Clone, Copy)]
87pub struct VirtioPollConfig {
88    /// Host-side gateway MAC visible to the guest.
89    pub gateway_mac: [u8; 6],
90    /// Guest MAC address.
91    pub guest_mac: [u8; 6],
92    /// Gateway IPv4 address.
93    pub gateway_ipv4: Ipv4Addr,
94    /// Guest IPv4 address.
95    pub guest_ipv4: Ipv4Addr,
96    /// Gateway IPv6 (ULA) address.
97    pub gateway_ipv6: Ipv6Addr,
98    /// Guest IPv6 (ULA) address.
99    pub guest_ipv6: Ipv6Addr,
100    /// IPv6 prefix length for the virtual link.
101    pub prefix_len6: u8,
102    /// IP-level MTU.
103    pub mtu: usize,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107enum FrameAction {
108    TcpSyn {
109        source: SocketAddr,
110        destination: SocketAddr,
111    },
112    DnsQuery,
113    UnsupportedUdp,
114    Passthrough,
115}
116
117/// Start the dedicated smoltcp poll thread for the virtio-net backend.
118///
119/// This creates one long-lived poll loop thread per guest NIC. That thread owns
120/// the smoltcp `Interface`, its socket set, and the TCP relay table.
121///
122/// Ownership boundary:
123/// - this thread owns all smoltcp state
124/// - relay threads never touch smoltcp sockets directly
125/// - frame bridge threads never parse protocols beyond raw Ethernet framing
126pub fn start_network_stack(
127    queues: Arc<NetworkFrameQueues>,
128    config: VirtioPollConfig,
129    tcp_receiver: Option<Receiver<AcceptedTcpConnection>>,
130    egress: EgressPolicy,
131) -> std::io::Result<JoinHandle<()>> {
132    virtio_net_log!(
133        "virtio-net: spawning poll thread guest_ip={} gateway_ip={} mtu={}",
134        config.guest_ipv4,
135        config.gateway_ipv4,
136        config.mtu
137    );
138    thread::Builder::new()
139        .name("smolvm-net-poll".into())
140        .spawn(move || run_network_stack(queues, config, tcp_receiver, egress))
141}
142
143fn run_network_stack(
144    queues: Arc<NetworkFrameQueues>,
145    config: VirtioPollConfig,
146    mut tcp_receiver: Option<Receiver<AcceptedTcpConnection>>,
147    egress: EgressPolicy,
148) {
149    // Poll loop overview:
150    //
151    // 1. Drain staged guest Ethernet frames from the guest_to_host queue.
152    // 2. Pre-classify them so we can create relay/socket state before smoltcp
153    //    consumes the frame.
154    // 3. Poll smoltcp ingress/egress.
155    // 4. Forward DNS and relay TCP payloads.
156    // 5. Sleep in poll(2) on wake pipes until guest frames, relay activity, or
157    //    timers require more work.
158    //
159    // A useful mental model is:
160    //
161    //   queue input -> classify -> smoltcp -> protocol handling -> queue output
162    virtio_net_log!(
163        "virtio-net: poll loop started guest_ip={} gateway_ip={}",
164        config.guest_ipv4,
165        config.gateway_ipv4
166    );
167    let clock = StdInstant::now();
168    let mut device = VirtioNetworkDevice::new(queues.clone(), config.mtu);
169    let mut interface = create_interface(&mut device, &config);
170    let mut sockets = SocketSet::new(vec![]);
171    let dns_socket_handle = add_dns_socket(&mut sockets);
172    let relay_wake = Arc::new(queues.relay_wake.clone());
173    let mut relays = TcpRelayTable::new(None, egress.clone());
174
175    // The smoltcp loop is driven by fd-based wakeups rather than busy spinning.
176    // guest_wake  -> new guest frame or shutdown
177    // relay_wake  -> host TCP relay thread produced data or shutdown
178    let mut poll_fds = [
179        libc::pollfd {
180            fd: queues.guest_wake.as_raw_fd(),
181            events: libc::POLLIN,
182            revents: 0,
183        },
184        libc::pollfd {
185            fd: queues.relay_wake.as_raw_fd(),
186            events: libc::POLLIN,
187            revents: 0,
188        },
189    ];
190
191    loop {
192        if queues.is_shutting_down() {
193            return;
194        }
195        let now = smoltcp_now(clock);
196
197        while let Some(frame) = device.stage_next_frame() {
198            // We inspect the frame before giving it to smoltcp because certain
199            // flows need side effects first:
200            // - TCP SYN: pre-create a matching smoltcp socket + relay entry
201            // - DNS UDP: allow through for gateway-side forwarding
202            // - other UDP: currently unsupported in the MVP
203            match classify_guest_frame(frame) {
204                FrameAction::TcpSyn {
205                    source,
206                    destination,
207                } => {
208                    virtio_net_log!(
209                        "virtio-net: guest TCP SYN source={} destination={}",
210                        source,
211                        destination
212                    );
213                    if !relays.has_socket_for(&source, &destination) {
214                        relays.create_tcp_socket(source, destination, &mut sockets);
215                    }
216                    if matches!(
217                        interface.poll_ingress_single(now, &mut device, &mut sockets),
218                        PollIngressSingleResult::None
219                    ) {
220                        device.drop_staged_frame();
221                    }
222                }
223                FrameAction::DnsQuery | FrameAction::Passthrough => {
224                    if matches!(
225                        interface.poll_ingress_single(now, &mut device, &mut sockets),
226                        PollIngressSingleResult::None
227                    ) {
228                        device.drop_staged_frame();
229                    }
230                }
231                FrameAction::UnsupportedUdp => {
232                    // Phase 1 only supports DNS over UDP. Other UDP traffic is
233                    // intentionally dropped until a general UDP relay exists.
234                    virtio_net_log!("virtio-net: dropping unsupported guest UDP datagram");
235                    device.drop_staged_frame();
236                }
237            }
238        }
239
240        relay_accepted_tcp_connection(
241            &mut tcp_receiver,
242            &mut relays,
243            &mut interface,
244            &mut sockets,
245            config.gateway_ipv4,
246            config.guest_ipv4,
247        );
248
249        // First egress pass: let smoltcp emit any packets caused by the most
250        // recent ingress work before we service higher-level relays.
251        flush_interface_egress(&mut interface, &mut device, &mut sockets, now);
252        interface.poll_maintenance(now);
253        wake_guest_if_needed(&queues, &device);
254
255        // Move payloads between established smoltcp TCP sockets and host relay
256        // threads, and service the DNS gateway socket.
257        relays.relay_data(&mut sockets);
258        process_dns_queries(dns_socket_handle, &mut sockets, &egress);
259
260        // Once the guest-side TCP handshake is established inside smoltcp, we
261        // can spawn the corresponding host relay thread.
262        for connection in relays.take_new_connections(&mut sockets) {
263            spawn_tcp_relay(
264                connection.destination,
265                connection.relay_target,
266                connection.from_smoltcp,
267                connection.to_smoltcp,
268                relay_wake.clone(),
269                connection.exit_state,
270            );
271        }
272
273        relays.cleanup_closed(&mut sockets);
274
275        // Second egress pass: DNS responses or relay data may have queued more
276        // packets for the guest.
277        flush_interface_egress(&mut interface, &mut device, &mut sockets, now);
278        wake_guest_if_needed(&queues, &device);
279
280        let timeout_ms = interface
281            .poll_delay(now, &sockets)
282            .map(|duration| duration.total_millis().min(i32::MAX as u64) as i32)
283            .unwrap_or(DEFAULT_IDLE_TIMEOUT_MS);
284
285        // SAFETY: both pollfds contain valid wake-pipe descriptors.
286        unsafe {
287            libc::poll(
288                poll_fds.as_mut_ptr(),
289                poll_fds.len() as libc::nfds_t,
290                timeout_ms,
291            );
292        }
293
294        if poll_fds[0].revents & libc::POLLIN != 0 {
295            queues.guest_wake.drain();
296        }
297        if poll_fds[1].revents & libc::POLLIN != 0 {
298            queues.relay_wake.drain();
299        }
300    }
301}
302
303fn create_interface(device: &mut VirtioNetworkDevice, config: &VirtioPollConfig) -> Interface {
304    // This interface models the host-side gateway endpoint, not the guest NIC.
305    //
306    // Equivalent conceptual state:
307    //   MAC: config.gateway_mac
308    //   IP : config.gateway_ipv4/30
309    //        config.gateway_ipv6/64 (ULA) + fe80 link-local
310    //
311    // The guest IP exists as a peer on the same virtual link; it is not an
312    // address owned by this interface.
313    let mut interface = Interface::new(
314        Config::new(HardwareAddress::Ethernet(EthernetAddress(
315            config.gateway_mac,
316        ))),
317        device,
318        Instant::ZERO,
319    );
320    interface.update_ip_addrs(|addresses| {
321        addresses
322            .push(IpCidr::new(IpAddress::Ipv4(config.gateway_ipv4), 30))
323            .expect("failed to add gateway IPv4 address");
324        addresses
325            .push(IpCidr::new(
326                IpAddress::Ipv6(config.gateway_ipv6),
327                config.prefix_len6,
328            ))
329            .expect("failed to add gateway IPv6 address");
330        // RFC-clean NDP wants a link-local peer on the segment; derive the
331        // standard EUI-64 link-local from the gateway MAC so the guest kernel
332        // can talk NDP to fe80::… as well as to the ULA.
333        addresses
334            .push(IpCidr::new(
335                IpAddress::Ipv6(link_local_from_mac(config.gateway_mac)),
336                64,
337            ))
338            .expect("failed to add gateway IPv6 link-local address");
339    });
340    // The interface acts as the gateway and may need to answer packets for
341    // destinations other than its directly assigned IP, so the route table and
342    // "any IP" mode are opened up accordingly.
343    interface
344        .routes_mut()
345        .add_default_ipv4_route(config.gateway_ipv4)
346        .expect("failed to add default IPv4 route");
347    interface
348        .routes_mut()
349        .add_default_ipv6_route(config.gateway_ipv6)
350        .expect("failed to add default IPv6 route");
351    interface.set_any_ip(true);
352    interface
353}
354
355/// Derive the EUI-64 IPv6 link-local address for a MAC (RFC 4291 appendix A):
356/// flip the universal/local bit, insert `ff:fe` in the middle.
357fn link_local_from_mac(mac: [u8; 6]) -> Ipv6Addr {
358    Ipv6Addr::new(
359        0xfe80,
360        0,
361        0,
362        0,
363        u16::from_be_bytes([mac[0] ^ 0x02, mac[1]]),
364        u16::from_be_bytes([mac[2], 0xff]),
365        u16::from_be_bytes([0xfe, mac[3]]),
366        u16::from_be_bytes([mac[4], mac[5]]),
367    )
368}
369
370/// add_dns_socket is adding an UDP socket inside smoltcp, so that the guest DNS packet will
371/// hit this socket first. It is then proxied to the resolver. Note that this will not cause
372/// a host side :53 collesion, because the smoltcp Interface, SocketSet is per VM, and the
373/// gateway:53 is for that set of Interface and SocketSet, it is not bind to a host-kernel UDP socket.
374///
375/// The bind is wildcard (port-only) on purpose: combined with `set_any_ip`, every
376/// guest UDP datagram to port 53 — whatever its destination address or family
377/// (the v4 gateway, the v6 gateway, or an external resolver IP) — lands on this
378/// socket and is answered from that same destination address. That transparently
379/// intercepts hardcoded external resolvers too, matching TSI's DNS handling.
380fn add_dns_socket(sockets: &mut SocketSet<'_>) -> SocketHandle {
381    let rx_meta = vec![PacketMetadata::EMPTY; DNS_PACKET_SLOTS];
382    let tx_meta = vec![PacketMetadata::EMPTY; DNS_PACKET_SLOTS];
383    let rx_buffer = PacketBuffer::new(rx_meta, vec![0u8; DNS_BUFFER_BYTES]);
384    let tx_buffer = PacketBuffer::new(tx_meta, vec![0u8; DNS_BUFFER_BYTES]);
385    let mut socket = UdpSocket::new(rx_buffer, tx_buffer);
386    socket
387        .bind(smoltcp::wire::IpListenEndpoint {
388            addr: None,
389            port: DNS_SOCKET_PORT,
390        })
391        .expect("failed to bind gateway DNS socket");
392    sockets.add(socket)
393}
394
395/// Receive the accepted TCP connection from the tcp_channel, and then relay it to
396/// the TcpRelayTable where the TCP network packets will be relayed to the guest.
397fn relay_accepted_tcp_connection(
398    tcp_receiver: &mut Option<Receiver<AcceptedTcpConnection>>,
399    relays: &mut TcpRelayTable,
400    interface: &mut Interface,
401    sockets: &mut SocketSet<'_>,
402    gateway_ipv4: Ipv4Addr,
403    guest_ipv4: Ipv4Addr,
404) {
405    // Published-port model:
406    //
407    // host client -> accepted host TcpStream
408    //             -> create guest-facing smoltcp socket from gateway_ip:ephemeral
409    //             -> guest sees a normal inbound TCP connection to guest_port
410    //             -> once Established, the relay thread bridges payloads
411    //
412    // The guest does not see the original host peer address here. This path is
413    // effectively a small userspace TCP proxy/NAT at the gateway boundary.
414    let mut disconnected = false;
415
416    if let Some(receiver) = tcp_receiver.as_mut() {
417        loop {
418            match receiver.try_recv() {
419                Ok(connection) => {
420                    let guest_destination =
421                        SocketAddr::new(std::net::IpAddr::V4(guest_ipv4), connection.guest_port);
422                    virtio_net_log!(
423                        "virtio-net: accepted published TCP connection peer={} host_port={} guest_destination={}",
424                        connection.peer_addr,
425                        connection.host_port,
426                        guest_destination
427                    );
428                    if !relays.create_published_socket(
429                        interface,
430                        gateway_ipv4,
431                        guest_destination,
432                        connection.stream,
433                        sockets,
434                    ) {
435                        tracing::warn!(
436                            host_port = connection.host_port,
437                            guest_port = connection.guest_port,
438                            peer_addr = %connection.peer_addr,
439                            "dropping published TCP connection because the guest relay path could not be created"
440                        );
441                    }
442                }
443                Err(TryRecvError::Empty) => break,
444                Err(TryRecvError::Disconnected) => {
445                    disconnected = true;
446                    break;
447                }
448            }
449        }
450    }
451
452    if disconnected {
453        *tcp_receiver = None;
454    }
455}
456
457fn process_dns_queries(
458    dns_socket_handle: SocketHandle,
459    sockets: &mut SocketSet<'_>,
460    egress: &EgressPolicy,
461) {
462    // Phase 1 DNS model:
463    // guest UDP/53 -> smoltcp gateway socket -> host UDP socket -> upstream DNS
464    //               <-               response bytes               <-
465    let upstream_dns = match DEFAULT_DNS_ADDR {
466        std::net::IpAddr::V4(ip) => ip,
467        std::net::IpAddr::V6(_) => return,
468    };
469
470    let socket = sockets.get_mut::<UdpSocket>(dns_socket_handle);
471    while socket.can_recv() {
472        let (query, metadata) = match socket.recv() {
473            Ok(result) => result,
474            Err(_) => break,
475        };
476        virtio_net_log!(
477            "virtio-net: forwarding guest DNS query guest={} local_address={:?} query_len={} upstream_dns={}",
478            metadata.endpoint,
479            metadata.local_address,
480            query.len(),
481            upstream_dns
482        );
483        // allow-host filtering: only forward queries whose name is allow-listed;
484        // others get NXDOMAIN. A/AAAA records of allowed answers are learned as
485        // temporary egress IPs so the follow-up connection passes the filter.
486        // Mirrors libkrun's TSI DNS filter.
487        let response = if egress.dns_filter_active() {
488            match dns::question_name(query) {
489                Some(name) if egress.hostname_allowed(&name) => {
490                    match forward_dns_query(upstream_dns, query) {
491                        Ok(response) => {
492                            egress.learn_ip_records(&dns::answer_ip_records(&response));
493                            response
494                        }
495                        Err(err) => {
496                            virtio_net_log!("virtio-net: host DNS forwarding failed error={}", err);
497                            continue;
498                        }
499                    }
500                }
501                Some(name) => {
502                    virtio_net_log!(
503                        "virtio-net: blocking DNS query by allow-host policy name={}",
504                        name
505                    );
506                    dns::error_response(query, dns::DNS_RCODE_NXDOMAIN)
507                }
508                None => dns::error_response(query, dns::DNS_RCODE_SERVFAIL),
509            }
510        } else {
511            match forward_dns_query(upstream_dns, query) {
512                Ok(response) => response,
513                Err(err) => {
514                    virtio_net_log!("virtio-net: host DNS forwarding failed error={}", err);
515                    continue;
516                }
517            }
518        };
519        virtio_net_log!(
520            "virtio-net: forwarded DNS response back to guest guest={} response_len={}",
521            metadata.endpoint,
522            response.len()
523        );
524
525        let response_meta = UdpMetadata {
526            endpoint: metadata.endpoint,
527            local_address: metadata.local_address,
528            meta: Default::default(),
529        };
530        let _ = socket.send_slice(&response, response_meta);
531    }
532}
533
534fn forward_dns_query(upstream_dns: Ipv4Addr, query: &[u8]) -> std::io::Result<Vec<u8>> {
535    // This is intentionally a plain host UDP exchange rather than a smoltcp
536    // socket-to-socket relay. Once the guest packet reaches the gateway, the
537    // simplest MVP path is to proxy it with the host kernel's UDP stack.
538    //
539    // Rough shell equivalent:
540    //   send raw DNS message to `<upstream_dns>:53`
541    //   wait up to 2 seconds for one reply
542    let socket = HostUdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
543    socket.set_read_timeout(Some(Duration::from_secs(2)))?;
544    let local_addr = socket.local_addr()?;
545    virtio_net_log!(
546        "virtio-net: sending DNS query to upstream resolver local_addr={} upstream_dns={} query_len={}",
547        local_addr,
548        upstream_dns,
549        query.len()
550    );
551    socket.send_to(query, (upstream_dns, DNS_SOCKET_PORT))?;
552
553    let mut buffer = vec![0u8; DNS_BUFFER_BYTES];
554    let (bytes_read, _) = socket.recv_from(&mut buffer)?;
555    buffer.truncate(bytes_read);
556    virtio_net_log!(
557        "virtio-net: received DNS response from upstream resolver upstream_dns={} response_len={}",
558        upstream_dns,
559        buffer.len()
560    );
561    Ok(buffer)
562}
563
564fn flush_interface_egress(
565    interface: &mut Interface,
566    device: &mut VirtioNetworkDevice,
567    sockets: &mut SocketSet<'_>,
568    now: Instant,
569) {
570    // smoltcp may have multiple pending egress packets after a single ingress
571    // event or timeout. Keep polling until the interface reports there is no
572    // more immediate work.
573    loop {
574        let result = interface.poll_egress(now, device, sockets);
575        if matches!(result, PollResult::None) {
576            break;
577        }
578    }
579}
580
581fn wake_guest_if_needed(queues: &NetworkFrameQueues, device: &VirtioNetworkDevice) {
582    // The device records only that "some frame was emitted". We convert that
583    // sticky bit into one wake for the writer thread and let the writer drain
584    // the entire host_to_guest queue.
585    if device.frames_emitted.swap(false, Ordering::Relaxed) {
586        queues.host_wake.wake();
587    }
588}
589
590fn smoltcp_now(clock: StdInstant) -> Instant {
591    let elapsed = clock.elapsed();
592    Instant::from_millis(elapsed.as_millis() as i64)
593}
594
595fn classify_guest_frame(frame: &[u8]) -> FrameAction {
596    let ethernet = match EthernetFrame::new_checked(frame) {
597        Ok(frame) => frame,
598        Err(_) => return FrameAction::Passthrough,
599    };
600
601    // Extract (src, dst, transport protocol, transport payload) from either IP
602    // family. Anything that isn't plain IPv4/IPv6 — ARP, and IPv6 packets with
603    // extension headers (which guest TCP/UDP traffic doesn't use) — passes
604    // through to smoltcp untouched; that also covers ICMPv6/NDP.
605    let (src_ip, dst_ip, protocol, transport): (IpAddr, IpAddr, _, _) = match ethernet.ethertype() {
606        EthernetProtocol::Ipv4 => {
607            let ipv4 = match Ipv4Packet::new_checked(ethernet.payload()) {
608                Ok(packet) => packet,
609                Err(_) => return FrameAction::Passthrough,
610            };
611            (
612                IpAddr::V4(ipv4.src_addr()),
613                IpAddr::V4(ipv4.dst_addr()),
614                ipv4.next_header(),
615                ipv4.payload(),
616            )
617        }
618        EthernetProtocol::Ipv6 => {
619            let ipv6 = match Ipv6Packet::new_checked(ethernet.payload()) {
620                Ok(packet) => packet,
621                Err(_) => return FrameAction::Passthrough,
622            };
623            (
624                IpAddr::V6(ipv6.src_addr()),
625                IpAddr::V6(ipv6.dst_addr()),
626                ipv6.next_header(),
627                ipv6.payload(),
628            )
629        }
630        _ => return FrameAction::Passthrough,
631    };
632
633    match protocol {
634        smoltcp::wire::IpProtocol::Tcp => {
635            let tcp = match TcpPacket::new_checked(transport) {
636                Ok(packet) => packet,
637                Err(_) => return FrameAction::Passthrough,
638            };
639
640            if tcp.syn() && !tcp.ack() {
641                FrameAction::TcpSyn {
642                    source: SocketAddr::new(src_ip, tcp.src_port()),
643                    destination: SocketAddr::new(dst_ip, tcp.dst_port()),
644                }
645            } else {
646                FrameAction::Passthrough
647            }
648        }
649        smoltcp::wire::IpProtocol::Udp => {
650            let udp = match UdpPacket::new_checked(transport) {
651                Ok(packet) => packet,
652                Err(_) => return FrameAction::Passthrough,
653            };
654
655            if udp.dst_port() == DNS_SOCKET_PORT {
656                FrameAction::DnsQuery
657            } else {
658                FrameAction::UnsupportedUdp
659            }
660        }
661        _ => FrameAction::Passthrough,
662    }
663}