Skip to main content

conmonrs/
listener.rs

1//! Helpers for the listening sockets of the server: private sockets, socket
2//! paths of any length and the errors of the accept loops.
3
4use anyhow::{Context, Result};
5use nix::sys::{
6    socket::{AddressFamily, Backlog, SockFlag, SockType, UnixAddr, bind, listen, socket},
7    stat::{Mode, fchmod},
8};
9use std::{
10    error::Error,
11    ffi::OsStr,
12    fmt,
13    fs::{self, File},
14    io,
15    os::{fd::OwnedFd, unix::io::AsRawFd},
16    path::{Path, PathBuf},
17    time::{Duration, Instant},
18};
19use tokio::time;
20use tracing::{debug, error};
21
22/// How long an accept loop pauses after a failed accept.
23const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
24
25/// How often the failed accepts of a listener get logged as errors, a
26/// server out of descriptors would log one every [`ACCEPT_BACKOFF`]
27/// otherwise.
28const ACCEPT_ERROR_INTERVAL: Duration = Duration::from_secs(10);
29
30/// The failed accepts of a listener which keeps going.
31#[derive(Debug, Default)]
32pub(crate) struct AcceptErrors {
33    /// When the last one got logged as an error.
34    last_logged: Option<Instant>,
35}
36
37impl AcceptErrors {
38    /// Handle a failed accept. Most errors, like running out of descriptors,
39    /// fail every accept until something changes, so pause instead of
40    /// spinning. Only a connection which failed on its own is no reason to
41    /// wait, or to tell about it beyond the debug log.
42    pub(crate) async fn failed(&mut self, e: &io::Error) {
43        if connection_failed(e) {
44            debug!("Unable to accept connection: {e}");
45            return;
46        }
47        if self.log_error_now(Instant::now()) {
48            error!("Unable to accept connection: {e}");
49        } else {
50            debug!("Unable to accept connection: {e}");
51        }
52        time::sleep(ACCEPT_BACKOFF).await;
53    }
54
55    /// Whether an error is due to be logged as one, at most once per
56    /// [`ACCEPT_ERROR_INTERVAL`].
57    fn log_error_now(&mut self, now: Instant) -> bool {
58        if self
59            .last_logged
60            .is_some_and(|last| now.saturating_duration_since(last) < ACCEPT_ERROR_INTERVAL)
61        {
62            return false;
63        }
64        self.last_logged = Some(now);
65        true
66    }
67}
68
69/// Whether an accept failed for the connection only, which the client
70/// aborted or reset before it got accepted.
71fn connection_failed(e: &io::Error) -> bool {
72    matches!(
73        e.raw_os_error(),
74        Some(libc::ECONNABORTED | libc::ECONNREFUSED | libc::ECONNRESET | libc::EPROTO)
75    )
76}
77
78/// A socket which a running process still serves, so it must not be
79/// replaced. A type of its own tells it apart from other errors, like for
80/// the tests, which retry while a process they forked holds a socket.
81#[derive(Debug)]
82pub(crate) struct SocketInUse(pub(crate) String);
83
84impl fmt::Display for SocketInUse {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.0)
87    }
88}
89
90impl Error for SocketInUse {}
91
92/// Create a non-blocking UNIX socket which only its owner can connect to.
93/// Linux creates the file of a socket on bind with the mode of the socket
94/// minus the umask, so others cannot connect at any time, unlike with a
95/// chmod after the bind.
96pub(crate) fn private_socket(typ: SockType) -> io::Result<OwnedFd> {
97    let fd = socket(
98        AddressFamily::Unix,
99        typ,
100        SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC,
101        None,
102    )?;
103    fchmod(&fd, Mode::S_IRWXU)?;
104    Ok(fd)
105}
106
107/// Listen on a stream socket at `path` which only its owner can connect to,
108/// see [`private_socket`].
109pub(crate) fn listen_private(path: &Path) -> io::Result<OwnedFd> {
110    let fd = private_socket(SockType::Stream)?;
111    bind(fd.as_raw_fd(), &UnixAddr::new(path)?)?;
112    listen(&fd, Backlog::MAXCONN)?;
113    Ok(fd)
114}
115
116/// Bind a listener with `bind` at a socket path whatever its length, see
117/// [`shorten_path`]. The parent directory gets created if it does not exist.
118/// The callers name the socket in the error of a failed bind.
119pub(crate) fn bind_long_path<L>(
120    path: &Path,
121    bind: impl FnOnce(PathBuf) -> io::Result<L>,
122) -> Result<L> {
123    // keep parent_fd in scope until the bind, or else the socket will not work
124    let (path, _parent_dir) = shorten_socket_path(path)?;
125    Ok(bind(path)?)
126}
127
128/// Like [`shorten_path`], creating the parent directory if it does not
129/// exist.
130pub(crate) fn shorten_socket_path(path: &Path) -> Result<(PathBuf, File)> {
131    let (parent, _) = split_socket_path(path)?;
132    fs::create_dir_all(parent).context("create parent directory")?;
133    shorten_path(path)
134}
135
136/// Refer to a socket path through its parent directory, so that it fits into
137/// a socket address whatever its length. The directory has to exist and
138/// stays open as long as the returned file, the path is only valid until
139/// then.
140pub(crate) fn shorten_path(path: &Path) -> Result<(PathBuf, File)> {
141    let (parent, name) = split_socket_path(path)?;
142    let parent = File::open(parent).context("open parent directory")?;
143    let fd = parent.as_raw_fd();
144
145    Ok((
146        PathBuf::from("/proc/self/fd")
147            .join(fd.to_string())
148            .join(name),
149        parent,
150    ))
151}
152
153/// Split a socket path into its parent directory and name, a relative name
154/// lives in the current directory.
155fn split_socket_path(path: &Path) -> Result<(&Path, &OsStr)> {
156    let parent = path.parent().with_context(|| {
157        format!(
158            "tried to specify / as socket to bind to: {}",
159            path.display()
160        )
161    })?;
162    let parent = if parent.as_os_str().is_empty() {
163        Path::new(".")
164    } else {
165        parent
166    };
167    let name = path.file_name().with_context(|| {
168        format!(
169            "tried to specify '..' as socket to bind to: {}",
170            path.display(),
171        )
172    })?;
173    Ok((parent, name))
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use std::os::unix::fs::PermissionsExt;
180    use tokio::net::UnixListener;
181
182    #[test]
183    fn accept_errors_get_logged_once_per_interval() {
184        let mut errors = AcceptErrors::default();
185        let start = Instant::now();
186        assert!(errors.log_error_now(start));
187        assert!(!errors.log_error_now(start + ACCEPT_BACKOFF));
188        // Another listener logs its own.
189        assert!(AcceptErrors::default().log_error_now(start + ACCEPT_BACKOFF));
190        assert!(errors.log_error_now(start + ACCEPT_ERROR_INTERVAL));
191    }
192
193    #[tokio::test(start_paused = true)]
194    async fn only_failed_connections_accept_at_once() {
195        let start = time::Instant::now();
196        let mut errors = AcceptErrors::default();
197        for errno in [
198            libc::ECONNABORTED,
199            libc::ECONNREFUSED,
200            libc::ECONNRESET,
201            libc::EPROTO,
202        ] {
203            errors.failed(&io::Error::from_raw_os_error(errno)).await;
204        }
205        assert_eq!(start.elapsed(), Duration::ZERO);
206        errors
207            .failed(&io::Error::from_raw_os_error(libc::EMFILE))
208            .await;
209        assert_eq!(start.elapsed(), ACCEPT_BACKOFF);
210    }
211
212    #[tokio::test]
213    async fn bind_long_path_creates_the_directory() -> Result<()> {
214        let dir = tempfile::tempdir()?;
215        // Longer than a socket address holds.
216        let parent = dir.path().join("x".repeat(100)).join("y".repeat(100));
217        let path = parent.join("socket");
218        let listener = bind_long_path(&path, UnixListener::bind)?;
219        assert!(path.exists());
220        drop(listener);
221
222        // A path in use fails.
223        assert!(bind_long_path(&path, UnixListener::bind).is_err());
224        Ok(())
225    }
226
227    #[tokio::test]
228    async fn listen_private_binds_private_listeners() -> Result<()> {
229        let dir = tempfile::tempdir()?;
230        let path = dir.path().join("socket");
231        let fd = bind_long_path(&path, |path| listen_private(&path))?;
232        assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o077, 0);
233        let listener = UnixListener::from_std(fd.into())?;
234        let _client = tokio::net::UnixStream::connect(&path).await?;
235        listener.accept().await?;
236        Ok(())
237    }
238
239    #[test]
240    fn shorten_socket_path_refers_to_the_parent() -> Result<()> {
241        let dir = tempfile::tempdir()?;
242        let (path, parent) = shorten_socket_path(&dir.path().join("new").join("socket"))?;
243        assert!(path.ends_with(format!("{}/socket", parent.as_raw_fd())));
244        assert!(dir.path().join("new").is_dir());
245
246        // Relative names live in the current directory.
247        assert!(split_socket_path(Path::new("socket"))?.0 == Path::new("."));
248        assert!(shorten_socket_path(Path::new("/")).is_err());
249        Ok(())
250    }
251
252    #[test]
253    fn shorten_socket_path_failures() -> Result<()> {
254        let dir = tempfile::tempdir()?;
255        let file = dir.path().join("file");
256        fs::write(&file, "")?;
257        // The parent cannot be created.
258        assert!(shorten_socket_path(&file.join("socket")).is_err());
259        // Without creating it, a missing parent cannot be opened.
260        assert!(shorten_path(&dir.path().join("missing").join("socket")).is_err());
261        Ok(())
262    }
263
264    #[test]
265    fn private_socket_is_created_private() -> Result<()> {
266        let dir = tempfile::tempdir()?;
267        let path = dir.path().join("socket");
268        let fd = private_socket(SockType::Stream)?;
269        // The file gets the mode of the socket right on bind, before anything
270        // could change it. The umask may take more bits, but never adds any.
271        bind(fd.as_raw_fd(), &UnixAddr::new(&path)?)?;
272        assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o077, 0);
273        Ok(())
274    }
275}