public inbox for pve-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal