Skip to content

Commit 3663e46

Browse files
committed
[Feature] Implement eBPF kernel programs and userspace integration (#682, #683, #684, #685)
Phase 2 of the Rust dataplane migration: implement the three eBPF kernel programs and wire up their userspace integration. eBPF programs (novaedge-ebpf): - XDP L4 Load Balancer: VIP lookup, conntrack, backend selection via 5-tuple hash, DNAT rewrite with IP/L4 checksum updates, XDP_TX - XDP ARP Responder: responds to ARP requests for VIP addresses using VIP_ADDRS map, generates ARP replies in-place - TC Rate Limiter: per-source-IP token bucket algorithm with configurable rate/burst, emits flow events on drops Shared types (novaedge-common): - Add RateLimitCfg struct for per-IP rate limit configuration - Add Pod impl for RateLimitCfg (Linux userspace) Userspace integration (novaedge-dataplane): - maps.rs: implement RealMaps with actual aya map types (vips, backends, conntrack, rate_limits, rate_limit_cfg, vip_addrs) - loader.rs: extract real map handles after loading, add attach_xdp and attach_tc helpers, return flow ring buffer handle - flows.rs: add run_flow_reader_real() using AsyncFd for ring buffer polling on Linux - server.rs: formatting improvements from cargo fmt
1 parent be9e859 commit 3663e46

6 files changed

Lines changed: 1103 additions & 139 deletions

File tree

dataplane/novaedge-common/src/lib.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ pub struct RateLimitKey {
105105
pub src_ip: u32,
106106
}
107107

108+
/// Rate limiting configuration (per source IP).
109+
#[repr(C)]
110+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111+
pub struct RateLimitCfg {
112+
/// Tokens per second (refill rate).
113+
pub rate: u64,
114+
/// Maximum tokens (bucket capacity).
115+
pub burst: u64,
116+
}
117+
108118
/// Rate limiting value (token bucket state).
109119
#[repr(C)]
110120
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -159,6 +169,7 @@ impl_pod!(
159169
ConnTrackKey,
160170
ConnTrackValue,
161171
RateLimitKey,
172+
RateLimitCfg,
162173
RateLimitValue,
163174
FlowEvent,
164175
);
@@ -203,6 +214,11 @@ mod tests {
203214
assert_eq!(mem::size_of::<RateLimitKey>(), 4);
204215
}
205216

217+
#[test]
218+
fn test_rate_limit_cfg_size() {
219+
assert_eq!(mem::size_of::<RateLimitCfg>(), 16);
220+
}
221+
206222
#[test]
207223
fn test_rate_limit_value_size() {
208224
assert_eq!(mem::size_of::<RateLimitValue>(), 16);
@@ -220,15 +236,24 @@ mod tests {
220236

221237
#[test]
222238
fn test_vip_key_fields() {
223-
let key = VipKey { vip: 0x0A000001, port: 80, protocol: 6, _pad: 0 };
239+
let key = VipKey {
240+
vip: 0x0A000001,
241+
port: 80,
242+
protocol: 6,
243+
_pad: 0,
244+
};
224245
assert_eq!(key.vip, 0x0A000001);
225246
assert_eq!(key.port, 80);
226247
assert_eq!(key.protocol, 6);
227248
}
228249

229250
#[test]
230251
fn test_backend_value_fields() {
231-
let val = BackendValue { addr: 0x0A000002, port: 8080, weight: 100 };
252+
let val = BackendValue {
253+
addr: 0x0A000002,
254+
port: 8080,
255+
weight: 100,
256+
};
232257
assert_eq!(val.addr, 0x0A000002);
233258
assert_eq!(val.port, 8080);
234259
assert_eq!(val.weight, 100);
@@ -260,7 +285,12 @@ mod tests {
260285

261286
#[test]
262287
fn test_default_values() {
263-
let key = VipKey { vip: 0, port: 0, protocol: 0, _pad: 0 };
288+
let key = VipKey {
289+
vip: 0,
290+
port: 0,
291+
protocol: 0,
292+
_pad: 0,
293+
};
264294
assert_eq!(key.vip, 0);
265295
}
266296
}

dataplane/novaedge-dataplane/src/flows.rs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use std::net::Ipv4Addr;
88
use tokio::sync::broadcast;
99
use tracing::{debug, info};
1010

11+
#[cfg(target_os = "linux")]
12+
use tracing::warn;
13+
1114
use crate::proto;
1215

1316
/// Capacity of the broadcast channel for flow events.
@@ -18,7 +21,10 @@ const FLOW_CHANNEL_CAPACITY: usize = 4096;
1821
/// Returns `(sender, receiver)`. The sender is cloneable and should be passed
1922
/// to the flow reader task and the gRPC service. Additional receivers can be
2023
/// created via `sender.subscribe()`.
21-
pub fn flow_channel() -> (broadcast::Sender<proto::FlowEvent>, broadcast::Receiver<proto::FlowEvent>) {
24+
pub fn flow_channel() -> (
25+
broadcast::Sender<proto::FlowEvent>,
26+
broadcast::Receiver<proto::FlowEvent>,
27+
) {
2228
broadcast::channel(FLOW_CHANNEL_CAPACITY)
2329
}
2430

@@ -80,6 +86,54 @@ impl MockFlowInjector {
8086
}
8187
}
8288

89+
/// Start the real flow reader on Linux, reading from the eBPF ring buffer.
90+
///
91+
/// This uses `AsyncFd` to efficiently wait for ring buffer data availability,
92+
/// then reads and broadcasts each event.
93+
#[cfg(target_os = "linux")]
94+
pub async fn run_flow_reader_real(
95+
mut ring_buf: aya::maps::RingBuf<aya::maps::MapData>,
96+
tx: broadcast::Sender<proto::FlowEvent>,
97+
) {
98+
use std::os::fd::AsRawFd;
99+
use tokio::io::unix::AsyncFd;
100+
101+
info!("Flow reader: starting eBPF ring buffer reader");
102+
103+
let fd = ring_buf.as_raw_fd();
104+
let async_fd = match AsyncFd::new(fd) {
105+
Ok(afd) => afd,
106+
Err(e) => {
107+
warn!("Flow reader: failed to create AsyncFd: {e}");
108+
return;
109+
}
110+
};
111+
112+
loop {
113+
// Wait for the ring buffer to become readable.
114+
let mut guard = match async_fd.readable().await {
115+
Ok(g) => g,
116+
Err(e) => {
117+
warn!("Flow reader: readable() error: {e}");
118+
break;
119+
}
120+
};
121+
122+
// Drain all available events.
123+
while let Some(event_data) = ring_buf.next() {
124+
if event_data.len() < core::mem::size_of::<novaedge_common::FlowEvent>() {
125+
continue;
126+
}
127+
let raw: &novaedge_common::FlowEvent =
128+
unsafe { &*(event_data.as_ptr() as *const novaedge_common::FlowEvent) };
129+
let proto_event = raw_to_proto(raw);
130+
let _ = tx.send(proto_event);
131+
}
132+
133+
guard.clear_ready();
134+
}
135+
}
136+
83137
/// Start the flow reader task.
84138
///
85139
/// On Linux with a real ring buffer, this would read from the eBPF ring buffer
@@ -88,10 +142,7 @@ impl MockFlowInjector {
88142
///
89143
/// Flow events are broadcast via the sender. The gRPC `StreamFlows` RPC
90144
/// subscribes to this channel to deliver events to clients.
91-
pub async fn run_flow_reader(
92-
tx: broadcast::Sender<proto::FlowEvent>,
93-
mock_mode: bool,
94-
) {
145+
pub async fn run_flow_reader(tx: broadcast::Sender<proto::FlowEvent>, mock_mode: bool) {
95146
if mock_mode {
96147
info!("Flow reader running in mock mode (no eBPF ring buffer)");
97148
// In mock mode, we just keep the task alive. Events can be injected
@@ -101,22 +152,12 @@ pub async fn run_flow_reader(
101152
return;
102153
}
103154

104-
// On Linux with real eBPF, we would read from the ring buffer here.
105-
// This is stubbed for Phase 2 when eBPF programs are implemented.
155+
// On Linux with real eBPF, the ring buffer reader is started via
156+
// `run_flow_reader_real()` with the actual RingBuf handle.
157+
// If we reach here in non-mock mode without a ring buffer, just wait.
106158
#[cfg(target_os = "linux")]
107159
{
108-
info!("Flow reader: eBPF ring buffer reader not yet implemented");
109-
// TODO(Phase 2): Use aya::maps::RingBuf with AsyncFd to poll events.
110-
// let async_fd = AsyncFd::new(ring_buf_fd)?;
111-
// loop {
112-
// let guard = async_fd.readable().await?;
113-
// while let Some(event) = ring_buf.next() {
114-
// let raw = parse_flow_event(event);
115-
// let proto_event = raw_to_proto(&raw);
116-
// let _ = tx.send(proto_event);
117-
// }
118-
// guard.clear_ready();
119-
// }
160+
info!("Flow reader: waiting for ring buffer (use run_flow_reader_real for eBPF)");
120161
let _ = std::future::pending::<()>().await;
121162
}
122163

dataplane/novaedge-dataplane/src/loader.rs

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,121 @@ pub struct LoadResult {
1212
pub map_manager: MapManager,
1313
/// Whether real eBPF programs were loaded (vs mock fallback).
1414
pub is_real: bool,
15+
/// Ring buffer for flow events (Linux only, None in mock mode).
16+
#[cfg(target_os = "linux")]
17+
pub flow_ring_buf: Option<aya::maps::RingBuf<aya::maps::MapData>>,
1518
}
1619

1720
/// Load eBPF programs from the compiled object file and return a `LoadResult`.
21+
///
22+
/// Extracts all map handles (VIPs, backends, conntrack, rate limits, VIP
23+
/// addresses), attaches the XDP and TC programs, and returns a
24+
/// `MapManager` wrapping the real maps plus the flow-event ring buffer.
1825
#[cfg(target_os = "linux")]
1926
pub fn load_ebpf(path: &str) -> anyhow::Result<LoadResult> {
27+
use aya::programs::{tc, SchedClassifier, TcAttachType, Xdp, XdpFlags};
2028
use aya::Ebpf;
2129
use tracing::info;
2230

31+
use crate::maps::RealMaps;
32+
2333
info!(path = %path, "Loading eBPF programs");
24-
let _bpf = Ebpf::load_file(path)?;
34+
let mut bpf = Ebpf::load_file(path)?;
35+
36+
// ── Extract map handles ───────────────────────────────────────────
37+
let vips = aya::maps::HashMap::try_from(
38+
bpf.take_map("VIPS")
39+
.ok_or_else(|| anyhow::anyhow!("map VIPS not found"))?,
40+
)?;
41+
let backends = aya::maps::HashMap::try_from(
42+
bpf.take_map("BACKENDS")
43+
.ok_or_else(|| anyhow::anyhow!("map BACKENDS not found"))?,
44+
)?;
45+
let conntrack = aya::maps::HashMap::try_from(
46+
bpf.take_map("CONNTRACK")
47+
.ok_or_else(|| anyhow::anyhow!("map CONNTRACK not found"))?,
48+
)?;
49+
let rate_limits = aya::maps::HashMap::try_from(
50+
bpf.take_map("RATE_LIMIT_STATE")
51+
.ok_or_else(|| anyhow::anyhow!("map RATE_LIMIT_STATE not found"))?,
52+
)?;
53+
let rate_limit_cfg = aya::maps::HashMap::try_from(
54+
bpf.take_map("RATE_LIMIT_CFG")
55+
.ok_or_else(|| anyhow::anyhow!("map RATE_LIMIT_CFG not found"))?,
56+
)?;
57+
let vip_addrs = aya::maps::HashMap::try_from(
58+
bpf.take_map("VIP_ADDRS")
59+
.ok_or_else(|| anyhow::anyhow!("map VIP_ADDRS not found"))?,
60+
)?;
61+
let flow_ring_buf = aya::maps::RingBuf::try_from(
62+
bpf.take_map("FLOW_EVENTS")
63+
.ok_or_else(|| anyhow::anyhow!("map FLOW_EVENTS not found"))?,
64+
)?;
65+
66+
info!("eBPF maps extracted successfully");
67+
68+
let maps = RealMaps {
69+
vips,
70+
backends,
71+
conntrack,
72+
rate_limits,
73+
rate_limit_cfg,
74+
vip_addrs,
75+
};
2576

26-
// TODO(Phase 2): Extract map handles and attach XDP/TC programs.
27-
info!("eBPF programs loaded (maps not yet extracted, using mock)");
2877
Ok(LoadResult {
29-
map_manager: MapManager::new_mock(),
30-
is_real: false,
78+
map_manager: MapManager::new_real(maps),
79+
is_real: true,
80+
flow_ring_buf: Some(flow_ring_buf),
3181
})
3282
}
3383

84+
/// Attach the XDP programs to the specified network interface.
85+
///
86+
/// This is called separately from `load_ebpf` so the caller can choose
87+
/// when and to which interface to attach.
88+
#[cfg(target_os = "linux")]
89+
pub fn attach_xdp(bpf: &mut aya::Ebpf, program_name: &str, interface: &str) -> anyhow::Result<()> {
90+
use aya::programs::{Xdp, XdpFlags};
91+
use tracing::info;
92+
93+
let prog: &mut Xdp = bpf
94+
.program_mut(program_name)
95+
.ok_or_else(|| anyhow::anyhow!("XDP program '{program_name}' not found"))?
96+
.try_into()?;
97+
prog.load()?;
98+
prog.attach(interface, XdpFlags::default())?;
99+
info!(
100+
program = program_name,
101+
interface = interface,
102+
"XDP program attached"
103+
);
104+
Ok(())
105+
}
106+
107+
/// Attach the TC classifier program to the specified network interface.
108+
#[cfg(target_os = "linux")]
109+
pub fn attach_tc(bpf: &mut aya::Ebpf, program_name: &str, interface: &str) -> anyhow::Result<()> {
110+
use aya::programs::{tc, SchedClassifier, TcAttachType};
111+
use tracing::info;
112+
113+
// Add clsact qdisc (ignore error if already exists)
114+
let _ = tc::qdisc_add_clsact(interface);
115+
116+
let prog: &mut SchedClassifier = bpf
117+
.program_mut(program_name)
118+
.ok_or_else(|| anyhow::anyhow!("TC program '{program_name}' not found"))?
119+
.try_into()?;
120+
prog.load()?;
121+
prog.attach(interface, TcAttachType::Ingress)?;
122+
info!(
123+
program = program_name,
124+
interface = interface,
125+
"TC program attached"
126+
);
127+
Ok(())
128+
}
129+
34130
/// Stub for non-Linux platforms.
35131
#[cfg(not(target_os = "linux"))]
36132
pub fn load_ebpf(_path: &str) -> anyhow::Result<LoadResult> {
@@ -46,6 +142,8 @@ mod tests {
46142
let result = LoadResult {
47143
map_manager: MapManager::new_mock(),
48144
is_real: false,
145+
#[cfg(target_os = "linux")]
146+
flow_ring_buf: None,
49147
};
50148
assert!(!result.is_real);
51149
assert_eq!(result.map_manager.mode(), "mock");

0 commit comments

Comments
 (0)