|
1 | 1 | # AF_XDP Traffic Testing Guide |
2 | 2 |
|
3 | | -This guide explains how to test AF_XDP with **real network traffic** instead of just testing infrastructure setup. |
| 3 | +This guide explains traffic testing with AF_XDP. |
4 | 4 |
|
5 | 5 | ## Overview |
6 | 6 |
|
7 | | -The tests in `src/lib/traffic_test.zig` actually: |
8 | | -- ✓ Create virtual network interfaces (veth pairs) |
9 | | -- ✓ Inject real Ethernet frames into the network stack |
10 | | -- ✓ Receive packets via AF_XDP sockets |
11 | | -- ✓ Process packets through the pipeline |
12 | | -- ✓ Forward packets to other interfaces |
13 | | -- ✓ Verify packet counts and stats |
| 7 | +The tests in `src/lib/traffic_test.zig` do the following: |
| 8 | +- Create virtual network interfaces (veth pairs) |
| 9 | +- Inject Ethernet frames into the network stack |
| 10 | +- Receive packets via AF_XDP sockets |
| 11 | +- Process packets through the pipeline |
| 12 | +- Forward packets to other interfaces |
| 13 | +- Verify packet counts and stats |
14 | 14 |
|
15 | 15 | --- |
16 | 16 |
|
@@ -84,6 +84,64 @@ const stats = service.getStats(); |
84 | 84 | 5. Forwarder swaps them (A→B and B→A) |
85 | 85 | 6. Verifies bidirectional traffic works |
86 | 86 |
|
| 87 | +**Architecture:** |
| 88 | + |
| 89 | +``` |
| 90 | + Bidirectional L2 Forwarding Test |
| 91 | +
|
| 92 | + ┌────────────────────────────────────────────────────────────┐ |
| 93 | + │ Test Process │ |
| 94 | + │ │ |
| 95 | + │ ┌──────────────┐ ┌──────────────┐ │ |
| 96 | + │ │ Injector A │ │ Injector B │ │ |
| 97 | + │ │ (AF_PACKET) │ │ (AF_PACKET) │ │ |
| 98 | + │ └──────┬───────┘ └───────┬──────┘ │ |
| 99 | + │ │ │ │ |
| 100 | + │ │ Send packets │ Send packets |
| 101 | + │ │ via raw socket │ via raw socket |
| 102 | + │ ▼ ▼ │ |
| 103 | + └─────────┼────────────────────────────────────┼────────────┘ |
| 104 | + │ │ |
| 105 | + │ │ |
| 106 | + ┌─────────▼────────┐ ┌──────────────▼─────────┐ |
| 107 | + │ veth_fwd_a │◄─────────►│ veth_fwd_b │ |
| 108 | + │ │ Kernel │ │ |
| 109 | + │ AF_XDP Socket #1 │ veth │ AF_XDP Socket #2 │ |
| 110 | + │ │ pair │ │ |
| 111 | + └─────────┬────────┘ └──────────┬─────────────┘ |
| 112 | + │ │ |
| 113 | + │ RX: Packets from B │ RX: Packets from A |
| 114 | + │ │ |
| 115 | + ▼ ▼ |
| 116 | + ┌─────────────────────────────────────────────────────────┐ |
| 117 | + │ AF_XDP Service (Service.zig) │ |
| 118 | + │ │ |
| 119 | + │ ┌──────────────────────────────────────────────────┐ │ |
| 120 | + │ │ L2 Forwarder Pipeline │ │ |
| 121 | + │ │ │ │ |
| 122 | + │ │ RX from A ──► Process ──► TX to B │ │ |
| 123 | + │ │ │ │ |
| 124 | + │ │ RX from B ──► Process ──► TX to A │ │ |
| 125 | + │ │ │ │ |
| 126 | + │ │ (Swaps source/destination interfaces) │ │ |
| 127 | + │ └──────────────────────────────────────────────────┘ │ |
| 128 | + └─────────────────────────────────────────────────────────┘ |
| 129 | +
|
| 130 | +Flow Example: |
| 131 | + 1. Injector A sends packet → veth_fwd_a |
| 132 | + 2. AF_XDP RX on veth_fwd_a receives it |
| 133 | + 3. L2 Forwarder processes: forward to veth_fwd_b |
| 134 | + 4. AF_XDP TX on veth_fwd_b transmits it |
| 135 | + 5. Packet appears on veth_fwd_b (visible to Injector B) |
| 136 | +
|
| 137 | + (Same flow happens in reverse: B → A) |
| 138 | +
|
| 139 | +Verification: |
| 140 | + ✓ Service stats show RX packets on both interfaces |
| 141 | + ✓ Service stats show TX packets on both interfaces |
| 142 | + ✓ Confirms bidirectional forwarding works |
| 143 | +``` |
| 144 | + |
87 | 145 | --- |
88 | 146 |
|
89 | 147 | ## Running the Tests |
@@ -140,104 +198,6 @@ Service stats: |
140 | 198 |
|
141 | 199 | --- |
142 | 200 |
|
143 | | -## Key Components Explained |
144 | | - |
145 | | -### 1. Creating veth Pairs |
146 | | - |
147 | | -**Why veth instead of dummy interfaces?** |
148 | | -- Dummy interfaces don't actually pass traffic |
149 | | -- veth pairs are connected: packet sent to one end appears on the other |
150 | | -- Perfect for testing without physical NICs |
151 | | - |
152 | | -```zig |
153 | | -fn createVethPair(name_a: []const u8, name_b: []const u8) !void { |
154 | | - // Runs: ip link add veth_a type veth peer name veth_b |
155 | | - const argv = [_][]const u8{ |
156 | | - "ip", "link", "add", name_a, "type", "veth", "peer", "name", name_b, |
157 | | - }; |
158 | | - var child = std.process.Child.init(&argv, std.heap.page_allocator); |
159 | | - const result = try child.spawnAndWait(); |
160 | | - if (result != .Exited or result.Exited != 0) { |
161 | | - return error.FailedToCreateVethPair; |
162 | | - } |
163 | | -} |
164 | | -``` |
165 | | - |
166 | | -### 2. Packet Injection |
167 | | - |
168 | | -**Uses AF_PACKET raw sockets** to inject Ethernet frames: |
169 | | - |
170 | | -```zig |
171 | | -fn injectPacket(ifname: []const u8, packet_data: []const u8) !void { |
172 | | - // Create raw socket |
173 | | - const sock_fd = try std.posix.socket( |
174 | | - std.posix.AF.PACKET, |
175 | | - std.posix.SOCK.RAW, |
176 | | - @byteSwap(@as(u16, 0x0003)), // ETH_P_ALL |
177 | | - ); |
178 | | - defer std.posix.close(sock_fd); |
179 | | -
|
180 | | - // Bind to interface |
181 | | - var addr = std.mem.zeroes(std.posix.sockaddr.ll); |
182 | | - addr.sll_family = std.posix.AF.PACKET; |
183 | | - addr.sll_ifindex = @intCast(ifindex); |
184 | | -
|
185 | | - // Send raw Ethernet frame |
186 | | - _ = try std.posix.sendto(sock_fd, packet_data, 0, @ptrCast(&addr), @sizeOf(@TypeOf(addr))); |
187 | | -} |
188 | | -``` |
189 | | - |
190 | | -**Why this works:** |
191 | | -- AF_PACKET socket operates at Layer 2 (Ethernet) |
192 | | -- Can send raw frames directly to network interface |
193 | | -- Bypasses normal network stack (no routing, no TCP/IP processing) |
194 | | -- Frame appears on wire (or veth peer) exactly as sent |
195 | | - |
196 | | -### 3. Test Packet Structure |
197 | | - |
198 | | -```zig |
199 | | -fn buildTestPacket(buffer: []u8, src_mac: [6]u8, dst_mac: [6]u8, payload_byte: u8) []u8 { |
200 | | - // Ethernet: 14 bytes |
201 | | - @memcpy(buffer[0..6], &dst_mac); |
202 | | - @memcpy(buffer[6..12], &src_mac); |
203 | | - std.mem.writeInt(u16, buffer[12..14], 0x0800, .big); // EtherType: IPv4 |
204 | | -
|
205 | | - // IPv4: 20 bytes |
206 | | - buffer[14] = 0x45; // version=4, ihl=5 |
207 | | - // ... (see code for full header) |
208 | | -
|
209 | | - // UDP: 8 bytes |
210 | | - std.mem.writeInt(u16, buffer[34..36], 12345, .big); // src port |
211 | | - std.mem.writeInt(u16, buffer[36..38], 53, .big); // dst port |
212 | | -
|
213 | | - // Payload: 20 bytes |
214 | | - @memset(buffer[42..62], payload_byte); |
215 | | -
|
216 | | - return buffer[0..62]; // Total: 62 bytes |
217 | | -} |
218 | | -``` |
219 | | - |
220 | | -### 4. Service Thread |
221 | | - |
222 | | -**Why run service in separate thread?** |
223 | | - |
224 | | -```zig |
225 | | -var service_thread = try std.Thread.spawn(.{}, serviceRunThread, .{&service}); |
226 | | -
|
227 | | -// Main thread injects packets while service thread receives them |
228 | | -injectPacket(...); |
229 | | -
|
230 | | -// Later: stop and wait |
231 | | -service.stop(); |
232 | | -service_thread.join(); |
233 | | -``` |
234 | | - |
235 | | -Without a separate thread, you'd have a chicken-and-egg problem: |
236 | | -- Can't inject packets until service is running |
237 | | -- Can't run service (blocking poll) while injecting packets |
238 | | - |
239 | | ---- |
240 | | - |
241 | 201 | ## Common Issues and Solutions |
242 | 202 |
|
243 | 203 | ### Issue 1: "Permission Denied" / PERM Error |
|
0 commit comments