From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ebpf v3 03/45] policy: add a check for non-first fragments
Date: Tue, 27 Oct 2026 09:54:45 +0100 [thread overview]
Message-ID: <20261027085527.702738-4-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-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>
---
src/policy/bpf/policy.bpf.c | 26 +++++++++++----
src/policy/bpf/types.h | 3 ++
src/policy/mod.rs | 63 ++++++++++++++++++++++---------------
src/policy/types.rs | 4 +++
tests/policy.rs | 62 ++++++++++++++++++++++++++++++++++++
5 files changed, 127 insertions(+), 31 deletions(-)
diff --git a/src/policy/bpf/policy.bpf.c b/src/policy/bpf/policy.bpf.c
index 53acd35cc6df..fcf513bb13de 100644
--- a/src/policy/bpf/policy.bpf.c
+++ b/src/policy/bpf/policy.bpf.c
@@ -82,7 +82,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;
@@ -95,6 +98,7 @@ struct frame {
__u8 has_ports;
__u8 has_icmp;
__u8 unreadable;
+ __u8 fragment;
};
static __always_inline int is_ext_header(__u8 proto) {
@@ -114,7 +118,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) {
@@ -206,10 +214,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 == ETHERTYPE_IPV6) {
struct ipv6hdr ip6;
@@ -229,9 +239,13 @@ 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) return 0;
+ if ((c->match_flags & CHECK_HAS_PROTO) && c->ip_proto != f->ip_proto) 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/src/policy/bpf/types.h b/src/policy/bpf/types.h
index a8b1df66fc41..0bfe1d3b9038 100644
--- a/src/policy/bpf/types.h
+++ b/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/src/policy/mod.rs b/src/policy/mod.rs
index 62ee7576b6fc..e3d3e580f7f3 100644
--- a/src/policy/mod.rs
+++ b/src/policy/mod.rs
@@ -76,8 +76,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>,
@@ -88,6 +91,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;
}
@@ -103,7 +112,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,
@@ -681,13 +690,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()
}
}
@@ -718,25 +724,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()
@@ -748,7 +748,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]);
@@ -758,27 +758,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/src/policy/types.rs b/src/policy/types.rs
index e411ba4ae6f2..c8e97c177625 100644
--- a/src/policy/types.rs
+++ b/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/tests/policy.rs b/tests/policy.rs
index c755c2cc5a60..5ca034da5f68 100644
--- a/tests/policy.rs
+++ b/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-10-27 8:55 UTC|newest]
Thread overview: 46+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 8:54 [PATCH access-control/cluster/container/docs/manager/network/proxmox{-ebpf,-ve-rs,-perl-rs}/qemu-server v3 00/45] sdn: add microsegmentation support Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ebpf v3 01/45] build: keep the build directory out of the BPF objects Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ebpf v3 02/45] policy: add the enforcement subsystem for guest interfaces Hannes Laimer
2026-10-27 8:54 ` Hannes Laimer [this message]
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 04/45] ve-config: firewall: add a constructor for sctp matches Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 05/45] ve-config: sdn: microseg: add identity allocation for group sets Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 06/45] ve-config: sdn: microseg: add config types Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 07/45] ve-config: sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 08/45] ve-config: sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 09/45] ve-config: sdn: microseg: narrow rules by protocol and ports Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 10/45] ve-config: sdn: microseg: add group hierarchy Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 11/45] ve-config: firewall: expose protocol, icmp type and code numbers Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 12/45] ve-config: sdn: microseg: add the desired state Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 13/45] microseg: add a crate that applies the policy to a node's guest NICs Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 14/45] microseg: add a rule option that matches non-first fragments Hannes Laimer
2026-10-27 8:54 ` [PATCH pve-cluster v3 15/45] cfs: add 'sdn/microseg.cfg' to observed files Hannes Laimer
2026-10-27 8:54 ` [PATCH pve-access-control v3 16/45] permissions: add ACL path for SDN microsegmentation Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-perl-rs v3 17/45] pve-rs: sdn: add microseg config binding Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 18/45] pve-rs: sdn: microseg: bind the data plane Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 19/45] pve-rs: sdn: microseg: check the data plane limits on render Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 20/45] pve-rs: debian: fire the pve-microseg-update trigger on upgrade Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 21/45] sdn: zones: add vxlan-gbp option to vxlan and evpn zones Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 22/45] sdn: microseg: add config, API and guest inventory Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 23/45] sdn: dry-run: show pending microseg changes Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 24/45] sdn: microseg: enforce the policy on guest NICs Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 25/45] sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 26/45] sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 27/45] sdn: microseg: add an update for a running NIC Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 28/45] sdn: microseg: expose protocol, port and macro rule options Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 29/45] sdn: microseg: allow nesting groups under a parent Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 30/45] sdn: microseg: expose the fragments rule option Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 31/45] network: apply the microseg policy on reload Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 32/45] pve-sdn-commit: apply the microseg policy at boot Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 33/45] ui: sdn: zones: add vxlan-gbp checkbox to vxlan and evpn Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 34/45] ui: sdn: add microsegmentation panel Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 35/45] ui: sdn: dry-run: show pending microseg diff Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 36/45] ui: sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 37/45] ui: sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 38/45] ui: sdn: microseg: add narrowing to the rule form Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 39/45] ui: sdn: microseg: show and edit the group hierarchy Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 40/45] ui: sdn: microseg: add fragments option to the rule form Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 41/45] ui: sdn: microseg: add a guest tab with its groups and rules Hannes Laimer
2026-10-27 8:55 ` [PATCH qemu-server v3 42/45] network: update the microseg enforcement on a tag or name change Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-container v3 43/45] lxc: update the microseg enforcement on a tag or hostname change Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-docs v3 44/45] sdn: zones: document the VXLAN-GBP option Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-docs v3 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=20261027085527.702738-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