Skip to main content

agave_xdp/
tx_loop.rs

1#![allow(clippy::arithmetic_side_effects)]
2
3use {
4    crate::{
5        device::{DeviceQueue, NetworkDevice, QueueId, RingSizes, TxCompletionRing},
6        ecn_codepoint::EcnCodepoint,
7        gre::{
8            construct_gre_packet, gre_packet_size,
9            packet::{GRE_HEADER_BASE_SIZE, INNER_PACKET_HEADER_SIZE},
10        },
11        netlink::MacAddress,
12        packet::{
13            IP_HEADER_SIZE, PACKET_HEADER_SIZE, UDP_HEADER_SIZE, VLAN_PACKET_HEADER_SIZE,
14            construct_packet, construct_vlan_packet,
15        },
16        route::NextHop,
17        socket::{Socket, Tx, TxRing},
18        umem::{Frame, OwnedUmem, PageAlignedMemory, Umem},
19    },
20    agave_cpu_utils::set_cpu_affinity,
21    crossbeam_queue::ArrayQueue,
22    libc::{_SC_PAGESIZE, sysconf},
23    std::{
24        error::Error,
25        fmt, io,
26        net::{IpAddr, SocketAddr, SocketAddrV4},
27        sync::{
28            Arc,
29            atomic::{AtomicUsize, Ordering},
30        },
31        thread,
32        time::Duration,
33    },
34};
35
36pub struct TxLoopConfigBuilder {
37    zero_copy: bool,
38    maybe_src_mac: Option<MacAddress>,
39}
40
41impl TxLoopConfigBuilder {
42    pub fn new() -> Self {
43        Self {
44            zero_copy: false,
45            maybe_src_mac: None,
46        }
47    }
48
49    pub fn zero_copy(&mut self, enable: bool) -> &mut Self {
50        self.zero_copy = enable;
51        self
52    }
53
54    pub fn override_src_mac(&mut self, mac: MacAddress) -> &mut Self {
55        self.maybe_src_mac = Some(mac);
56        self
57    }
58
59    pub fn build_with_src_device(self, src_device: &NetworkDevice) -> TxLoopConfig {
60        let Self {
61            zero_copy,
62            maybe_src_mac,
63        } = self;
64
65        let src_mac = maybe_src_mac.unwrap_or_else(|| {
66            // if no source MAC is provided, use the device's MAC address
67            src_device
68                .mac_addr()
69                .expect("no src_mac provided, device must have a MAC address")
70        });
71
72        TxLoopConfig { zero_copy, src_mac }
73    }
74}
75
76impl Default for TxLoopConfigBuilder {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[derive(Clone, Debug)]
83pub struct TxLoopConfig {
84    zero_copy: bool,
85    src_mac: MacAddress,
86}
87
88pub struct TxLoopBuilder<U: Umem> {
89    cpu_id: usize,
90    zero_copy: bool,
91    src_mac: MacAddress,
92    queue: DeviceQueue,
93    tx_size: usize,
94    umem: U,
95}
96
97impl TxLoopBuilder<OwnedUmem> {
98    pub fn new(
99        cpu_id: usize,
100        queue_id: QueueId,
101        config: TxLoopConfig,
102        dev: &NetworkDevice,
103    ) -> TxLoopBuilder<OwnedUmem> {
104        let TxLoopConfig { zero_copy, src_mac } = config;
105
106        log::info!(
107            "starting xdp loop on {} queue {queue_id:?} cpu {cpu_id}",
108            dev.name()
109        );
110
111        // We don't support MTUs larger than page size due to AF_XDP limitations in single-buffer
112        // mode and a possible workaround might be to use multi-buffer TX.
113
114        // some drivers require frame_size=page_size
115        let frame_size = unsafe { sysconf(_SC_PAGESIZE) } as usize;
116
117        let queue = dev
118            .open_queue(queue_id)
119            .expect("failed to open queue for AF_XDP socket");
120        let RingSizes {
121            rx: rx_size,
122            tx: tx_size,
123        } = queue.ring_sizes().unwrap_or_else(|| {
124            log::info!(
125                "using default ring sizes for {} queue {queue_id:?}",
126                dev.name()
127            );
128            RingSizes::default()
129        });
130
131        let frame_count = (rx_size + tx_size) * 2;
132
133        // try to allocate huge pages first, then fall back to regular pages
134        const HUGE_2MB: usize = 2 * 1024 * 1024;
135        let memory =
136            PageAlignedMemory::alloc_with_page_size(frame_size, frame_count, HUGE_2MB, true)
137                .or_else(|_| {
138                    log::warn!("huge page alloc failed, falling back to regular page size");
139                    PageAlignedMemory::alloc(frame_size, frame_count)
140                })
141                .unwrap();
142        let umem = OwnedUmem::new(memory, frame_size as u32).unwrap();
143
144        TxLoopBuilder {
145            cpu_id,
146            zero_copy,
147            src_mac,
148            queue,
149            tx_size,
150            umem,
151        }
152    }
153
154    pub fn build(self) -> Result<TxLoop<OwnedUmem>, io::Error> {
155        let TxLoopBuilder {
156            cpu_id,
157            zero_copy,
158            src_mac,
159            queue,
160            tx_size,
161            umem,
162        } = self;
163
164        let queue_id = queue.id();
165        let (socket, tx) =
166            Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size).map_err(|err| {
167                log::error!(
168                    "failed to create AF_XDP TX socket for queue {queue_id:?} on CPU {cpu_id}: \
169                     {err}"
170                );
171                err
172            })?;
173
174        let Tx {
175            // this is where we'll queue frames
176            ring,
177            // this is where we'll get completion events once frames have been picked up by the NIC
178            completion,
179        } = tx;
180        let ring = ring.unwrap();
181
182        Ok(TxLoop {
183            cpu_id,
184            src_mac,
185            socket,
186            ring,
187            completion,
188        })
189    }
190}
191
192pub struct TxLoop<U: Umem> {
193    cpu_id: usize,
194    src_mac: MacAddress,
195    socket: Socket<U>,
196    ring: TxRing<U::Frame>,
197    completion: TxCompletionRing,
198}
199
200/// [`TxPacket`] represents a packet to transmit via XDP to a list of addresses with the provided
201/// `payload` and source address.
202pub trait TxPacket {
203    type Addrs: AsRef<[SocketAddr]>;
204    type Payload: AsRef<[u8]>;
205
206    /// List of destination addresses to which the packet should be sent.
207    fn dst_addrs(&self) -> &Self::Addrs;
208
209    /// Payload of the packet to be sent.
210    fn payload(&self) -> &Self::Payload;
211
212    /// Source address used when sending the packet.
213    fn src_addr(&self) -> SocketAddrV4;
214
215    /// Explicit congestion notification bits to set on the packet.
216    fn ecn(&self) -> Option<EcnCodepoint>;
217
218    /// Returns true when this packet is expected to occasionally exceed the route MTU.
219    fn allow_mtu_overflow(&self) -> bool;
220}
221
222#[derive(Copy, Clone, Debug, PartialEq, Eq)]
223pub enum TryRecvError {
224    Empty,
225    Disconnected,
226}
227
228pub enum TrySendError<T> {
229    Full(T),
230    Disconnected(T),
231}
232
233impl<T> fmt::Debug for TrySendError<T> {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        match self {
236            Self::Full(_) => f.write_str("Full(..)"),
237            Self::Disconnected(_) => f.write_str("Disconnected(..)"),
238        }
239    }
240}
241
242impl<T> fmt::Display for TrySendError<T> {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        match self {
245            Self::Full(_) => f.write_str("sending into a full XDP transmit channel"),
246            Self::Disconnected(_) => f.write_str("sending into a closed XDP transmit channel"),
247        }
248    }
249}
250
251impl<T> Error for TrySendError<T> {}
252
253pub trait Receiver<T> {
254    fn try_recv(&self) -> Result<T, TryRecvError>;
255}
256
257pub struct TxSender<T> {
258    queue: Arc<SharedQueue<T>>,
259}
260
261pub struct TxReceiver<T> {
262    queue: Arc<SharedQueue<T>>,
263}
264
265struct SharedQueue<T> {
266    queue: ArrayQueue<T>,
267    senders: AtomicUsize,
268    receivers: AtomicUsize,
269}
270
271pub fn channel<T>(capacity: usize) -> (TxSender<T>, TxReceiver<T>) {
272    let queue = Arc::new(SharedQueue {
273        queue: ArrayQueue::new(capacity),
274        senders: AtomicUsize::new(1),
275        receivers: AtomicUsize::new(1),
276    });
277    (
278        TxSender {
279            queue: Arc::clone(&queue),
280        },
281        TxReceiver { queue },
282    )
283}
284
285impl<T> Clone for TxSender<T> {
286    fn clone(&self) -> Self {
287        self.queue.senders.fetch_add(1, Ordering::Relaxed);
288        Self {
289            queue: Arc::clone(&self.queue),
290        }
291    }
292}
293
294impl<T> Drop for TxSender<T> {
295    fn drop(&mut self) {
296        self.queue.senders.fetch_sub(1, Ordering::Release);
297    }
298}
299
300impl<T> TxSender<T> {
301    pub fn try_send(&self, item: T) -> Result<(), TrySendError<T>> {
302        if self.queue.receivers.load(Ordering::Relaxed) == 0 {
303            return Err(TrySendError::Disconnected(item));
304        }
305        self.queue.queue.push(item).map_err(TrySendError::Full)
306    }
307}
308
309impl<T> Drop for TxReceiver<T> {
310    fn drop(&mut self) {
311        self.queue.receivers.fetch_sub(1, Ordering::Relaxed);
312    }
313}
314
315impl<T> Receiver<T> for TxReceiver<T> {
316    fn try_recv(&self) -> Result<T, TryRecvError> {
317        if let Some(item) = self.queue.queue.pop() {
318            return Ok(item);
319        }
320        if self.queue.senders.load(Ordering::Acquire) != 0 {
321            return Err(TryRecvError::Empty);
322        }
323        self.queue.queue.pop().ok_or(TryRecvError::Disconnected)
324    }
325}
326
327impl<U: Umem> TxLoop<U> {
328    pub fn run<T, Rx, D, R>(self, receiver: Rx, mut drop_item: D, mut route_fn: R)
329    where
330        T: TxPacket,
331        Rx: Receiver<T>,
332        D: FnMut(T),
333        R: FnMut(&IpAddr) -> Option<NextHop>,
334    {
335        // How long we sleep waiting to receive packets from the channel.
336        const RECV_TIMEOUT: Duration = Duration::from_nanos(1000);
337
338        const MAX_TIMEOUTS: usize = 1;
339
340        // Publish TX descriptors in batches to avoid per-packet commits and driver kicks. An idle
341        // timeout commits any partial batch so low rate traffic is not held indefinitely.
342        const BATCH_SIZE: usize = 64;
343
344        let TxLoop {
345            cpu_id,
346            src_mac,
347            socket,
348            mut ring,
349            mut completion,
350        } = self;
351
352        // each queue is bound to its own CPU core
353        set_cpu_affinity(None, [agave_cpu_utils::CpuId::new(cpu_id).unwrap()]).unwrap();
354
355        let umem = socket.umem();
356        let umem_tx_capacity = umem.available();
357        let umem_frame_size = umem.frame_size();
358
359        // How many descriptors are written into the TX ring but not yet committed.
360        let mut written_uncommitted = 0;
361
362        let mut timeouts = 0;
363        loop {
364            let item = match receiver.try_recv() {
365                Ok(item) => {
366                    timeouts = 0;
367                    item
368                }
369                Err(TryRecvError::Empty) => {
370                    if timeouts < MAX_TIMEOUTS {
371                        timeouts += 1;
372                        thread::sleep(RECV_TIMEOUT);
373                    } else {
374                        timeouts = 0;
375                        commit_pending(&mut ring, &mut written_uncommitted);
376                        // we haven't received anything in a while, kick the driver
377                        kick(&ring);
378                    }
379                    continue;
380                }
381                Err(TryRecvError::Disconnected) => break,
382            };
383
384            let src_addr = item.src_addr();
385            let src_ip = src_addr.ip();
386            let src_port = src_addr.port();
387            let ecn = item.ecn();
388            let can_overflow_mtu = item.allow_mtu_overflow();
389            for addr in item.dst_addrs().as_ref() {
390                if ring.available() == 0 || umem.available() == 0 {
391                    commit_pending(&mut ring, &mut written_uncommitted);
392                    kick(&ring);
393
394                    // loop until we have space for the next packet
395                    loop {
396                        completion.sync(true);
397                        // we haven't written any frames so we only need to sync the consumer position
398                        ring.sync(false);
399
400                        // check if any frames were completed
401                        while let Some(frame) = completion.read() {
402                            umem.release_completed(frame);
403                        }
404
405                        if ring.available() > 0 && umem.available() > 0 {
406                            // we have space for the next packet, break out of the loop
407                            break;
408                        }
409
410                        // queues are full, if NEEDS_WAKEUP is set kick the driver so hopefully it'll
411                        // complete some work
412                        kick(&ring);
413                    }
414                }
415
416                // at this point we're guaranteed to have a frame to write the next packet into and
417                // a slot in the ring to submit it
418                let mut frame = umem.reserve().unwrap();
419                let IpAddr::V4(dst_ip) = addr.ip() else {
420                    panic!("IPv6 not supported");
421                };
422
423                let payload = item.payload().as_ref();
424                let len = payload.len();
425
426                let dst = addr.ip();
427                let Some(next_hop) = route_fn(&dst) else {
428                    log::warn!("dropping packet: no route for peer {addr}");
429                    umem.release(frame);
430                    continue;
431                };
432
433                if let Some(gre) = &next_hop.gre {
434                    let Some(dest_mac) = gre.underlay_mac_addr else {
435                        log::warn!(
436                            "dropping packet: GRE peer {addr} must be routed through {} on if{} \
437                             which has no known MAC address",
438                            gre.underlay_ip_addr,
439                            gre.underlay_if_index
440                        );
441                        umem.release(frame);
442                        continue;
443                    };
444
445                    let l3_inner_packet_len = INNER_PACKET_HEADER_SIZE + len;
446                    let l3_outer_gre_packet_len =
447                        IP_HEADER_SIZE + GRE_HEADER_BASE_SIZE + l3_inner_packet_len;
448
449                    if l3_inner_packet_len > gre.mtu as usize
450                        || l3_outer_gre_packet_len > next_hop.mtu as usize
451                    {
452                        if !can_overflow_mtu {
453                            log::warn!(
454                                "dropping packet: GRE payload exceeds MTU for {addr}: L3 inner \
455                                 packet length {l3_inner_packet_len}, L3 outer GRE packet length \
456                                 {l3_outer_gre_packet_len}, MTU: {mtu}, underlay_mtu: \
457                                 {underlay_mtu}.",
458                                mtu = gre.mtu,
459                                underlay_mtu = next_hop.mtu
460                            );
461                        }
462                        umem.release(frame);
463                        continue;
464                    }
465
466                    let packet_len = gre_packet_size(len);
467                    if packet_len > umem_frame_size {
468                        log::warn!(
469                            "dropping packet: GRE packet size {packet_len} exceeds frame size \
470                             {umem_frame_size} for {addr}"
471                        );
472                        umem.release(frame);
473                        continue;
474                    }
475
476                    frame.set_len(packet_len);
477                    let mut packet = umem.map_frame_mut(frame);
478                    let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip);
479                    if let Err(err) = construct_gre_packet(
480                        &mut packet,
481                        &src_mac,
482                        &dest_mac,
483                        inner_src_ip,
484                        &dst_ip,
485                        src_port,
486                        addr.port(),
487                        payload,
488                        ecn,
489                        &gre.tunnel_info,
490                    ) {
491                        log::warn!("dropping packet: {err}");
492                        umem.release(packet.into_frame());
493                        continue;
494                    }
495                    frame = packet.into_frame();
496                } else if let Some(vlan) = &next_hop.vlan {
497                    // we need the MAC address to send the packet
498                    let Some(dest_mac) = next_hop.mac_addr else {
499                        log::warn!(
500                            "dropping packet: peer {addr} must be routed through {} which has no \
501                             known MAC address",
502                            next_hop.ip_addr
503                        );
504                        umem.release(frame);
505                        continue;
506                    };
507
508                    // The 802.1Q tag is added at L2, so the L3 size compared against the MTU
509                    // is the same as the untagged path.
510                    let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len;
511                    if l3_packet_len > next_hop.mtu as usize {
512                        if !can_overflow_mtu {
513                            log::warn!(
514                                "dropping packet: packet size {l3_packet_len} exceeds MTU {mtu} \
515                                 for {addr}",
516                                mtu = next_hop.mtu
517                            );
518                        }
519                        umem.release(frame);
520                        continue;
521                    }
522
523                    let packet_len = VLAN_PACKET_HEADER_SIZE + len;
524                    if packet_len > umem_frame_size {
525                        log::warn!(
526                            "dropping packet: VLAN packet size {packet_len} exceeds frame size \
527                             {umem_frame_size} for {addr}"
528                        );
529                        umem.release(frame);
530                        continue;
531                    }
532
533                    frame.set_len(packet_len);
534                    let mut packet = umem.map_frame_mut(frame);
535
536                    // The route's preferred src is the IP assigned to the VLAN sub-interface,
537                    // which is the right inner src for traffic egressing this VLAN. Fall back
538                    // to the device's src IP if the route did not carry one.
539                    let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip);
540
541                    if !construct_vlan_packet(
542                        &mut packet,
543                        &src_mac.0,
544                        &dest_mac.0,
545                        inner_src_ip,
546                        &dst_ip,
547                        src_port,
548                        addr.port(),
549                        vlan.vid,
550                        vlan.pcp,
551                        payload,
552                        ecn,
553                    ) {
554                        log::warn!("dropping packet: VLAN frame did not fit in UMEM slot");
555                        umem.release(packet.into_frame());
556                        continue;
557                    }
558                    frame = packet.into_frame();
559                } else {
560                    // we need the MAC address to send the packet
561                    let Some(dest_mac) = next_hop.mac_addr else {
562                        log::warn!(
563                            "dropping packet: peer {addr} must be routed through {} which has no \
564                             known MAC address",
565                            next_hop.ip_addr
566                        );
567                        umem.release(frame);
568                        continue;
569                    };
570
571                    let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len;
572                    if l3_packet_len > next_hop.mtu as usize {
573                        if !can_overflow_mtu {
574                            log::warn!(
575                                "dropping packet: packet size {l3_packet_len} exceeds MTU {mtu} \
576                                 for {addr}",
577                                mtu = next_hop.mtu
578                            );
579                        }
580                        umem.release(frame);
581                        continue;
582                    }
583
584                    let packet_len = PACKET_HEADER_SIZE + len;
585                    if packet_len > umem_frame_size {
586                        log::warn!(
587                            "dropping packet: packet size {packet_len} exceeds frame size \
588                             {umem_frame_size} for {addr}"
589                        );
590                        umem.release(frame);
591                        continue;
592                    }
593
594                    frame.set_len(packet_len);
595                    let mut packet = umem.map_frame_mut(frame);
596
597                    if !construct_packet(
598                        &mut packet,
599                        &src_mac.0,
600                        &dest_mac.0,
601                        src_ip,
602                        &dst_ip,
603                        src_port,
604                        addr.port(),
605                        payload,
606                        ecn,
607                    ) {
608                        log::warn!("dropping packet: frame did not fit in UMEM slot");
609                        umem.release(packet.into_frame());
610                        continue;
611                    }
612                    frame = packet.into_frame();
613                }
614
615                ring.write(frame, 0)
616                    .map_err(|_| "ring full")
617                    // this should never happen as we check for available slots above
618                    .expect("failed to write to ring");
619
620                written_uncommitted += 1;
621
622                // check if it's time to publish descriptors and kick the driver
623                if written_uncommitted >= BATCH_SIZE {
624                    commit_pending(&mut ring, &mut written_uncommitted);
625                    kick(&ring);
626                }
627            }
628            drop_item(item);
629        }
630        commit_pending(&mut ring, &mut written_uncommitted);
631        kick(&ring);
632
633        // drain the ring
634        while umem.available() < umem_tx_capacity || ring.available() < ring.capacity() {
635            log::debug!(
636                "draining xdp ring umem {}/{} ring {}/{}",
637                umem.available(),
638                umem_tx_capacity,
639                ring.available(),
640                ring.capacity()
641            );
642
643            completion.sync(true);
644            while let Some(frame) = completion.read() {
645                umem.release_completed(frame);
646            }
647
648            ring.sync(false);
649            kick(&ring);
650        }
651    }
652}
653
654#[inline(always)]
655fn commit_pending<F: Frame>(ring: &mut TxRing<F>, pending_uncommitted: &mut usize) {
656    if *pending_uncommitted == 0 {
657        return;
658    }
659    ring.commit();
660    *pending_uncommitted = 0;
661}
662
663// With some drivers, or always when we work in SKB mode, we need to explicitly kick the driver once
664// we want the NIC to do something.
665#[inline(always)]
666fn kick<F: Frame>(ring: &TxRing<F>) {
667    if !ring.needs_wakeup() {
668        return;
669    }
670
671    if let Err(e) = ring.wake() {
672        kick_error(e);
673    }
674}
675
676#[inline(never)]
677fn kick_error(e: std::io::Error) {
678    match e.raw_os_error() {
679        // these are non-fatal errors
680        Some(libc::EBUSY | libc::ENOBUFS | libc::EAGAIN) => {}
681        // this can temporarily happen with some drivers when changing
682        // settings (eg with ethtool)
683        Some(libc::ENETDOWN) => {
684            log::warn!("network interface is down")
685        }
686        // we should never get here, hopefully the driver recovers?
687        _ => {
688            log::error!("network interface driver error: {e:?}");
689        }
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use crate::tx_loop::{Receiver, TryRecvError, TrySendError, channel};
696
697    #[test]
698    fn test_send_full() {
699        let (sender, _receiver) = channel(1);
700
701        assert!(sender.try_send(1).is_ok());
702        match sender.try_send(2) {
703            Err(TrySendError::Full(item)) => assert_eq!(item, 2),
704            result => panic!("expected full queue, got {result:?}"),
705        }
706    }
707
708    #[test]
709    fn test_send_disconnected() {
710        let (sender, receiver) = channel(1);
711        drop(receiver);
712
713        match sender.try_send(1) {
714            Err(TrySendError::Disconnected(item)) => assert_eq!(item, 1),
715            result => panic!("expected disconnected queue, got {result:?}"),
716        }
717    }
718
719    #[test]
720    fn test_recv_disconnected() {
721        let (sender, receiver) = channel(1);
722        sender
723            .try_send(1)
724            .expect("send item before dropping sender");
725        drop(sender);
726
727        assert_eq!(receiver.try_recv().expect("receive queued item"), 1);
728        assert!(matches!(
729            receiver.try_recv(),
730            Err(TryRecvError::Disconnected)
731        ));
732    }
733
734    #[test]
735    fn test_recv_waits_for_cloned_sender() {
736        let (sender, receiver) = channel::<i32>(1);
737        let sender_clone = sender.clone();
738        drop(sender);
739
740        assert!(matches!(receiver.try_recv(), Err(TryRecvError::Empty)));
741        drop(sender_clone);
742        assert!(matches!(
743            receiver.try_recv(),
744            Err(TryRecvError::Disconnected)
745        ));
746    }
747}