Skip to main content

ados_protocol/ipc/
peer_class.rs

1//! Command sockets with a reader group: peers that may connect and read, but
2//! carry neither the service identity nor the operator group.
3
4use std::io;
5use std::path::Path;
6
7use super::{bind_socket_file, group_gid, set_socket_group, OperatorListener, OPERATOR_GROUP};
8
9/// What an admitted peer on a command socket is.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PeerClass {
12    /// Root or the serving process's own uid: an agent service.
13    Service,
14    /// A member of [`OPERATOR_GROUP`].
15    Operator,
16    /// A member of the socket's reader group only. It may read what the
17    /// server offers and nothing more.
18    Reader,
19}
20
21/// Classify a peer from its kernel-reported credentials, or `None` when the
22/// socket must refuse it. Service first, then the operator group, then the
23/// reader group, so a peer in both groups is an operator. With a group absent
24/// from the host (its gid `None`) nobody is admitted through it.
25///
26/// Pure over the credentials so the decision is testable without a second uid.
27pub fn classify_peer(
28    peer_uid: u32,
29    peer_gids: &[u32],
30    server_uid: u32,
31    operator_gid: Option<u32>,
32    reader_gid: Option<u32>,
33) -> Option<PeerClass> {
34    if peer_uid == 0 || peer_uid == server_uid {
35        return Some(PeerClass::Service);
36    }
37    let member = |gid: Option<u32>| gid.is_some_and(|g| peer_gids.contains(&g));
38    if member(operator_gid) {
39        Some(PeerClass::Operator)
40    } else if member(reader_gid) {
41        Some(PeerClass::Reader)
42    } else {
43        None
44    }
45}
46
47/// Bind a command socket that `reader_group` may also open: the hygiene of
48/// [`super::bind_command_socket`], but the file is handed to `reader_group`
49/// (so its members can `connect()`), and the listener admits service peers,
50/// operators and reader-group members, telling them apart through
51/// [`OperatorListener::accept_classified`]. An operator that is not also in
52/// `reader_group` cannot open the file; the installer adds the human operator
53/// to the reader groups it creates.
54///
55/// Like [`tokio::net::UnixListener::bind`], this must be called from within a
56/// Tokio runtime context.
57pub fn bind_reader_socket(
58    path: impl AsRef<Path>,
59    mode: u32,
60    reader_group: &str,
61) -> io::Result<OperatorListener> {
62    let path = path.as_ref();
63    let inner = bind_socket_file(path, mode)?;
64    let reader_gid = group_gid(reader_group);
65    set_socket_group(path, reader_group, reader_gid);
66    Ok(OperatorListener {
67        inner,
68        path: path.to_path_buf(),
69        server_uid: nix::unistd::geteuid().as_raw(),
70        operator_gid: group_gid(OPERATOR_GROUP),
71        reader_gid,
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    const SERVER: u32 = 900;
80    const OPERATOR: u32 = 988;
81    const SENSORS: u32 = 977;
82    const PLUGINS: u32 = 966;
83
84    fn class(uid: u32, gids: &[u32]) -> Option<PeerClass> {
85        classify_peer(uid, gids, SERVER, Some(OPERATOR), Some(SENSORS))
86    }
87
88    #[test]
89    fn service_peers_are_root_and_the_server_uid() {
90        assert_eq!(class(0, &[0]), Some(PeerClass::Service));
91        assert_eq!(class(SERVER, &[SERVER]), Some(PeerClass::Service));
92    }
93
94    #[test]
95    fn the_operator_group_outranks_the_reader_group() {
96        assert_eq!(class(1000, &[1000, OPERATOR]), Some(PeerClass::Operator));
97        assert_eq!(
98            class(1000, &[1000, SENSORS, OPERATOR]),
99            Some(PeerClass::Operator)
100        );
101    }
102
103    #[test]
104    fn a_reader_group_member_is_only_a_reader() {
105        // A plugin: primary group the plugin group, one supplementary reader.
106        assert_eq!(class(61_001, &[PLUGINS, SENSORS]), Some(PeerClass::Reader));
107    }
108
109    #[test]
110    fn anyone_else_is_refused() {
111        assert_eq!(class(61_001, &[PLUGINS]), None);
112        assert_eq!(class(1000, &[1000]), None);
113        // An absent group admits nobody through it.
114        assert_eq!(
115            classify_peer(61_001, &[SENSORS], SERVER, Some(OPERATOR), None),
116            None
117        );
118        assert_eq!(classify_peer(1000, &[OPERATOR], SERVER, None, None), None);
119    }
120
121    /// A socket bound for a reader group classifies this process (the server's
122    /// own uid) as a service peer through the real accept path.
123    #[tokio::test]
124    async fn a_reader_socket_classifies_its_own_uid_as_service() {
125        let dir = tempfile::tempdir().unwrap();
126        let path = dir.path().join("bus.sock");
127        let listener = bind_reader_socket(&path, 0o660, "ados-test-no-such-group").unwrap();
128        let _client = tokio::net::UnixStream::connect(&path).await.unwrap();
129        let (_stream, class) = listener.accept_classified().await.unwrap();
130        assert_eq!(class, PeerClass::Service);
131    }
132}