From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox v4 03/45] ebpf: policy: add a check for non-first fragments
Date: Thu, 5 Nov 2026 12:12:42 +0100 [thread overview]
Message-ID: <20261105111324.706922-4-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261105111324.706922-1-h.laimer@proxmox.com>
A non-first fragment has no transport header, so no check matched it,
and a pair allowed only through narrowed rules dropped every later
fragment. Add a check that matches non-first fragments of a specific
(or any) protocol.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-ebpf/src/policy/bpf/policy.bpf.c | 28 ++++++++---
proxmox-ebpf/src/policy/bpf/types.h | 3 ++
proxmox-ebpf/src/policy/mod.rs | 63 ++++++++++++++----------
proxmox-ebpf/src/policy/types.rs | 4 ++
proxmox-ebpf/tests/policy.rs | 62 +++++++++++++++++++++++
5 files changed, 129 insertions(+), 31 deletions(-)
diff --git a/proxmox-ebpf/src/policy/bpf/policy.bpf.c b/proxmox-ebpf/src/policy/bpf/policy.bpf.c
index ed189a1227e1..f94857a17c18 100644
--- a/proxmox-ebpf/src/policy/bpf/policy.bpf.c
+++ b/proxmox-ebpf/src/policy/bpf/policy.bpf.c
@@ -75,7 +75,10 @@ int tc_tap_ingress(struct __sk_buff *skb) {
return TC_ACT_UNSPEC;
}
-// What a check matches on. `matchable` is set once the parse reached the L4 header.
+// What a check matches on. `matchable` is set once the parse reached the L4 header. `fragment`
+// marks a non-first fragment, which has a protocol and no L4 header. `unreadable` marks a frame
+// the parse could not read to its L4 header, such as one cut short or behind a second tag in the
+// frame data.
struct frame {
__u16 eth_proto;
__u16 sport;
@@ -88,6 +91,7 @@ struct frame {
__u8 has_ports;
__u8 has_icmp;
__u8 unreadable;
+ __u8 fragment;
// an authentication header was walked past, so a check for AH matches as well
__u8 ah;
};
@@ -109,7 +113,11 @@ static __always_inline int ipv6_l4(struct __sk_buff *skb, struct frame *f, __u32
return 0;
}
// the offset sits in the top 13 bits of the second halfword
- if (hdr[2] || (hdr[3] & 0xf8)) return 0;
+ if (hdr[2] || (hdr[3] & 0xf8)) {
+ f->fragment = 1;
+ f->ip_proto = hdr[0];
+ return 0;
+ }
*off += 8;
} else {
if (bpf_skb_load_bytes(skb, *off, hdr, 2) < 0) {
@@ -202,10 +210,12 @@ static __always_inline void parse_frame(struct __sk_buff *skb, struct frame *f)
f->unreadable = 1;
return;
}
- // only the first fragment carries the L4 header
- if (bpf_ntohs(ip.frag_off) & IPV4_FRAG_OFFSET) return;
f->family = FAMILY_V4;
f->ip_proto = ip.protocol;
+ if (bpf_ntohs(ip.frag_off) & IPV4_FRAG_OFFSET) {
+ f->fragment = 1;
+ return;
+ }
off += (__u32)ip.ihl * 4;
} else if (f->eth_proto == ETH_P_IPV6) {
struct ipv6hdr ip6;
@@ -225,9 +235,15 @@ static __always_inline void parse_frame(struct __sk_buff *skb, struct frame *f)
}
static __always_inline int check_matches(const struct frame *f, const struct check_value *c) {
- if (!f->matchable) return 0;
+ if (c->match_flags & CHECK_FRAGMENT) {
+ if (!f->fragment) return 0;
+ } else if (!f->matchable) {
+ return 0;
+ }
if (c->family && c->family != f->family) return 0;
- if (c->ip_proto != f->ip_proto && !(c->ip_proto == PROTO_AH && f->ah)) return 0;
+ if ((c->match_flags & CHECK_HAS_PROTO) && c->ip_proto != f->ip_proto &&
+ !(c->ip_proto == PROTO_AH && f->ah))
+ return 0;
if (c->match_flags & CHECK_HAS_SPORT) {
if (!f->has_ports || f->sport < c->sport_lo || f->sport > c->sport_hi) return 0;
diff --git a/proxmox-ebpf/src/policy/bpf/types.h b/proxmox-ebpf/src/policy/bpf/types.h
index dbeb9bea59db..971af2c1e754 100644
--- a/proxmox-ebpf/src/policy/bpf/types.h
+++ b/proxmox-ebpf/src/policy/bpf/types.h
@@ -26,6 +26,9 @@
#define CHECK_HAS_DPORT (1 << 1)
#define CHECK_HAS_ICMP_TYPE (1 << 2)
#define CHECK_HAS_ICMP_CODE (1 << 3)
+// the check is for non-first fragments, which no other check matches
+#define CHECK_FRAGMENT (1 << 4)
+#define CHECK_HAS_PROTO (1 << 5)
struct nic_entry {
__u16 id;
diff --git a/proxmox-ebpf/src/policy/mod.rs b/proxmox-ebpf/src/policy/mod.rs
index 4ca65bb9f595..9a670bb11577 100644
--- a/proxmox-ebpf/src/policy/mod.rs
+++ b/proxmox-ebpf/src/policy/mod.rs
@@ -74,8 +74,11 @@ pub enum Family {
/// left out is not matched on.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CheckEntry {
- pub ip_proto: u8,
+ pub ip_proto: Option<u8>,
pub family: Family,
+ /// Match non-first fragments instead of frames with an L4 header. A fragment has no L4 header,
+ /// so a check that names a port or an ICMP field matches nothing.
+ pub fragment: bool,
pub sport: Option<(u16, u16)>,
pub dport: Option<(u16, u16)>,
pub icmp_type: Option<u8>,
@@ -86,6 +89,12 @@ pub struct CheckEntry {
impl CheckEntry {
fn value(&self) -> CheckValue {
let mut match_flags = 0;
+ if self.fragment {
+ match_flags |= CHECK_FRAGMENT;
+ }
+ if self.ip_proto.is_some() {
+ match_flags |= CHECK_HAS_PROTO;
+ }
if self.sport.is_some() {
match_flags |= CHECK_HAS_SPORT;
}
@@ -101,7 +110,7 @@ impl CheckEntry {
let (sport_lo, sport_hi) = self.sport.unwrap_or_default();
let (dport_lo, dport_hi) = self.dport.unwrap_or_default();
CheckValue {
- ip_proto: self.ip_proto,
+ ip_proto: self.ip_proto.unwrap_or_default(),
family: self.family as u8,
match_flags,
allow: self.allow as u8,
@@ -551,13 +560,10 @@ mod tests {
fn entry(port: u16) -> CheckEntry {
CheckEntry {
- ip_proto: 6,
- family: Family::Any,
- sport: None,
+ ip_proto: Some(6),
dport: Some((port, port)),
- icmp_type: None,
- icmp_code: None,
allow: true,
+ ..Default::default()
}
}
@@ -588,25 +594,19 @@ mod tests {
#[test]
fn check_entry_bytes() {
let plain = CheckEntry {
- ip_proto: 17,
- family: Family::Any,
- sport: None,
- dport: None,
- icmp_type: None,
- icmp_code: None,
- allow: false,
+ ip_proto: Some(17),
+ ..Default::default()
};
- assert_eq!(
- bytes(plain.value()),
- [17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- );
+ let b = bytes(plain.value());
+ assert_eq!(b[..4], [17, 0, CHECK_HAS_PROTO, 0]);
+ assert_eq!(b[4..], [0; 12]);
let v4 = CheckEntry {
family: Family::V4,
allow: true,
..plain.clone()
};
- assert_eq!(bytes(v4.value())[..4], [17, 4, 0, 1]);
+ assert_eq!(bytes(v4.value())[..4], [17, 4, CHECK_HAS_PROTO, 1]);
let v6 = CheckEntry {
family: Family::V6,
..plain.clone()
@@ -618,7 +618,7 @@ mod tests {
..plain.clone()
};
let b = bytes(sport.value());
- assert_eq!(b[2], CHECK_HAS_SPORT);
+ assert_eq!(b[2], CHECK_HAS_PROTO | CHECK_HAS_SPORT);
assert_eq!(b[4..6], 1000u16.to_ne_bytes());
assert_eq!(b[6..8], 2000u16.to_ne_bytes());
assert_eq!(b[8..], [0; 8]);
@@ -628,27 +628,40 @@ mod tests {
..plain.clone()
};
let b = bytes(dport.value());
- assert_eq!(b[2], CHECK_HAS_DPORT);
+ assert_eq!(b[2], CHECK_HAS_PROTO | CHECK_HAS_DPORT);
assert_eq!(b[4..8], [0; 4]);
assert_eq!(b[8..10], 22u16.to_ne_bytes());
assert_eq!(b[10..12], 23u16.to_ne_bytes());
let icmp = CheckEntry {
- ip_proto: 1,
+ ip_proto: Some(1),
icmp_type: Some(3),
icmp_code: Some(4),
..plain.clone()
};
let b = bytes(icmp.value());
- assert_eq!(b[2], CHECK_HAS_ICMP_TYPE | CHECK_HAS_ICMP_CODE);
+ assert_eq!(
+ b[2],
+ CHECK_HAS_PROTO | CHECK_HAS_ICMP_TYPE | CHECK_HAS_ICMP_CODE
+ );
assert_eq!(b[12..], [3, 4, 0, 0]);
let code_only = CheckEntry {
- ip_proto: 1,
+ ip_proto: Some(1),
icmp_code: Some(0),
+ ..plain.clone()
+ };
+ assert_eq!(
+ bytes(code_only.value())[2],
+ CHECK_HAS_PROTO | CHECK_HAS_ICMP_CODE
+ );
+
+ let fragment = CheckEntry {
+ ip_proto: None,
+ fragment: true,
..plain
};
- assert_eq!(bytes(code_only.value())[2], CHECK_HAS_ICMP_CODE);
+ assert_eq!(bytes(fragment.value())[..3], [0, 0, CHECK_FRAGMENT]);
}
#[test]
diff --git a/proxmox-ebpf/src/policy/types.rs b/proxmox-ebpf/src/policy/types.rs
index e411ba4ae6f2..c8e97c177625 100644
--- a/proxmox-ebpf/src/policy/types.rs
+++ b/proxmox-ebpf/src/policy/types.rs
@@ -25,6 +25,8 @@ pub const CHECK_HAS_SPORT: u8 = 1 << 0;
pub const CHECK_HAS_DPORT: u8 = 1 << 1;
pub const CHECK_HAS_ICMP_TYPE: u8 = 1 << 2;
pub const CHECK_HAS_ICMP_CODE: u8 = 1 << 3;
+pub const CHECK_FRAGMENT: u8 = 1 << 4;
+pub const CHECK_HAS_PROTO: u8 = 1 << 5;
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -151,5 +153,7 @@ mod layout {
defined("CHECK_HAS_ICMP_CODE"),
usize::from(CHECK_HAS_ICMP_CODE)
);
+ assert_eq!(defined("CHECK_FRAGMENT"), usize::from(CHECK_FRAGMENT));
+ assert_eq!(defined("CHECK_HAS_PROTO"), usize::from(CHECK_HAS_PROTO));
}
}
diff --git a/proxmox-ebpf/tests/policy.rs b/proxmox-ebpf/tests/policy.rs
index 11c513c69945..03bcf438850a 100644
--- a/proxmox-ebpf/tests/policy.rs
+++ b/proxmox-ebpf/tests/policy.rs
@@ -51,6 +51,8 @@ struct Check {
dport: Option<(u16, u16)>,
icmp_type: Option<u8>,
icmp_code: Option<u8>,
+ fragment: bool,
+ any_proto: bool,
allow: bool,
}
@@ -80,6 +82,12 @@ impl Check {
v.match_flags |= CHECK_HAS_ICMP_CODE;
v.icmp_code = code;
}
+ if self.fragment {
+ v.match_flags |= CHECK_FRAGMENT;
+ }
+ if !self.any_proto {
+ v.match_flags |= CHECK_HAS_PROTO;
+ }
v
}
}
@@ -381,6 +389,60 @@ fn a_later_fragment_matches_nothing() {
assert_eq!(run(&fragment), TC_ACT_UNSPEC);
}
+#[test]
+fn a_fragment_check_matches_later_fragments_only() {
+ let fragments = Check {
+ fragment: true,
+ any_proto: true,
+ allow: true,
+ ..Default::default()
+ };
+ install(false, &[fragments]);
+
+ let later = ipv4(PROTO_TCP, 185, &tcp(40000, 22));
+ assert_eq!(run(ðernet(ETHERTYPE_IPV4, 0, &later)), TC_ACT_UNSPEC);
+ let mut later = fragment_header(PROTO_UDP, 185);
+ later.extend_from_slice(&udp(40000, 53));
+ assert_eq!(
+ run(ðernet(ETHERTYPE_IPV6, 0, &ipv6(PROTO_FRAGMENT, &later))),
+ TC_ACT_UNSPEC
+ );
+
+ // the first fragment carries the L4 header, more fragments following or not
+ let first = ipv4(PROTO_TCP, 0x2000, &tcp(40000, 22));
+ assert_eq!(run(ðernet(ETHERTYPE_IPV4, 0, &first)), TC_ACT_SHOT);
+ assert_eq!(run(&tcp_frame(40000, 22)), TC_ACT_SHOT);
+ let mut first = fragment_header(PROTO_UDP, 0);
+ first.extend_from_slice(&udp(40000, 53));
+ assert_eq!(
+ run(ðernet(ETHERTYPE_IPV6, 0, &ipv6(PROTO_FRAGMENT, &first))),
+ TC_ACT_SHOT
+ );
+}
+
+#[test]
+fn a_fragment_check_can_name_the_protocol() {
+ let udp_fragments = Check {
+ ip_proto: PROTO_UDP,
+ fragment: true,
+ allow: true,
+ ..Default::default()
+ };
+ install(false, &[udp_fragments]);
+
+ let later = |protocol| ethernet(ETHERTYPE_IPV4, 0, &ipv4(protocol, 185, &[0; 8]));
+ assert_eq!(run(&later(PROTO_UDP)), TC_ACT_UNSPEC);
+ assert_eq!(run(&later(PROTO_TCP)), TC_ACT_SHOT);
+
+ let later = |protocol| {
+ let mut payload = fragment_header(protocol, 185);
+ payload.extend_from_slice(&[0; 8]);
+ ethernet(ETHERTYPE_IPV6, 0, &ipv6(PROTO_FRAGMENT, &payload))
+ };
+ assert_eq!(run(&later(PROTO_UDP)), TC_ACT_UNSPEC);
+ assert_eq!(run(&later(PROTO_TCP)), TC_ACT_SHOT);
+}
+
#[test]
fn one_vlan_tag_is_parsed_through() {
install(
--
2.47.3
next prev parent reply other threads:[~2026-11-05 11:13 UTC|newest]
Thread overview: 46+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 11:12 [PATCH access-control/cluster/container/docs/manager/network/proxmox{,-ve-rs,-perl-rs}/qemu-server v4 00/45] sdn: add microsegmentation support Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox v4 01/45] ebpf: build: keep the build directory out of the BPF objects Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox v4 02/45] ebpf: policy: add the enforcement subsystem for guest interfaces Hannes Laimer
2026-11-05 11:12 ` Hannes Laimer [this message]
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 04/45] ve-config: firewall: add a constructor for sctp matches Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 05/45] ve-config: sdn: microseg: add identity allocation for group sets Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 06/45] ve-config: sdn: microseg: add config types Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 07/45] ve-config: sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 08/45] ve-config: sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 09/45] ve-config: sdn: microseg: narrow rules by protocol and ports Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 10/45] ve-config: sdn: microseg: add group hierarchy Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 11/45] ve-config: firewall: expose protocol, icmp type and code numbers Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 12/45] ve-config: sdn: microseg: add the desired state Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 13/45] microseg: add a crate that applies the policy to a node's guest NICs Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 14/45] microseg: add a rule option that matches non-first fragments Hannes Laimer
2026-11-05 11:12 ` [PATCH pve-cluster v4 15/45] cfs: add 'sdn/microseg.cfg' to observed files Hannes Laimer
2026-11-05 11:12 ` [PATCH pve-access-control v4 16/45] permissions: add ACL path for SDN microsegmentation Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 17/45] pve-rs: sdn: add microseg config binding Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 18/45] pve-rs: sdn: microseg: bind the data plane Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 19/45] pve-rs: sdn: microseg: check the data plane limits on render Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 20/45] pve-rs: debian: fire the pve-microseg-update trigger on upgrade Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 21/45] sdn: zones: add vxlan-gbp option to vxlan and evpn zones Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 22/45] sdn: microseg: add config, API and guest inventory Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 23/45] sdn: dry-run: show pending microseg changes Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 24/45] sdn: microseg: enforce the policy on guest NICs Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 25/45] sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 26/45] sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 27/45] sdn: microseg: add an update for a running NIC Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 28/45] sdn: microseg: expose protocol, port and macro rule options Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 29/45] sdn: microseg: allow nesting groups under a parent Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 30/45] sdn: microseg: expose the fragments rule option Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 31/45] network: apply the microseg policy on reload Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 32/45] pve-sdn-commit: apply the microseg policy at boot Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 33/45] ui: sdn: zones: add vxlan-gbp checkbox to vxlan and evpn Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 34/45] ui: sdn: add microsegmentation panel Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 35/45] ui: sdn: dry-run: show pending microseg diff Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 36/45] ui: sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 37/45] ui: sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 38/45] ui: sdn: microseg: add narrowing to the rule form Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 39/45] ui: sdn: microseg: show and edit the group hierarchy Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 40/45] ui: sdn: microseg: add fragments option to the rule form Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 41/45] ui: sdn: microseg: add a guest tab with its groups and rules Hannes Laimer
2026-11-05 11:13 ` [PATCH qemu-server v4 42/45] network: update the microseg enforcement on a tag or name change Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-container v4 43/45] lxc: update the microseg enforcement on a tag or hostname change Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-docs v4 44/45] sdn: zones: document the VXLAN-GBP option Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-docs v4 45/45] sdn: add microsegmentation section Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261105111324.706922-4-h.laimer@proxmox.com \
--to=h.laimer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox