parsec_service/front/
domain_socket.rs

1// Copyright 2019 Contributors to the Parsec project.
2// SPDX-License-Identifier: Apache-2.0
3//! Service front using Unix domain sockets
4//!
5//! Expose Parsec functionality using Unix domain sockets as an IPC layer.
6//! The local socket is created at a predefined location.
7use super::listener;
8use anyhow::{Context, Result};
9use listener::Listen;
10use listener::{Connection, ConnectionMetadata};
11use log::{error, warn};
12use std::fs;
13use std::fs::Permissions;
14use std::io::{Error, ErrorKind};
15use std::os::unix::fs::FileTypeExt;
16use std::os::unix::fs::PermissionsExt;
17use std::os::unix::io::FromRawFd;
18use std::os::unix::io::RawFd;
19use std::os::unix::net::UnixListener;
20use std::path::PathBuf;
21use std::time::Duration;
22
23static DEFAULT_SOCKET_PATH: &str = "/run/parsec/parsec.sock";
24
25/// Unix Domain Socket IPC manager
26///
27/// Listener implementation for Unix sockets as the underlying IPC mechanism.
28///
29/// Holds references to a `UnixListener`.
30#[derive(Debug)]
31pub struct DomainSocketListener {
32    listener: UnixListener,
33    timeout: Duration,
34}
35
36impl DomainSocketListener {
37    /// Initialise the connection to the Unix socket.
38    pub fn new(timeout: Duration, socket_path: PathBuf) -> Result<Self> {
39        // If Parsec was service activated or not started under systemd, this
40        // will return `0`. `1` will be returned in case Parsec is socket activated.
41        let listeners: Vec<RawFd> = sd_notify::listen_fds()?.collect();
42        let listener = match listeners.len() {
43            0 => {
44                if socket_path.exists() {
45                    let meta = fs::metadata(&socket_path)?;
46                    if meta.file_type().is_socket() {
47                        warn!(
48                            "Removing the existing socket file at {}.",
49                            socket_path.display()
50                        );
51                        fs::remove_file(&socket_path)?;
52                    } else {
53                        error!(
54                            "A file exists at {} but is not a Unix Domain Socket.",
55                            socket_path.display()
56                        );
57                    }
58                }
59
60                // Will fail if a file already exists at the path.
61                let listener = UnixListener::bind(&socket_path).with_context(|| {
62                    format!("Failed to bind to Unix socket at {:?}", socket_path)
63                })?;
64                listener.set_nonblocking(true)?;
65
66                // Set the socket's permission to 666 to allow clients of different user to
67                // connect.
68                let permissions = Permissions::from_mode(0o666);
69                fs::set_permissions(socket_path, permissions)?;
70
71                listener
72            }
73            1 => {
74                // No need to set the socket as non-blocking, parsec.service
75                // already requests that.
76                let nfd = listeners[0];
77                // Safe as listen_fds gives us the information that one file descriptor was
78                // received and its value starts from SD_LISTEN_FDS_START.
79                unsafe { UnixListener::from_raw_fd(nfd) }
80                // Expect the socket created by systemd to be 666 on permissions.
81            }
82            n => {
83                error!(
84                    "Received too many file descriptors ({} received, 0 or 1 expected).",
85                    n
86                );
87                return Err(Error::new(
88                    ErrorKind::InvalidData,
89                    "too many file descriptors received",
90                )
91                .into());
92            }
93        };
94
95        Ok(Self { listener, timeout })
96    }
97}
98
99impl Listen for DomainSocketListener {
100    fn set_timeout(&mut self, duration: Duration) {
101        self.timeout = duration;
102    }
103
104    fn accept(&self) -> Option<Connection> {
105        let stream_result = self.listener.accept();
106        match stream_result {
107            Ok((stream, _)) => {
108                if let Err(err) = stream.set_read_timeout(Some(self.timeout)) {
109                    format_error!("Failed to set read timeout", err);
110                    None
111                } else if let Err(err) = stream.set_write_timeout(Some(self.timeout)) {
112                    format_error!("Failed to set write timeout", err);
113                    None
114                } else if let Err(err) = stream.set_nonblocking(false) {
115                    format_error!("Failed to set stream as blocking", err);
116                    None
117                } else {
118                    let ucred = peer_credentials::peer_cred(&stream)
119                        .map_err(|err| {
120                            format_error!(
121                                "Failed to grab peer credentials metadata from UnixStream",
122                                err
123                            );
124                            err
125                        })
126                        .ok()?;
127                    Some(Connection {
128                        stream: Box::new(stream),
129                        metadata: Some(ConnectionMetadata::UnixPeerCredentials {
130                            uid: ucred.uid,
131                            gid: ucred.gid,
132                            pid: ucred.pid,
133                        }),
134                    })
135                }
136            }
137            Err(err) => {
138                // Check if the error is because no connections are currently present.
139                if err.kind() != ErrorKind::WouldBlock {
140                    // Only log the real errors.
141                    format_error!("Failed to connect with a UnixStream", err);
142                }
143                None
144            }
145        }
146    }
147}
148
149/// Builder for `DomainSocketListener`
150#[derive(Clone, Debug, Default)]
151pub struct DomainSocketListenerBuilder {
152    timeout: Option<Duration>,
153    socket_path: Option<PathBuf>,
154}
155
156impl DomainSocketListenerBuilder {
157    /// Create a new DomainSocketListener builder
158    pub fn new() -> Self {
159        DomainSocketListenerBuilder {
160            timeout: None,
161            socket_path: None,
162        }
163    }
164
165    /// Add a timeout on the Unix Domain Socket used
166    pub fn with_timeout(mut self, timeout: Duration) -> Self {
167        self.timeout = Some(timeout);
168        self
169    }
170
171    /// Specify the Unix Domain Socket path
172    pub fn with_socket_path(mut self, socket_path: Option<PathBuf>) -> Self {
173        self.socket_path = socket_path;
174        self
175    }
176
177    /// Build the builder into the listener
178    pub fn build(self) -> Result<DomainSocketListener> {
179        DomainSocketListener::new(
180            self.timeout.ok_or_else(|| {
181                error!("The listener timeout was not set.");
182                Error::new(ErrorKind::InvalidInput, "listener timeout missing")
183            })?,
184            self.socket_path
185                .unwrap_or_else(|| DEFAULT_SOCKET_PATH.into()),
186        )
187    }
188}
189
190// == IMPORTANT NOTE ==
191//
192// The code below has been cherry-picked from the following PR:
193//
194//     https://github.com/rust-lang/rust/pull/75148
195//
196// At the time of writing (16/09/20), this patch is in the nightly Rust channel. To avoid needing
197// to use the nightly compiler to build Parsec, we have instead opted to cherry-pick the change
198// from the patch to allow us to use this feature 'early'.
199//
200// Once the feature hits stable, it should be safe to revert the commit that introduced the changes
201// below with `git revert`. You can find the stabilizing Rust issue here:
202//
203//     https://github.com/rust-lang/rust/issues/42839
204
205/// Implementation of peer credentials fetching for Unix domain socket.
206pub mod peer_credentials {
207    use libc::{gid_t, pid_t, uid_t};
208
209    /// Credentials for a UNIX process for credentials passing.
210    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
211    pub struct UCred {
212        /// The UID part of the peer credential. This is the effective UID of the process at the domain
213        /// socket's endpoint.
214        pub uid: uid_t,
215        /// The GID part of the peer credential. This is the effective GID of the process at the domain
216        /// socket's endpoint.
217        pub gid: gid_t,
218        /// The PID part of the peer credential. This field is optional because the PID part of the
219        /// peer credentials is not supported on every platform. On platforms where the mechanism to
220        /// discover the PID exists, this field will be populated to the PID of the process at the
221        /// domain socket's endpoint. Otherwise, it will be set to None.
222        pub pid: Option<pid_t>,
223    }
224
225    #[cfg(any(target_os = "android", target_os = "linux"))]
226    pub use self::impl_linux::peer_cred;
227
228    #[cfg(any(
229        target_os = "dragonfly",
230        target_os = "freebsd",
231        target_os = "ios",
232        target_os = "macos",
233        target_os = "openbsd"
234    ))]
235    pub use self::impl_bsd::peer_cred;
236
237    #[cfg(any(target_os = "linux", target_os = "android"))]
238    #[allow(missing_docs, trivial_casts)] // docs not required; only used for selective compilation.
239    pub mod impl_linux {
240        use super::UCred;
241        use libc::{c_void, getsockopt, socklen_t, ucred, SOL_SOCKET, SO_PEERCRED};
242        use std::os::unix::io::AsRawFd;
243        use std::os::unix::net::UnixStream;
244        use std::{io, mem};
245
246        pub fn peer_cred(socket: &UnixStream) -> io::Result<UCred> {
247            let ucred_size = mem::size_of::<ucred>();
248
249            // Trivial sanity checks.
250            assert!(mem::size_of::<u32>() <= mem::size_of::<usize>());
251            assert!(ucred_size <= u32::MAX as usize);
252
253            let mut ucred_size = ucred_size as socklen_t;
254            let mut ucred: ucred = ucred {
255                pid: 1,
256                uid: 1,
257                gid: 1,
258            };
259
260            unsafe {
261                let ret = getsockopt(
262                    socket.as_raw_fd(),
263                    SOL_SOCKET,
264                    SO_PEERCRED,
265                    &mut ucred as *mut ucred as *mut c_void,
266                    &mut ucred_size,
267                );
268
269                if ret == 0 && ucred_size as usize == mem::size_of::<ucred>() {
270                    Ok(UCred {
271                        uid: ucred.uid,
272                        gid: ucred.gid,
273                        pid: Some(ucred.pid),
274                    })
275                } else {
276                    Err(io::Error::last_os_error())
277                }
278            }
279        }
280    }
281
282    #[cfg(any(
283        target_os = "dragonfly",
284        target_os = "macos",
285        target_os = "ios",
286        target_os = "freebsd",
287        target_os = "openbsd"
288    ))]
289    #[allow(missing_docs)] // docs not required; only used for selective compilation.
290    pub mod impl_bsd {
291        use super::UCred;
292        use std::io;
293        use std::os::unix::io::AsRawFd;
294        use std::os::unix::net::UnixStream;
295
296        pub fn peer_cred(socket: &UnixStream) -> io::Result<UCred> {
297            let mut cred = UCred {
298                uid: 1,
299                gid: 1,
300                pid: None,
301            };
302            unsafe {
303                let ret = libc::getpeereid(socket.as_raw_fd(), &mut cred.uid, &mut cred.gid);
304
305                if ret == 0 {
306                    Ok(cred)
307                } else {
308                    Err(io::Error::last_os_error())
309                }
310            }
311        }
312    }
313}