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 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 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 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 ring,
172 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
195pub trait TxPacket {
198 type Addrs: AsRef<[SocketAddr]>;
199 type Payload: AsRef<[u8]>;
200
201 fn dst_addrs(&self) -> &Self::Addrs;
203
204 fn payload(&self) -> &Self::Payload;
206
207 fn src_addr(&self) -> SocketAddrV4;
209
210 fn ecn(&self) -> Option<EcnCodepoint>;
212
213 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 const RECV_TIMEOUT: Duration = Duration::from_nanos(1000);
226
227 const MAX_TIMEOUTS: usize = 1;
228
229 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 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 let mut batched_items = Vec::with_capacity(BATCH_SIZE);
251
252 let mut batched_packets = 0;
256 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 kick(&ring);
279 }
280 }
281 Err(TryRecvError::Disconnected) => {
282 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 {
302 completion.sync(true);
303 ring.sync(false);
305
306 while let Some(frame_offset) = completion.read() {
308 umem.release(frame_offset);
309 }
310
311 if ring.available() > 0 && umem.available() > 0 {
312 break;
314 }
315
316 kick(&ring);
319 }
320 }
321
322 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 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 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 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 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 .expect("failed to write to ring");
523
524 batched_packets -= 1;
525 written_uncommitted += 1;
526
527 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 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#[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 Some(libc::EBUSY | libc::ENOBUFS | libc::EAGAIN) => {}
589 Some(libc::ENETDOWN) => {
592 log::warn!("network interface is down")
593 }
594 _ => {
596 log::error!("network interface driver error: {e:?}");
597 }
598 }
599}