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_channel::{Receiver, TryRecvError},
22    libc::{_SC_PAGESIZE, sysconf},
23    std::{
24        io,
25        net::{IpAddr, SocketAddr, SocketAddrV4},
26        thread,
27        time::Duration,
28    },
29};
30
31pub struct TxLoopConfigBuilder {
32    zero_copy: bool,
33    maybe_src_mac: Option<MacAddress>,
34}
35
36impl TxLoopConfigBuilder {
37    pub fn new() -> Self {
38        Self {
39            zero_copy: false,
40            maybe_src_mac: None,
41        }
42    }
43
44    pub fn zero_copy(&mut self, enable: bool) -> &mut Self {
45        self.zero_copy = enable;
46        self
47    }
48
49    pub fn override_src_mac(&mut self, mac: MacAddress) -> &mut Self {
50        self.maybe_src_mac = Some(mac);
51        self
52    }
53
54    pub fn build_with_src_device(self, src_device: &NetworkDevice) -> TxLoopConfig {
55        let Self {
56            zero_copy,
57            maybe_src_mac,
58        } = self;
59
60        let src_mac = maybe_src_mac.unwrap_or_else(|| {
61            // if no source MAC is provided, use the device's MAC address
62            src_device
63                .mac_addr()
64                .expect("no src_mac provided, device must have a MAC address")
65        });
66
67        TxLoopConfig { zero_copy, src_mac }
68    }
69}
70
71impl Default for TxLoopConfigBuilder {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77#[derive(Clone, Debug)]
78pub struct TxLoopConfig {
79    zero_copy: bool,
80    src_mac: MacAddress,
81}
82
83pub struct TxLoopBuilder<U: Umem> {
84    cpu_id: usize,
85    zero_copy: bool,
86    src_mac: MacAddress,
87    queue: DeviceQueue,
88    tx_size: usize,
89    umem: U,
90}
91
92impl TxLoopBuilder<OwnedUmem<PageAlignedMemory>> {
93    pub fn new(
94        cpu_id: usize,
95        queue_id: QueueId,
96        config: TxLoopConfig,
97        dev: &NetworkDevice,
98    ) -> TxLoopBuilder<OwnedUmem<PageAlignedMemory>> {
99        let TxLoopConfig { zero_copy, src_mac } = config;
100
101        log::info!(
102            "starting xdp loop on {} queue {queue_id:?} cpu {cpu_id}",
103            dev.name()
104        );
105
106        // We don't support MTUs larger than page size due to AF_XDP limitations in single-buffer
107        // mode and a possible workaround might be to use multi-buffer TX.
108
109        // some drivers require frame_size=page_size
110        let frame_size = unsafe { sysconf(_SC_PAGESIZE) } as usize;
111
112        let queue = dev
113            .open_queue(queue_id)
114            .expect("failed to open queue for AF_XDP socket");
115        let RingSizes {
116            rx: rx_size,
117            tx: tx_size,
118        } = queue.ring_sizes().unwrap_or_else(|| {
119            log::info!(
120                "using default ring sizes for {} queue {queue_id:?}",
121                dev.name()
122            );
123            RingSizes::default()
124        });
125
126        let frame_count = (rx_size + tx_size) * 2;
127
128        // try to allocate huge pages first, then fall back to regular pages
129        const HUGE_2MB: usize = 2 * 1024 * 1024;
130        let memory =
131            PageAlignedMemory::alloc_with_page_size(frame_size, frame_count, HUGE_2MB, true)
132                .or_else(|_| {
133                    log::warn!("huge page alloc failed, falling back to regular page size");
134                    PageAlignedMemory::alloc(frame_size, frame_count)
135                })
136                .unwrap();
137        let umem = OwnedUmem::new(memory, frame_size as u32).unwrap();
138
139        TxLoopBuilder {
140            cpu_id,
141            zero_copy,
142            src_mac,
143            queue,
144            tx_size,
145            umem,
146        }
147    }
148
149    pub fn build(self) -> Result<TxLoop<OwnedUmem<PageAlignedMemory>>, io::Error> {
150        let TxLoopBuilder {
151            cpu_id,
152            zero_copy,
153            src_mac,
154            queue,
155            tx_size,
156            umem,
157        } = self;
158
159        let queue_id = queue.id();
160        let (socket, tx) =
161            Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size).map_err(|err| {
162                log::error!(
163                    "failed to create AF_XDP TX socket for queue {queue_id:?} on CPU {cpu_id}: \
164                     {err}"
165                );
166                err
167            })?;
168
169        let Tx {
170            // this is where we'll queue frames
171            ring,
172            // this is where we'll get completion events once frames have been picked up by the NIC
173            completion,
174        } = tx;
175        let ring = ring.unwrap();
176
177        Ok(TxLoop {
178            cpu_id,
179            src_mac,
180            socket,
181            ring,
182            completion,
183        })
184    }
185}
186
187pub struct TxLoop<U: Umem> {
188    cpu_id: usize,
189    src_mac: MacAddress,
190    socket: Socket<U>,
191    ring: TxRing<U::Frame>,
192    completion: TxCompletionRing,
193}
194
195/// [`TxPacket`] represents a packet to transmit via XDP to a list of addresses with the provided
196/// `payload` and source address.
197pub trait TxPacket {
198    type Addrs: AsRef<[SocketAddr]>;
199    type Payload: AsRef<[u8]>;
200
201    /// List of destination addresses to which the packet should be sent.
202    fn dst_addrs(&self) -> &Self::Addrs;
203
204    /// Payload of the packet to be sent.
205    fn payload(&self) -> &Self::Payload;
206
207    /// Source address used when sending the packet.
208    fn src_addr(&self) -> SocketAddrV4;
209
210    /// Explicit congestion notification bits to set on the packet.
211    fn ecn(&self) -> Option<EcnCodepoint>;
212
213    /// Returns true when this packet is expected to occasionally exceed the route MTU.
214    fn allow_mtu_overflow(&self) -> bool;
215}
216
217impl<U: Umem> TxLoop<U> {
218    pub fn run<T, D, R>(self, receiver: Receiver<T>, mut drop_item: D, route_fn: R)
219    where
220        T: TxPacket,
221        D: FnMut(T),
222        R: Fn(&IpAddr) -> Option<NextHop>,
223    {
224        // How long we sleep waiting to receive packets from the channel.
225        const RECV_TIMEOUT: Duration = Duration::from_nanos(1000);
226
227        const MAX_TIMEOUTS: usize = 1;
228
229        // We try to collect _at least_ BATCH_SIZE packets before queueing into the NIC. This is to
230        // avoid introducing too much per-packet overhead and giving the NIC time to complete work
231        // before we queue the next chunk of packets.
232        const BATCH_SIZE: usize = 64;
233
234        let TxLoop {
235            cpu_id,
236            src_mac,
237            mut socket,
238            mut ring,
239            mut completion,
240        } = self;
241
242        // each queue is bound to its own CPU core
243        set_cpu_affinity(None, [agave_cpu_utils::CpuId::new(cpu_id).unwrap()]).unwrap();
244
245        let umem = socket.umem();
246        let umem_tx_capacity = umem.available();
247        let umem_frame_size = umem.frame_size();
248
249        // Local buffer where we store packets before sending them.
250        let mut batched_items = Vec::with_capacity(BATCH_SIZE);
251
252        // How many packets we've batched. This is _not_ batched_items.len(), but item * peers. For
253        // example if we have 3 packets to transmit to 2 destination addresses each, we have 6 batched
254        // packets.
255        let mut batched_packets = 0;
256        // How many descriptors are written into the TX ring but not yet committed.
257        let mut written_uncommitted = 0;
258
259        let mut timeouts = 0;
260        loop {
261            match receiver.try_recv() {
262                Ok(item) => {
263                    batched_packets += item.dst_addrs().as_ref().len();
264                    batched_items.push(item);
265                    timeouts = 0;
266                    if batched_packets < BATCH_SIZE {
267                        continue;
268                    }
269                }
270                Err(TryRecvError::Empty) => {
271                    if timeouts < MAX_TIMEOUTS {
272                        timeouts += 1;
273                        thread::sleep(RECV_TIMEOUT);
274                    } else {
275                        timeouts = 0;
276                        commit_pending(&mut ring, &mut written_uncommitted);
277                        // we haven't received anything in a while, kick the driver
278                        kick(&ring);
279                    }
280                }
281                Err(TryRecvError::Disconnected) => {
282                    // keep looping until we've flushed all the packets
283                    if batched_packets == 0 {
284                        break;
285                    }
286                }
287            };
288
289            for item in batched_items.drain(..) {
290                let src_addr = item.src_addr();
291                let src_ip = src_addr.ip();
292                let src_port = src_addr.port();
293                let ecn = item.ecn();
294                let can_overflow_mtu = item.allow_mtu_overflow();
295                for addr in item.dst_addrs().as_ref() {
296                    if ring.available() == 0 || umem.available() == 0 {
297                        commit_pending(&mut ring, &mut written_uncommitted);
298                        kick(&ring);
299
300                        // loop until we have space for the next packet
301                        loop {
302                            completion.sync(true);
303                            // we haven't written any frames so we only need to sync the consumer position
304                            ring.sync(false);
305
306                            // check if any frames were completed
307                            while let Some(frame_offset) = completion.read() {
308                                umem.release(frame_offset);
309                            }
310
311                            if ring.available() > 0 && umem.available() > 0 {
312                                // we have space for the next packet, break out of the loop
313                                break;
314                            }
315
316                            // queues are full, if NEEDS_WAKEUP is set kick the driver so hopefully it'll
317                            // complete some work
318                            kick(&ring);
319                        }
320                    }
321
322                    // at this point we're guaranteed to have a frame to write the next packet into and
323                    // a slot in the ring to submit it
324                    let mut frame = umem.reserve().unwrap();
325                    let IpAddr::V4(dst_ip) = addr.ip() else {
326                        panic!("IPv6 not supported");
327                    };
328
329                    let payload = item.payload().as_ref();
330                    let len = payload.len();
331
332                    let dst = addr.ip();
333                    let Some(next_hop) = route_fn(&dst) else {
334                        log::warn!("dropping packet: no route for peer {addr}");
335                        batched_packets -= 1;
336                        umem.release(frame.offset());
337                        continue;
338                    };
339
340                    if let Some(gre) = &next_hop.gre {
341                        let l3_inner_packet_len = INNER_PACKET_HEADER_SIZE + len;
342                        let l3_outer_gre_packet_len =
343                            IP_HEADER_SIZE + GRE_HEADER_BASE_SIZE + l3_inner_packet_len;
344
345                        if l3_inner_packet_len > gre.mtu as usize
346                            || l3_outer_gre_packet_len > next_hop.mtu as usize
347                        {
348                            if !can_overflow_mtu {
349                                log::warn!(
350                                    "dropping packet: GRE payload exceeds MTU for {addr}: L3 \
351                                     inner packet length {l3_inner_packet_len}, L3 outer GRE \
352                                     packet length {l3_outer_gre_packet_len}, MTU: {mtu}, \
353                                     underlay_mtu: {underlay_mtu}.",
354                                    mtu = gre.mtu,
355                                    underlay_mtu = next_hop.mtu
356                                );
357                            }
358                            batched_packets -= 1;
359                            umem.release(frame.offset());
360                            continue;
361                        }
362
363                        let packet_len = gre_packet_size(len);
364                        if packet_len > umem_frame_size {
365                            log::warn!(
366                                "dropping packet: GRE packet size {packet_len} exceeds frame size \
367                                 {umem_frame_size} for {addr}"
368                            );
369                            batched_packets -= 1;
370                            umem.release(frame.offset());
371                            continue;
372                        }
373
374                        frame.set_len(packet_len);
375                        let packet = umem.map_frame_mut(&frame);
376                        let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip);
377                        if let Err(err) = construct_gre_packet(
378                            packet,
379                            &src_mac,
380                            &gre.mac_addr,
381                            inner_src_ip,
382                            &dst_ip,
383                            src_port,
384                            addr.port(),
385                            payload,
386                            ecn,
387                            &gre.tunnel_info,
388                        ) {
389                            log::warn!("dropping packet: {err}");
390                            batched_packets -= 1;
391                            umem.release(frame.offset());
392                            continue;
393                        }
394                    } else if let Some(vlan) = &next_hop.vlan {
395                        // we need the MAC address to send the packet
396                        let Some(dest_mac) = next_hop.mac_addr else {
397                            log::warn!(
398                                "dropping packet: peer {addr} must be routed through {} which has \
399                                 no known MAC address",
400                                next_hop.ip_addr
401                            );
402                            batched_packets -= 1;
403                            umem.release(frame.offset());
404                            continue;
405                        };
406
407                        // The 802.1Q tag is added at L2, so the L3 size compared against the MTU
408                        // is the same as the untagged path.
409                        let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len;
410                        if l3_packet_len > next_hop.mtu as usize {
411                            if !can_overflow_mtu {
412                                log::warn!(
413                                    "dropping packet: packet size {l3_packet_len} exceeds MTU \
414                                     {mtu} for {addr}",
415                                    mtu = next_hop.mtu
416                                );
417                            }
418                            batched_packets -= 1;
419                            umem.release(frame.offset());
420                            continue;
421                        }
422
423                        let packet_len = VLAN_PACKET_HEADER_SIZE + len;
424                        if packet_len > umem_frame_size {
425                            log::warn!(
426                                "dropping packet: VLAN packet size {packet_len} exceeds frame \
427                                 size {umem_frame_size} for {addr}"
428                            );
429                            batched_packets -= 1;
430                            umem.release(frame.offset());
431                            continue;
432                        }
433
434                        frame.set_len(packet_len);
435                        let packet = umem.map_frame_mut(&frame);
436
437                        // The route's preferred src is the IP assigned to the VLAN sub-interface,
438                        // which is the right inner src for traffic egressing this VLAN. Fall back
439                        // to the device's src IP if the route did not carry one.
440                        let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip);
441
442                        if !construct_vlan_packet(
443                            packet,
444                            &src_mac.0,
445                            &dest_mac.0,
446                            inner_src_ip,
447                            &dst_ip,
448                            src_port,
449                            addr.port(),
450                            vlan.vid,
451                            vlan.pcp,
452                            payload,
453                            ecn,
454                        ) {
455                            log::warn!("dropping packet: VLAN frame did not fit in UMEM slot");
456                            batched_packets -= 1;
457                            umem.release(frame.offset());
458                            continue;
459                        }
460                    } else {
461                        // we need the MAC address to send the packet
462                        let Some(dest_mac) = next_hop.mac_addr else {
463                            log::warn!(
464                                "dropping packet: peer {addr} must be routed through {} which has \
465                                 no known MAC address",
466                                next_hop.ip_addr
467                            );
468                            batched_packets -= 1;
469                            umem.release(frame.offset());
470                            continue;
471                        };
472
473                        let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len;
474                        if l3_packet_len > next_hop.mtu as usize {
475                            if !can_overflow_mtu {
476                                log::warn!(
477                                    "dropping packet: packet size {l3_packet_len} exceeds MTU \
478                                     {mtu} for {addr}",
479                                    mtu = next_hop.mtu
480                                );
481                            }
482                            batched_packets -= 1;
483                            umem.release(frame.offset());
484                            continue;
485                        }
486
487                        let packet_len = PACKET_HEADER_SIZE + len;
488                        if packet_len > umem_frame_size {
489                            log::warn!(
490                                "dropping packet: packet size {packet_len} exceeds frame size \
491                                 {umem_frame_size} for {addr}"
492                            );
493                            batched_packets -= 1;
494                            umem.release(frame.offset());
495                            continue;
496                        }
497
498                        frame.set_len(packet_len);
499                        let packet = umem.map_frame_mut(&frame);
500
501                        if !construct_packet(
502                            packet,
503                            &src_mac.0,
504                            &dest_mac.0,
505                            src_ip,
506                            &dst_ip,
507                            src_port,
508                            addr.port(),
509                            payload,
510                            ecn,
511                        ) {
512                            log::warn!("dropping packet: frame did not fit in UMEM slot");
513                            batched_packets -= 1;
514                            umem.release(frame.offset());
515                            continue;
516                        }
517                    }
518
519                    ring.write(frame, 0)
520                        .map_err(|_| "ring full")
521                        // this should never happen as we check for available slots above
522                        .expect("failed to write to ring");
523
524                    batched_packets -= 1;
525                    written_uncommitted += 1;
526
527                    // check if it's time to publish descriptors and kick the driver
528                    if written_uncommitted >= BATCH_SIZE {
529                        commit_pending(&mut ring, &mut written_uncommitted);
530                        kick(&ring);
531                    }
532                }
533                drop_item(item);
534            }
535            debug_assert_eq!(batched_packets, 0);
536        }
537        assert_eq!(batched_packets, 0);
538        commit_pending(&mut ring, &mut written_uncommitted);
539        kick(&ring);
540
541        // drain the ring
542        while umem.available() < umem_tx_capacity || ring.available() < ring.capacity() {
543            log::debug!(
544                "draining xdp ring umem {}/{} ring {}/{}",
545                umem.available(),
546                umem_tx_capacity,
547                ring.available(),
548                ring.capacity()
549            );
550
551            completion.sync(true);
552            while let Some(frame_offset) = completion.read() {
553                umem.release(frame_offset);
554            }
555
556            ring.sync(false);
557            kick(&ring);
558        }
559    }
560}
561
562#[inline(always)]
563fn commit_pending<F: Frame>(ring: &mut TxRing<F>, pending_uncommitted: &mut usize) {
564    if *pending_uncommitted == 0 {
565        return;
566    }
567    ring.commit();
568    *pending_uncommitted = 0;
569}
570
571// With some drivers, or always when we work in SKB mode, we need to explicitly kick the driver once
572// we want the NIC to do something.
573#[inline(always)]
574fn kick<F: Frame>(ring: &TxRing<F>) {
575    if !ring.needs_wakeup() {
576        return;
577    }
578
579    if let Err(e) = ring.wake() {
580        kick_error(e);
581    }
582}
583
584#[inline(never)]
585fn kick_error(e: std::io::Error) {
586    match e.raw_os_error() {
587        // these are non-fatal errors
588        Some(libc::EBUSY | libc::ENOBUFS | libc::EAGAIN) => {}
589        // this can temporarily happen with some drivers when changing
590        // settings (eg with ethtool)
591        Some(libc::ENETDOWN) => {
592            log::warn!("network interface is down")
593        }
594        // we should never get here, hopefully the driver recovers?
595        _ => {
596            log::error!("network interface driver error: {e:?}");
597        }
598    }
599}