Skip to content

Commit d21bcad

Browse files
completely revamp examples
1 parent 42f8350 commit d21bcad

33 files changed

+598
-412
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ members = [
66
"examples/nat64",
77
"examples/ping4d",
88
"examples/pktdump",
9-
"examples/signals",
109
"examples/skeleton",
1110
"examples/syn-flood",
1211
"ffi",

core/src/net/mac.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl MacAddr {
3232

3333
/// Creates a MAC address from 6 octets.
3434
#[allow(clippy::many_single_char_names)]
35-
pub fn new(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> Self {
35+
pub const fn new(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> Self {
3636
MacAddr([a, b, c, d, e, f])
3737
}
3838

core/src/packets/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ mod udp;
3030
pub use self::ethernet::*;
3131
pub use self::tcp::*;
3232
pub use self::udp::*;
33+
pub use crate::dpdk::Mbuf;
3334

34-
use crate::Mbuf;
3535
use anyhow::{Context, Result};
3636
use std::fmt;
3737
use std::marker::PhantomData;
@@ -336,6 +336,20 @@ impl<T> Deref for Immutable<'_, T> {
336336
}
337337
}
338338

339+
/// Mark of the packet as either `Emit` or `Drop`.
340+
///
341+
/// Together, a `Result<Postmark>` represents all three possible outcome
342+
/// of packet processing. A packet can either be emitted through port TX,
343+
/// intentionally dropped, or aborted due to an error.
344+
#[derive(Debug)]
345+
pub enum Postmark {
346+
/// Packet emitted through a port TX.
347+
Emit,
348+
349+
/// Packet intentionally dropped.
350+
Drop(Mbuf),
351+
}
352+
339353
#[cfg(test)]
340354
mod tests {
341355
use super::*;

core/src/packets/udp.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ impl<E: IpPacket> Udp<E> {
146146
self.header_mut().checksum = u16be::default();
147147
}
148148

149+
/// Returns the data as a `u8` slice.
150+
#[inline]
151+
pub fn data(&self) -> &[u8] {
152+
if let Ok(data) = self
153+
.mbuf()
154+
.read_data_slice(self.payload_offset(), self.payload_len())
155+
{
156+
unsafe { &*data.as_ptr() }
157+
} else {
158+
unreachable!()
159+
}
160+
}
161+
149162
/// Returns the 5-tuple that uniquely identifies a UDP connection.
150163
#[inline]
151164
pub fn flow(&self) -> Flow {
@@ -394,6 +407,7 @@ mod tests {
394407
assert_eq!(1087, udp.dst_port());
395408
assert_eq!(18, udp.length());
396409
assert_eq!(0x7228, udp.checksum());
410+
assert_eq!(10, udp.data().len());
397411
}
398412

399413
#[capsule::test]

core/src/rt2/lcore.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ use anyhow::Result;
2323
use async_executor::Executor;
2424
use futures_lite::future;
2525
use std::collections::HashMap;
26+
use std::fmt;
2627
use std::future::Future;
2728
use std::sync::Arc;
2829
use thiserror::Error;
2930

3031
/// An async executor abstraction on top of a DPDK logical core.
31-
pub(crate) struct Lcore {
32+
pub struct Lcore {
3233
id: LcoreId,
3334
executor: Arc<Executor<'static>>,
3435
shutdown: Option<ShutdownTrigger>,
@@ -75,11 +76,17 @@ impl Lcore {
7576
}
7677

7778
/// Spawns a background async task.
78-
pub(crate) fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
79+
pub fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
7980
self.executor.spawn(future).detach();
8081
}
8182
}
8283

84+
impl fmt::Debug for Lcore {
85+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86+
f.debug_struct("Lcore").field("id", &self.id()).finish()
87+
}
88+
}
89+
8390
impl Drop for Lcore {
8491
fn drop(&mut self) {
8592
if let Some(trigger) = self.shutdown.take() {
@@ -92,19 +99,20 @@ impl Drop for Lcore {
9299
/// Lcore not found error.
93100
#[derive(Debug, Error)]
94101
#[error("lcore not found.")]
95-
pub(crate) struct LcoreNotFound;
102+
pub struct LcoreNotFound;
96103

97104
/// Map to lookup the lcore by the assigned id.
98-
pub(crate) struct LcoreMap(HashMap<usize, Lcore>);
105+
#[derive(Debug)]
106+
pub struct LcoreMap(HashMap<usize, Lcore>);
99107

100108
impl LcoreMap {
101109
/// Returns the lcore with the assigned id.
102-
pub(crate) fn get(&self, id: usize) -> Result<&Lcore> {
110+
pub fn get(&self, id: usize) -> Result<&Lcore> {
103111
self.0.get(&id).ok_or_else(|| LcoreNotFound.into())
104112
}
105113

106114
/// Returns a lcore iterator.
107-
pub(crate) fn iter(&self) -> impl Iterator<Item = &Lcore> {
115+
pub fn iter(&self) -> impl Iterator<Item = &Lcore> {
108116
self.0.values()
109117
}
110118
}

core/src/rt2/mempool.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ use std::ptr::{self, NonNull};
2626
use thiserror::Error;
2727

2828
/// A memory pool is an allocator of message buffers, or `Mbuf`. For best
29-
/// performance, each socket should have a dedicated `Mempool`. However,
30-
/// for simplicity, we currently only support one global Mempool. Multi-
31-
/// socket support may be added in the future.
32-
pub(crate) struct Mempool {
29+
/// performance, each socket should have a dedicated `Mempool`.
30+
pub struct Mempool {
3331
ptr: MempoolPtr,
3432
}
3533

@@ -75,13 +73,13 @@ impl Mempool {
7573

7674
/// Returns the maximum number of Mbufs in the pool.
7775
#[inline]
78-
pub(crate) fn capacity(&self) -> usize {
76+
pub fn capacity(&self) -> usize {
7977
self.ptr.size as usize
8078
}
8179

8280
/// Returns the per core cache size.
8381
#[inline]
84-
pub(crate) fn cache_size(&self) -> usize {
82+
pub fn cache_size(&self) -> usize {
8583
self.ptr.cache_size as usize
8684
}
8785

core/src/rt2/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
*/
1818

19+
//! Capsule runtime.
20+
1921
mod config;
2022
mod lcore;
2123
mod mempool;
2224
mod port;
2325

2426
pub use self::config::*;
2527
pub(crate) use self::lcore::*;
28+
pub use self::lcore::{Lcore, LcoreMap, LcoreNotFound};
29+
pub use self::mempool::Mempool;
2630
pub(crate) use self::mempool::*;
2731
pub use self::port::{Outbox, Port, PortError, PortMap};
28-
pub use crate::dpdk::Mbuf;
2932

3033
use crate::ffi::dpdk::{self, LcoreId};
34+
use crate::packets::{Mbuf, Postmark};
3135
use crate::{debug, info};
3236
use anyhow::Result;
3337
use async_channel::{self, Receiver, Sender};
@@ -88,6 +92,19 @@ pub struct Runtime {
8892
}
8993

9094
impl Runtime {
95+
/// Returns the mempool.
96+
///
97+
/// For simplicity, we currently only support one global Mempool. Multi-
98+
/// socket support may be added in the future.
99+
pub fn mempool(&self) -> &Mempool {
100+
&self.mempool
101+
}
102+
103+
/// Returns the lcores.
104+
pub fn lcores(&self) -> &LcoreMap {
105+
&self.lcores
106+
}
107+
91108
/// Returns the configured ports.
92109
pub fn ports(&self) -> &PortMap {
93110
&self.ports
@@ -151,7 +168,7 @@ impl Runtime {
151168
/// Sets the packet processing pipeline for port.
152169
pub fn set_port_pipeline<F>(&self, port: &str, f: F) -> Result<()>
153170
where
154-
F: Fn(Mbuf) -> Result<()> + Clone + Send + Sync + 'static,
171+
F: Fn(Mbuf) -> Result<Postmark> + Clone + Send + Sync + 'static,
155172
{
156173
let port = self.ports.get(port)?;
157174
port.spawn_rx_loops(f, &self.lcores)?;

core/src/rt2/port.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
*/
1818

19-
use super::{LcoreMap, Mbuf, Mempool, ShutdownTrigger};
19+
use super::{LcoreMap, Mempool, ShutdownTrigger};
2020
use crate::ffi::dpdk::{self, LcoreId, MbufPtr, PortId, PortRxQueueId, PortTxQueueId};
2121
use crate::net::MacAddr;
22-
use crate::packets::Packet;
22+
use crate::packets::{Mbuf, Packet, Postmark};
2323
use crate::{debug, ensure, info, warn};
2424
use anyhow::Result;
2525
use async_channel::{self, Receiver, Sender};
@@ -96,7 +96,7 @@ impl Port {
9696
/// Spawns the port receiving loop.
9797
pub(crate) fn spawn_rx_loops<F>(&self, f: F, lcores: &LcoreMap) -> Result<()>
9898
where
99-
F: Fn(Mbuf) -> Result<()> + Clone + Send + Sync + 'static,
99+
F: Fn(Mbuf) -> Result<Postmark> + Clone + Send + Sync + 'static,
100100
{
101101
// port is built with the builder, this would not panic.
102102
let shutdown = self.shutdown.as_ref().unwrap();
@@ -231,7 +231,7 @@ async fn rx_loop<F>(
231231
batch_size: usize,
232232
f: F,
233233
) where
234-
F: Fn(Mbuf) -> Result<()> + Send + Sync + 'static,
234+
F: Fn(Mbuf) -> Result<Postmark> + Send + Sync + 'static,
235235
{
236236
debug!(port = ?port_name, lcore = ?LcoreId::current(), "executing rx loop.");
237237

@@ -240,8 +240,18 @@ async fn rx_loop<F>(
240240

241241
loop {
242242
rxq.receive(&mut ptrs);
243+
let mut drops = vec![];
244+
243245
for ptr in ptrs.drain(..) {
244-
let _ = f(Mbuf::from_easyptr(ptr));
246+
match f(Mbuf::from_easyptr(ptr)) {
247+
Ok(Postmark::Emit) => (),
248+
Ok(Postmark::Drop(ptr)) => drops.push(ptr),
249+
Err(_) => (),
250+
}
251+
}
252+
253+
if !drops.is_empty() {
254+
Mbuf::free_bulk(drops);
245255
}
246256

247257
// cooperatively moves to the back of the execution queue,
@@ -325,7 +335,7 @@ impl PortMap {
325335
}
326336

327337
/// Returns a port iterator.
328-
pub(crate) fn iter(&self) -> impl Iterator<Item = &Port> {
338+
pub fn iter(&self) -> impl Iterator<Item = &Port> {
329339
self.0.values()
330340
}
331341

examples/kni/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ doctest = false
1818
[dependencies]
1919
anyhow = "1.0"
2020
capsule = { version = "0.1", path = "../../core" }
21-
metrics-core = "0.5"
22-
metrics-observer-yaml = "0.1"
21+
colored = "2.0"
22+
signal-hook = "0.3"
2323
tracing = "0.1"
2424
tracing-subscriber = "0.2"

examples/kni/README.md

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
# Kernel NIC interface example
22

3-
The Kernel NIC Interface (KNI) is a DPDK control plane solution that allows userspace applications to exchange packets with the Linux kernel networking stack. See DPDK's [KNI documentation](https://doc.dpdk.org/guides/prog_guide/kernel_nic_interface.html) for more information. This example is a minimum program that can forward packets to and from the Linux kernel.
3+
The Kernel NIC Interface (KNI) is a DPDK control plane solution that allows userspace applications to exchange packets with the Linux kernel networking stack. See DPDK's [KNI documentation](https://doc.dpdk.org/guides/prog_guide/kernel_nic_interface.html) for more information.
44

55
## Overview
66

7-
KNI is useful for applications that want to conceptually share the port with the Linux kernel. For example, the application may want to leverage the kernel's built-in ability to handle [ARP](https://tools.ietf.org/html/rfc826) traffic instead of implementing the protocol natively. By enabling KNI for a port, a virtual device with the same name and MAC address as the port is exposed to the Linux kernel. The kernel will be able to receive all packets that are forwarded to this virtual device and the application will receive all packets the kernel sends to it.
7+
KNI is useful for Capsule applications that want to delegate the processing of various network control plane protocols to either the Linux kernel or other implementations that run on Linux. For example, [Address Resolution Protocol](https://tools.ietf.org/html/rfc826) is the mechanism for hosts to discover each other's link layer address on an IPv4 network. Typically, the Linux kernel handles the ARP discovery for all the network interfaces on the host. However, because Capsule-bound network devices are not visible to the kernel, each Capsule application needs its own ARP implementation, otherwise the network won't be able to route packets to it. Or alternatively, an easier approach is for the application to simply leverage the kernel stack implementation by delegating and forwarding ARP packets via KNI.
8+
9+
This example demonstrates said approach by delegating the processing of [Neighbor Discovery Procotol](https://tools.ietf.org/html/rfc4861), the IPv6 equivalent of ARP, to the Linux kernel.
810

911
## Prerequisite
1012

11-
This application requires the kernel module `rte_kni`. Kernel modules are version specific. If you are using our `Vagrant` with `Docker` setup, the module is already preloaded. Otherwise, you will have to compile it by installing the kernel headers or sources required to build kernel modules on your system, then [build `DPDK` from source](https://doc.dpdk.org/guides/linux_gsg/build_dpdk.html).
13+
This application requires the kernel module `rte_kni`. Kernel modules are version specific. If you are using our Vagrant with Docker setup, the module is already preloaded. Otherwise, you will have to compile it by installing the kernel headers or sources required to build kernel modules on your system, then [build `DPDK` from source](https://doc.dpdk.org/guides/linux_gsg/build_dpdk.html).
1214

1315
Once the build is complete, load the module with command:
1416

15-
```
16-
$ sudo insmod /lib/modules/`uname -r`/extra/dpdk/rte_kni.ko
17+
```bash
18+
$ sudo insmod /lib/modules/`uname -r`/extra/dpdk/rte_kni.ko carrier=on
1719
```
1820

1921
We may provide precompiled modules for different kernel versions and Linux distributions in the future.
@@ -22,26 +24,65 @@ We may provide precompiled modules for different kernel versions and Linux distr
2224

2325
The example is located in the `examples/kni` sub-directory. To run the application,
2426

25-
```
27+
```bash
2628
/examples/kni$ cargo run -- -f kni.toml
2729
```
2830

29-
While the application is running, the new virtual device is exposed to the kernel,
31+
While the application is running, in a seperate Vagrant VM terminal, check that a new virtual device `kni0` is exposed to the kernel,
3032

33+
```bash
34+
vagrant$ ip link show dev kni0
35+
36+
14: kni0: <BROADCAST,MULTICAST> mtu 2034 qdisc noop state DOWN mode DEFAULT group default qlen 1000
37+
link/ether 6a:80:63:c3:01:42 brd ff:ff:ff:ff:ff:ff
3138
```
32-
$ ip link
3339

34-
254: kni0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
35-
link/ether ba:dc:af:eb:ee:f1 brd ff:ff:ff:ff:ff:ff
40+
Change the MAC address of `kni0` to match the MAC address of the physical interface first; then bring up the link,
41+
42+
```bash
43+
vagrant$ sudo ip link set dev kni0 address 02:00:00:ff:ff:00
44+
vagrant$ sudo ip link set dev kni0 up
3645
```
3746

38-
The kernel can assign an IP address to the device and bring the link up, at which point the kernel and the application can forward each other packets.
47+
Once `kni0` is up, it should be automatically assigned an IPv6 address, we will need this address for the next step,
3948

49+
```bash
50+
vagrant$ ip addr show dev kni0
51+
52+
14: kni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 2034 qdisc pfifo_fast state UP group default qlen 1000
53+
link/ether 02:00:00:ff:ff:00 brd ff:ff:ff:ff:ff:ff
54+
inet6 fe80::ff:feff:ff00/64 scope link
55+
valid_lft forever preferred_lft forever
4056
```
41-
$ sudo ip addr add dev kni0 10.0.2.16/24
42-
$ sudo ip link set up dev kni0
57+
58+
Finally use `socat` to read from `stdin`, and send the input messages as UDP packets to this IPv6 address. These packets will be routed to the running Capsule application,
59+
60+
```bash
61+
vagrant$ socat -d -d -u - udp6:[fe80::ff:feff:ff00%eth3]:6667
62+
63+
Hello?
64+
Is there anybody in there?
65+
```
66+
67+
The running application should print out,
68+
69+
```
70+
Mar 28 19:59:34.805 INFO kni: to kni0: Neighbor Solicitation
71+
Mar 28 19:59:34.810 INFO kni: from kni0: Neighbor Advertisement
72+
Mar 28 19:59:34.811 INFO kni: you said: Hello?
73+
74+
Mar 28 19:59:39.475 INFO kni: you said: Is there anybody in there?
75+
4376
```
4477

4578
# Explanation
4679

47-
The assigned port `0000:00:08.0` has KNI support turned on by setting the `kni` flag to `true`. To forward packets received on the port to the kernel, the application adds a simple forwarding pipeline by calling `add_pipeline_to_port`. To forward packets received from the kernel through the port, the application adds another forwarding pipeline by calling `add_kni_rx_pipeline_to_port`.
80+
Capsule leverages the KNI poll mode driver instead of the `librte_kni` API directly. This lets the application to interact with KNI the same way as any other physical or virtual port device.
81+
82+
The example is configured with one PCI port, `cap0`, and one KNI port, `kni0`. As new packets arrive through `cap0`'s rx, the application will forward all ICMPv6 packets to the `kni0`'s tx. For the sake of simplicity, it is assumed that all ICMPv6 packets received in this example will be NDP messages, and the application is delegating this link layer address discovery process to the kernel stack. In the reverse direction, kernel stack's NDP responses will come in through `kni0`'s rx, and immediately forwarded out through `cap0`'s tx without modifications. Because we assigned `cap0`'s link layer MAC address, `02:00:00:ff:ff:00`, to `kni0` with the `ip link set` command, the NDP responses already contain the correct link layer information.
83+
84+
When `socat` sends out an UDP packet via `eth3` to `kni0`'s IPv6 address `fe80::ff:feff:ff00`, a lookup is performed trying to find the link layer address of the destination. On the very first attempt, that link layer address is not found through the lookup. A neighbor solicitation message is broadcasted instead to initiate the discovery process.
85+
86+
`cap0` receives the broadcasted neighbor solicitation message and forwards it to the kernel stack via `kni0`. Kernel responds with a neighbor advertisement message because the IPv6 address matches the address of the `kni0` interface. This response is sent back to `eth3` through `kni0`'s rx then `cap0`'s tx, completing the discovery.
87+
88+
The link layer address from the response is cached, all UDP packets are routed to `cap0` with this lookup until the cached entry expires. The Capsule application will receive the UDP packets from `socat`. It parses and prints out the data payload.

0 commit comments

Comments
 (0)