all lists on lists.proxmox.com
 help / color / mirror / Atom feed
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(&ethernet(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(&ethernet(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(&ethernet(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(&ethernet(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





  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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal