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 pve-cluster 04/12] rust: notify: add subscription matching
Date: Thu, 22 Oct 2026 16:21:57 +0200	[thread overview]
Message-ID: <20261022142205.930536-5-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261022142205.930536-1-h.laimer@proxmox.com>

A subscription is a set of named templates. An event matches when its
path or its rename target matches one of them. A rename carries the
captures of each side that matches. A connection without root
privileges gets no event for a private path, just as IPC and FUSE
hide the content from it.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/rust/pmxcfs-notify/src/conn.rs | 140 +++++++++++++++++++++++++++++
 src/rust/pmxcfs-notify/src/lib.rs  |   4 +-
 2 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 src/rust/pmxcfs-notify/src/conn.rs

diff --git a/src/rust/pmxcfs-notify/src/conn.rs b/src/rust/pmxcfs-notify/src/conn.rs
new file mode 100644
index 0000000..451504c
--- /dev/null
+++ b/src/rust/pmxcfs-notify/src/conn.rs
@@ -0,0 +1,140 @@
+use std::collections::BTreeMap;
+
+use crate::protocol::Params;
+use crate::template::Template;
+
+pub(crate) struct Conn {
+    privileged: bool,
+    templates: BTreeMap<String, Template>,
+}
+
+impl Conn {
+    pub(crate) fn new(privileged: bool) -> Self {
+        Self {
+            privileged,
+            templates: BTreeMap::new(),
+        }
+    }
+
+    pub(crate) fn matches<'a>(&'a self, path: &'a str, to: Option<&'a str>) -> Option<Params<'a>> {
+        if !self.privileged && (is_private(path) || to.is_some_and(is_private)) {
+            return None;
+        }
+        let mut params = Params::new();
+        for (name, template) in &self.templates {
+            let mut sets = Vec::new();
+            if let Some(captures) = template.matches(path) {
+                sets.push(captures);
+            }
+            // equal captures mean the same path, so a rename onto itself matches once
+            if let Some(to) = to
+                && let Some(captures) = template.matches(to)
+                && sets.first() != Some(&captures)
+            {
+                sets.push(captures);
+            }
+            if !sets.is_empty() {
+                params.insert(name.as_str(), sets);
+            }
+        }
+        (!params.is_empty()).then_some(params)
+    }
+}
+
+// Same rule as path_is_private in the daemon.
+fn is_private(path: &str) -> bool {
+    fn priv_component(path: &str) -> bool {
+        path == "priv" || path.starts_with("priv/")
+    }
+    let path = crate::normalize_path(path);
+    priv_component(path)
+        || path
+            .strip_prefix("nodes/")
+            .and_then(|rest| rest.split_once('/'))
+            .is_some_and(|(_, below)| priv_component(below))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn subscribed(privileged: bool, specs: &[(&str, &str)]) -> Conn {
+        let mut conn = Conn::new(privileged);
+        conn.templates = specs
+            .iter()
+            .map(|(name, text)| ((*name).to_owned(), text.parse().unwrap()))
+            .collect();
+        conn
+    }
+
+    #[test]
+    fn matches_per_template() {
+        let conn = subscribed(
+            true,
+            &[
+                ("guest", "nodes/{node}/qemu-server/{vmid}.conf"),
+                ("dc", "datacenter.cfg"),
+            ],
+        );
+        let params = conn.matches("nodes/n1/qemu-server/100.conf", None).unwrap();
+        assert_eq!(params.len(), 1);
+        assert_eq!(
+            params["guest"],
+            vec![BTreeMap::from([("node", "n1"), ("vmid", "100")])]
+        );
+        assert_eq!(
+            conn.matches("datacenter.cfg", None).unwrap()["dc"],
+            vec![BTreeMap::new()]
+        );
+        assert!(conn.matches("datacenter.cfg.tmp.1", None).is_none());
+        assert!(conn.matches("other", None).is_none());
+        assert!(Conn::new(true).matches("datacenter.cfg", None).is_none());
+    }
+
+    #[test]
+    fn rename_matches_on_either_side() {
+        let conn = subscribed(true, &[("guest", "nodes/{node}/qemu-server/{vmid}.conf")]);
+        let n1 = "nodes/n1/qemu-server/100.conf";
+        let n2 = "nodes/n2/qemu-server/100.conf";
+
+        let params = conn.matches(n1, Some(n2)).unwrap();
+        assert_eq!(params["guest"].len(), 2);
+        assert_eq!(params["guest"][0]["node"], "n1");
+        assert_eq!(params["guest"][1]["node"], "n2");
+
+        let params = conn
+            .matches("nodes/n2/qemu-server/100.conf.tmp.1", Some(n2))
+            .unwrap();
+        assert_eq!(params["guest"].len(), 1);
+        assert_eq!(params["guest"][0]["node"], "n2");
+
+        assert_eq!(conn.matches(n1, Some(n1)).unwrap()["guest"].len(), 1);
+    }
+
+    #[test]
+    fn private_paths_need_privileges() {
+        let conn = subscribed(false, &[("all", "{path...}")]);
+        assert!(conn.matches("datacenter.cfg", None).is_some());
+        assert!(conn.matches("priv/lock/x", None).is_none());
+        assert!(conn.matches("nodes/n1/priv/ssl.key", None).is_none());
+        assert!(conn.matches("datacenter.cfg", Some("priv/moved")).is_none());
+        assert!(conn.matches("priv/old", Some("datacenter.cfg")).is_none());
+
+        let conn = subscribed(true, &[("all", "{path...}")]);
+        assert!(conn.matches("priv/lock/x", None).is_some());
+        assert!(conn.matches("priv/old", Some("datacenter.cfg")).is_some());
+    }
+
+    #[test]
+    fn private_rules() {
+        assert!(is_private("priv"));
+        assert!(is_private("priv/lock/x"));
+        assert!(is_private("/priv/tfa.cfg"));
+        assert!(is_private("nodes/n1/priv"));
+        assert!(is_private("nodes/n1/priv/ssl.key"));
+        assert!(!is_private("private"));
+        assert!(!is_private("nodes/n1/qemu-server/100.conf"));
+        assert!(!is_private("nodes/priv"));
+        assert!(!is_private("datacenter.cfg"));
+    }
+}
diff --git a/src/rust/pmxcfs-notify/src/lib.rs b/src/rust/pmxcfs-notify/src/lib.rs
index d35d3c5..18e7d2f 100644
--- a/src/rust/pmxcfs-notify/src/lib.rs
+++ b/src/rust/pmxcfs-notify/src/lib.rs
@@ -10,10 +10,12 @@
 //! A template is a memdb path whose components may each hold one `{name}` placeholder for all or
 //! part of that component. The last component may instead be `{name...}` for the rest of the path.
 //! `nodes/{node}/qemu-server/{vmid}.conf` captures node and vmid, and `{path...}` matches every
-//! path.
+//! path. Connections admitted by their group id rather than as root never receive events that
+//! mention a private path, just as IPC and FUSE hide private files from them.
 
 #![allow(dead_code)]
 
+mod conn;
 mod protocol;
 mod template;
 
-- 
2.47.3





  parent reply	other threads:[~2026-10-22 14:22 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22 14:21 [PATCH cluster 00/12] pmxcfs: add a change notification socket Hannes Laimer
2026-10-22 14:21 ` [PATCH pve-cluster 01/12] buildsys: add rust workspace under src/rust Hannes Laimer
2026-10-22 14:21 ` [PATCH pve-cluster 02/12] rust: notify: add path templates with placeholder capture Hannes Laimer
2026-10-22 14:21 ` [PATCH pve-cluster 03/12] rust: notify: add the wire protocol Hannes Laimer
2026-10-22 14:21 ` Hannes Laimer [this message]
2026-10-22 14:21 ` [PATCH pve-cluster 05/12] rust: notify: add the socket server Hannes Laimer
2026-10-22 14:21 ` [PATCH pve-cluster 06/12] rust: ffi: add C ABI staticlib for pmxcfs Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 07/12] pmxcfs: memdb: add change notification hook Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 08/12] buildsys: link pmxcfs against the rust notify staticlib Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 09/12] pmxcfs: notify: emit change events over the notification socket Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 10/12] cfs: add perl client for the change " Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 11/12] cfs: add hook registry for change notification consumers Hannes Laimer
2026-10-22 14:22 ` [PATCH pve-cluster 12/12] cfs: add runner executing cluster change hooks in children 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=20261022142205.930536-5-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