Skip to content

Commit bc41309

Browse files
feat: Add IPv6 support for ingress and egress firewall rules
This commit implements dual-stack IPv6/IPv4 support with separate BPF maps for each IP version, allowing both IPv4 and IPv6 rules to coexist and be processed efficiently. Userspace changes: - models/rule.rs: Add IpAddr enum (V4/V6) support to PolicyRule - bpf_user/maps.rs: Implement dual-stack BPF map management * Add RuleEntryV6 and RuleMetadataV6 structs (matching C layout) * Create separate IPv6 maps (rules_v6, metadata_v6, rule_stats_v6) * Update list_all_metadata() to query both IPv4 and IPv6 maps * Update get_rule_metadata() to search both maps, return PolicyRule * Update delete_rule_metadata() to handle both IP versions * Fix struct sizes: RuleEntryV6=28 bytes, RuleMetadataV6=216 bytes - policy/parser.rs: Add IPv6 CIDR parsing and validation - state.rs: Update for dual-stack PolicyRule handling - tests: Add 5 new IPv6-specific test cases (111 total tests passing) - example-ipv6-policy.yaml: Add example policy with 10 IPv6 rules Kernel-side (BPF) changes: - bpf/firebee_common.h: Define IPv6 structures and maps * Add rule_entry_v6, rule_metadata_v6, rule_stats_v6 structs * Create rules_v6_map, metadata_v6_map, rule_stats_v6_map * Add MAX_ACTIVE_RULES=128 (separate from MAX_RULES=1024 storage) to avoid BPF verifier 1M instruction limit - bpf/firebee_helpers.h: Implement ipv6_matches() function * Use explicit array indexing (no loops) to avoid variable-offset stack reads that trigger BPF verifier errors * Support IPv6 CIDR prefix matching with /0 to /128 ranges - bpf/firebee.bpf.c: Add XDP ingress IPv6 packet processing * Parse IPv6 headers and extract src/dst addresses * Check both IPv4 and IPv6 rule sets * Support ICMPv6 (protocol 58) in addition to TCP/UDP/ICMP - bpf/firebee_egress.bpf.c: Add TC egress IPv6 packet processing * Mirror ingress implementation for egress direction * Track statistics in rule_stats_v6_map Signed-off-by: saiaunghlyanhtet <saiaunghlyanhtet2003@gmail.com>
1 parent 61b2881 commit bc41309

10 files changed

Lines changed: 1184 additions & 155 deletions

File tree

src/bpf/firebee.bpf.c

Lines changed: 190 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <linux/bpf.h>
99
#include <linux/if_ether.h>
1010
#include <linux/ip.h>
11+
#include <linux/ipv6.h>
1112
#include <linux/tcp.h>
1213
#include <linux/udp.h>
1314
#include <bpf/bpf_helpers.h>
@@ -17,6 +18,7 @@
1718
#include "firebee_helpers.h"
1819

1920
#define ETH_P_IP 0x0800
21+
#define ETH_P_IPV6 0x86DD
2022

2123
/* Maps are now defined in firebee_common.h and shared with TC program */
2224

@@ -57,6 +59,39 @@ static __always_inline void extract_ports(
5759
}
5860
}
5961

62+
/*
63+
* Extract transport layer ports from IPv6 packet
64+
* Only applies to TCP and UDP protocols
65+
*/
66+
static __always_inline void extract_ports_v6(
67+
struct ipv6hdr *ip6h,
68+
void *data_end,
69+
__u8 protocol,
70+
__u16 *src_port,
71+
__u16 *dst_port
72+
) {
73+
*src_port = 0;
74+
*dst_port = 0;
75+
76+
void *l4_hdr = (void *)(ip6h + 1);
77+
78+
if (protocol == IPPROTO_TCP) {
79+
struct tcphdr *tcph = l4_hdr;
80+
if ((void *)(tcph + 1) > data_end) {
81+
return;
82+
}
83+
*src_port = bpf_ntohs(tcph->source);
84+
*dst_port = bpf_ntohs(tcph->dest);
85+
} else if (protocol == IPPROTO_UDP) {
86+
struct udphdr *udph = l4_hdr;
87+
if ((void *)(udph + 1) > data_end) {
88+
return;
89+
}
90+
*src_port = bpf_ntohs(udph->source);
91+
*dst_port = bpf_ntohs(udph->dest);
92+
}
93+
}
94+
6095
/*
6196
* Log packet event to ring buffer for userspace
6297
*/
@@ -134,9 +169,9 @@ static __always_inline __u8 find_matching_rule(
134169
__u32 i;
135170
*rule_index = (__u32)-1; /* No match initially */
136171

137-
/* Bounded loop for BPF verifier compliance */
172+
/* Bounded loop for BPF verifier compliance - only check first MAX_ACTIVE_RULES */
138173
#pragma unroll
139-
for (i = 0; i < MAX_RULES; i++) {
174+
for (i = 0; i < MAX_ACTIVE_RULES; i++) {
140175
__u32 key = i;
141176
struct rule_entry *rule = bpf_map_lookup_elem(&rules_map, &key);
142177

@@ -154,6 +189,83 @@ static __always_inline __u8 find_matching_rule(
154189
return action;
155190
}
156191

192+
/*
193+
* Check if IPv6 packet matches a specific rule
194+
* Returns 1 if all conditions match, 0 otherwise
195+
*/
196+
static __always_inline int rule_matches_v6(
197+
struct rule_entry_v6 *rule,
198+
__u32 packet_ip[4],
199+
__u8 protocol,
200+
__u16 src_port,
201+
__u16 dst_port,
202+
__u8 packet_direction
203+
) {
204+
/* Check direction - DIRECTION_BOTH matches all traffic */
205+
if (rule->direction != DIRECTION_BOTH && rule->direction != packet_direction) {
206+
return 0;
207+
}
208+
209+
/* Check IPv6 address with prefix length */
210+
if (!ipv6_matches(packet_ip, rule->src_ip, rule->prefix_len)) {
211+
return 0;
212+
}
213+
214+
/* Check protocol */
215+
if (!protocol_matches(protocol, rule->protocol)) {
216+
return 0;
217+
}
218+
219+
/* Check ports (only for TCP/UDP) */
220+
if (protocol == IPPROTO_TCP || protocol == IPPROTO_UDP) {
221+
if (!port_matches(src_port, rule->src_port)) {
222+
return 0;
223+
}
224+
if (!port_matches(dst_port, rule->dst_port)) {
225+
return 0;
226+
}
227+
}
228+
229+
return 1;
230+
}
231+
232+
/*
233+
* Find matching IPv6 rule for packet and update statistics
234+
* Returns action (ACTION_ALLOW or ACTION_DROP)
235+
* First matching rule wins
236+
*/
237+
static __always_inline __u8 find_matching_rule_v6(
238+
__u32 packet_ip[4],
239+
__u8 protocol,
240+
__u16 src_port,
241+
__u16 dst_port,
242+
__u8 packet_direction,
243+
__u32 *rule_index
244+
) {
245+
__u8 action = ACTION_ALLOW; /* Default: allow */
246+
__u32 i;
247+
*rule_index = (__u32)-1; /* No match initially */
248+
249+
/* Bounded loop for BPF verifier compliance - only check first MAX_ACTIVE_RULES */
250+
#pragma unroll
251+
for (i = 0; i < MAX_ACTIVE_RULES; i++) {
252+
__u32 key = i;
253+
struct rule_entry_v6 *rule = bpf_map_lookup_elem(&rules_v6_map, &key);
254+
255+
if (!rule || !rule->valid) {
256+
continue;
257+
}
258+
259+
if (rule_matches_v6(rule, packet_ip, protocol, src_port, dst_port, packet_direction)) {
260+
action = rule->action;
261+
*rule_index = i;
262+
break; /* First match wins */
263+
}
264+
}
265+
266+
return action;
267+
}
268+
157269
/* ========================================================================
158270
* XDP Main Program
159271
* ======================================================================== */
@@ -169,45 +281,89 @@ int xdp_firewall(struct xdp_md *ctx) {
169281
return XDP_PASS;
170282
}
171283

172-
/* Only process IPv4 packets */
173-
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
174-
return XDP_PASS;
175-
}
176-
177-
/* Parse IP header */
178-
struct iphdr *iph = (void *)(eth + 1);
179-
if ((void *)(iph + 1) > data_end) {
180-
return XDP_PASS;
181-
}
284+
__u16 eth_proto = bpf_ntohs(eth->h_proto);
182285

183-
/* Extract packet information */
184-
__u32 packet_ip = bpf_ntohl(iph->saddr);
185-
__u8 protocol = iph->protocol;
186-
__u16 src_port, dst_port;
187-
__u32 matched_rule_idx;
188-
__u8 packet_direction = DIRECTION_INGRESS; /* XDP only sees ingress traffic */
189-
190-
extract_ports(iph, data_end, protocol, &src_port, &dst_port);
286+
/* Handle IPv4 packets */
287+
if (eth_proto == ETH_P_IP) {
288+
/* Parse IP header */
289+
struct iphdr *iph = (void *)(eth + 1);
290+
if ((void *)(iph + 1) > data_end) {
291+
return XDP_PASS;
292+
}
293+
294+
/* Extract packet information */
295+
__u32 packet_ip = bpf_ntohl(iph->saddr);
296+
__u8 protocol = iph->protocol;
297+
__u16 src_port, dst_port;
298+
__u32 matched_rule_idx;
299+
__u8 packet_direction = DIRECTION_INGRESS;
300+
301+
extract_ports(iph, data_end, protocol, &src_port, &dst_port);
191302

192-
/* Find matching firewall rule */
193-
__u8 action = find_matching_rule(packet_ip, protocol, src_port, dst_port, packet_direction, &matched_rule_idx);
303+
/* Find matching firewall rule */
304+
__u8 action = find_matching_rule(packet_ip, protocol, src_port, dst_port, packet_direction, &matched_rule_idx);
194305

195-
/* Update statistics if a rule matched */
196-
if (matched_rule_idx != (__u32)-1) {
197-
struct rule_stats *stats = bpf_map_lookup_elem(&rule_stats_map, &matched_rule_idx);
198-
if (stats) {
199-
/* Calculate packet size */
200-
__u64 packet_size = (__u64)(data_end - data);
201-
__sync_fetch_and_add(&stats->packets, 1);
202-
__sync_fetch_and_add(&stats->bytes, packet_size);
306+
/* Update statistics if a rule matched */
307+
if (matched_rule_idx != (__u32)-1) {
308+
struct rule_stats *stats = bpf_map_lookup_elem(&rule_stats_map, &matched_rule_idx);
309+
if (stats) {
310+
__u64 packet_size = (__u64)(data_end - data);
311+
__sync_fetch_and_add(&stats->packets, 1);
312+
__sync_fetch_and_add(&stats->bytes, packet_size);
313+
}
203314
}
315+
316+
/* Log the event */
317+
log_packet(packet_ip, action);
318+
319+
/* Apply action */
320+
return (action == ACTION_DROP) ? XDP_DROP : XDP_PASS;
204321
}
322+
/* Handle IPv6 packets */
323+
else if (eth_proto == ETH_P_IPV6) {
324+
/* Parse IPv6 header */
325+
struct ipv6hdr *ip6h = (void *)(eth + 1);
326+
if ((void *)(ip6h + 1) > data_end) {
327+
return XDP_PASS;
328+
}
329+
330+
/* Extract IPv6 source address (already in network byte order) */
331+
__u32 packet_ip[4];
332+
packet_ip[0] = ip6h->saddr.in6_u.u6_addr32[0];
333+
packet_ip[1] = ip6h->saddr.in6_u.u6_addr32[1];
334+
packet_ip[2] = ip6h->saddr.in6_u.u6_addr32[2];
335+
packet_ip[3] = ip6h->saddr.in6_u.u6_addr32[3];
205336

206-
/* Log the event */
207-
log_packet(packet_ip, action);
337+
/* Extract protocol from next header */
338+
__u8 protocol = ip6h->nexthdr;
339+
__u16 src_port, dst_port;
340+
__u32 matched_rule_idx;
341+
__u8 packet_direction = DIRECTION_INGRESS;
342+
343+
extract_ports_v6(ip6h, data_end, protocol, &src_port, &dst_port);
344+
345+
/* Find matching IPv6 firewall rule */
346+
__u8 action = find_matching_rule_v6(packet_ip, protocol, src_port, dst_port, packet_direction, &matched_rule_idx);
347+
348+
/* Update IPv6 statistics if a rule matched */
349+
if (matched_rule_idx != (__u32)-1) {
350+
struct rule_stats *stats = bpf_map_lookup_elem(&rule_stats_v6_map, &matched_rule_idx);
351+
if (stats) {
352+
__u64 packet_size = (__u64)(data_end - data);
353+
__sync_fetch_and_add(&stats->packets, 1);
354+
__sync_fetch_and_add(&stats->bytes, packet_size);
355+
}
356+
}
357+
358+
/* Log the event (using first 32-bit word for compatibility) */
359+
log_packet(bpf_ntohl(packet_ip[0]), action);
360+
361+
/* Apply action */
362+
return (action == ACTION_DROP) ? XDP_DROP : XDP_PASS;
363+
}
208364

209-
/* Apply action */
210-
return (action == ACTION_DROP) ? XDP_DROP : XDP_PASS;
365+
/* Pass all other protocols */
366+
return XDP_PASS;
211367
}
212368

213369
char _license[] SEC("license") = "GPL";

src/bpf/firebee_common.h

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#define IPPROTO_TCP 6
88
#define IPPROTO_UDP 17
99
#define IPPROTO_ICMP 1
10+
#define IPPROTO_ICMPV6 58
1011
#define IPPROTO_ANY 255
1112

1213
/* Port wildcard */
@@ -84,13 +85,56 @@ struct rule_stats {
8485
__u64 bytes; /* Total bytes matched */
8586
};
8687

88+
/* ========================================================================
89+
* IPv6 Support Structures
90+
* ======================================================================== */
91+
92+
/*
93+
* IPv6 Rule entry structure for the BPF array map
94+
* Similar to rule_entry but with IPv6 addresses (128-bit)
95+
*/
96+
struct rule_entry_v6 {
97+
__u32 src_ip[4]; /* Source IPv6 address (network byte order, 128-bit) */
98+
__u8 prefix_len; /* Prefix length for CIDR matching (0-128) */
99+
__u8 protocol; /* IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMPV6, or IPPROTO_ANY */
100+
__u8 action; /* ACTION_ALLOW or ACTION_DROP */
101+
__u8 direction; /* DIRECTION_INGRESS, DIRECTION_EGRESS, or DIRECTION_BOTH */
102+
__u8 valid; /* 1 = entry is valid, 0 = empty slot */
103+
__u8 _padding[3]; /* Padding for alignment */
104+
__u16 src_port; /* Source port (PORT_ANY for wildcard) */
105+
__u16 dst_port; /* Destination port (PORT_ANY for wildcard) */
106+
};
107+
108+
/*
109+
* IPv6 Rule metadata structure
110+
* Stores human-readable information about IPv6 rules
111+
*/
112+
struct rule_metadata_v6 {
113+
__u32 ip[4];
114+
__u8 prefix_len;
115+
__u8 action;
116+
__u8 protocol;
117+
__u8 direction;
118+
__u16 src_port;
119+
__u16 dst_port;
120+
char name[64];
121+
char description[128];
122+
};
123+
87124
/* ========================================================================
88125
* Shared BPF Maps - Defined once and pinned for use across programs
89126
* These maps are shared between XDP and TC-BPF programs via pinning
90127
* ======================================================================== */
91128

129+
/* Maximum number of firewall rules (map storage capacity) */
92130
#define MAX_RULES 1024
93131

132+
/* Maximum number of active rules to check per packet (loop bound for BPF verifier)
133+
* This must be small enough to keep the BPF program under 1M instruction limit.
134+
* The BPF verifier unrolls loops, so 128 rules × ~99 instructions ≈ 12.6K instructions
135+
* per loop, which leaves plenty of headroom under the 1M limit. */
136+
#define MAX_ACTIVE_RULES 128
137+
94138
/*
95139
* Primary rules storage - Array map for efficient iteration
96140
* Shared between XDP (ingress) and TC (egress) programs
@@ -150,4 +194,44 @@ struct {
150194
__uint(pinning, LIBBPF_PIN_BY_NAME);
151195
} rules_index SEC(".maps");
152196

197+
/* ========================================================================
198+
* IPv6 BPF Maps - Shared between XDP and TC programs
199+
* ======================================================================== */
200+
201+
/*
202+
* Primary IPv6 rules storage - Array map for efficient iteration
203+
* Shared between XDP (ingress) and TC (egress) programs
204+
*/
205+
struct {
206+
__uint(type, BPF_MAP_TYPE_ARRAY);
207+
__type(key, __u32);
208+
__type(value, struct rule_entry_v6);
209+
__uint(max_entries, MAX_RULES);
210+
__uint(pinning, LIBBPF_PIN_BY_NAME);
211+
} rules_v6_map SEC(".maps");
212+
213+
/*
214+
* IPv6 Rule metadata - Hash map indexed by rule name
215+
* Stores human-readable information (name, description)
216+
*/
217+
struct {
218+
__uint(type, BPF_MAP_TYPE_HASH);
219+
__type(key, char[64]);
220+
__type(value, struct rule_metadata_v6);
221+
__uint(max_entries, MAX_RULES);
222+
__uint(pinning, LIBBPF_PIN_BY_NAME);
223+
} rule_metadata_v6_map SEC(".maps");
224+
225+
/*
226+
* IPv6 Rule statistics - Array map for per-rule packet/byte counters
227+
* Indexed by rule array index, parallel to rules_v6_map
228+
*/
229+
struct {
230+
__uint(type, BPF_MAP_TYPE_ARRAY);
231+
__type(key, __u32);
232+
__type(value, struct rule_stats);
233+
__uint(max_entries, MAX_RULES);
234+
__uint(pinning, LIBBPF_PIN_BY_NAME);
235+
} rule_stats_v6_map SEC(".maps");
236+
153237
#endif /* __FIREBEE_COMMON_H__ */

0 commit comments

Comments
 (0)