Skip to main content

lonca_daemon/
server.rs

1//! Unix socket server: one thread per connection, one JSON line per request.
2//! How many, for how long and how long a line: `crate::limits`.
3
4use std::io::{BufRead, BufReader, ErrorKind, Write};
5use std::os::unix::net::{UnixListener, UnixStream};
6use std::path::Path;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use crate::limits::{Counters, Held, Limits, Registry, Slot, head_of};
11use crate::protocol::{Request, Response};
12use crate::service::Service;
13
14/// Longest socket path that fits `sockaddr_un` on every supported platform.
15pub const MAX_SOCKET_PATH: usize = 100;
16
17/// Socket access control. In hardened mode the daemon runs as its own
18/// system user and lets the developer in through a group and a uid list.
19#[derive(Clone, Debug)]
20pub struct Access {
21    pub mode: u32,
22    pub group: Option<u32>,
23    /// Peers whose uid is not listed are disconnected. `None`: anyone who
24    /// can open the socket file.
25    pub allowed_uids: Option<Vec<u32>>,
26}
27
28impl Default for Access {
29    fn default() -> Self {
30        Self {
31            mode: 0o600,
32            group: None,
33            allowed_uids: None,
34        }
35    }
36}
37
38pub fn bind(path: &Path) -> std::io::Result<UnixListener> {
39    bind_with(path, &Access::default())
40}
41
42pub fn bind_with(path: &Path, access: &Access) -> std::io::Result<UnixListener> {
43    if path.as_os_str().len() > MAX_SOCKET_PATH {
44        return Err(std::io::Error::new(
45            std::io::ErrorKind::InvalidInput,
46            format!(
47                "socket path {} is longer than {MAX_SOCKET_PATH} bytes; set LONCA_SOCKET to a shorter path",
48                path.display()
49            ),
50        ));
51    }
52    if path.exists() {
53        // A live daemon answers; a stale socket file does not.
54        if UnixStream::connect(path).is_ok() {
55            return Err(std::io::Error::new(
56                std::io::ErrorKind::AddrInUse,
57                "a Lonca daemon is already running",
58            ));
59        }
60        std::fs::remove_file(path)?;
61    }
62    let l = UnixListener::bind(path)?;
63    use std::os::unix::fs::PermissionsExt;
64    std::fs::set_permissions(path, std::fs::Permissions::from_mode(access.mode))?;
65    if let Some(gid) = access.group {
66        std::os::unix::fs::chown(path, None, Some(gid))?;
67    }
68    Ok(l)
69}
70
71#[cfg(not(target_os = "linux"))]
72pub use crate::peer_other::peer_in_sandbox;
73
74pub use crate::sys::peer_uid;
75
76/// How many pid namespaces a process of `status`, the text of
77/// `/proc/<pid>/status`, lies in, as far as the reader can see.
78#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
79fn pid_namespaces(status: &str) -> usize {
80    let line = status.lines().find_map(|l| l.strip_prefix("NSpid:"));
81    line.map_or(0, |l| l.split_whitespace().count())
82}
83
84/// Whether the process that connected lies in a pid namespace below this
85/// one, as bubblewrap's does: in a sandbox, not in this one. The same user can
86/// make such a namespace, and nothing on one account can prove otherwise
87/// (`docs/design.md` ยง3.1).
88#[cfg(target_os = "linux")]
89pub fn peer_in_sandbox(s: &UnixStream) -> bool {
90    let status = |who: String| std::fs::read_to_string(format!("/proc/{who}/status")).ok();
91    let Some(cred) = crate::sys::peer_cred(s) else {
92        return false;
93    };
94    match (status(cred.pid.to_string()), status("self".into())) {
95        (Some(peer), Some(me)) => pid_namespaces(&peer) > pid_namespaces(&me),
96        _ => false,
97    }
98}
99
100/// Resolves a group name or number.
101pub fn group_id(name: &str) -> Option<u32> {
102    if let Ok(n) = name.parse() {
103        return Some(n);
104    }
105    crate::sys::group_named(name)
106}
107
108pub fn serve(listener: UnixListener, svc: Arc<Service>) {
109    serve_with(listener, svc, Access::default(), Limits::default())
110}
111
112/// How long to wait after a failed `accept`: with no descriptors left it
113/// fails at once, every time.
114const ACCEPT_PAUSE: Duration = Duration::from_millis(20);
115
116/// `accept` itself never waits for a client, so nothing a client does can
117/// stop the next one being let in.
118pub fn serve_with(listener: UnixListener, svc: Arc<Service>, access: Access, limits: Limits) {
119    let registry = Registry::new(limits);
120    for conn in listener.incoming() {
121        let Ok(stream) = conn else {
122            std::thread::sleep(ACCEPT_PAUSE);
123            continue;
124        };
125        let uid = peer_uid(&stream);
126        if let Some(allowed) = &access.allowed_uids
127            && !uid.is_some_and(|u| allowed.contains(&u))
128        {
129            use crate::security::{Event, kind};
130            let e = Event::new(svc.now(), kind::REFUSED_PEER)
131                .uid(uid)
132                .detail("a uid the socket does not serve");
133            svc.security_event(e);
134            continue; // dropped: closes the connection
135        }
136        let stream = Arc::new(stream);
137        // A uid unknown to the system is one more client, not a free pass.
138        let Some((slot, evicted)) = registry.admit(uid.unwrap_or(u32::MAX), stream.clone()) else {
139            Counters::add(&svc.counters.busy);
140            let full = "the daemon has no room for another connection";
141            let _ = write_by(&stream, &Response::err(0, "busy", full), ACCEPT_WRITE);
142            continue;
143        };
144        if evicted {
145            Counters::add(&svc.counters.evicted);
146        }
147        let svc = svc.clone();
148        let _ = std::thread::Builder::new().spawn(move || {
149            let _ = handle_conn(&stream, &slot, &svc);
150        });
151    }
152}
153
154/// A refusal is one short line into an empty buffer, but `accept` must
155/// not depend on that.
156const ACCEPT_WRITE: Duration = Duration::from_millis(100);
157
158/// What a connection sent next.
159enum Incoming {
160    Line(String),
161    /// The line was read and thrown away; the connection goes on.
162    Refused(Response),
163    End,
164    /// Nothing came in time; `started`: in the middle of a line.
165    TimedOut {
166        started: bool,
167    },
168}
169
170/// What a read that returned `e` comes to: only a deadline is no failure.
171fn after_failed_read(e: std::io::Error, started: bool) -> std::io::Result<Incoming> {
172    match e.kind() {
173        ErrorKind::WouldBlock | ErrorKind::TimedOut => Ok(Incoming::TimedOut { started }),
174        _ => Err(e),
175    }
176}
177
178/// One line, under the connection's limits. A refused line is read to its
179/// end, so the client, still writing, gets to read why.
180fn read_request(
181    r: &mut BufReader<&UnixStream>,
182    held: &mut Held,
183    limits: &Limits,
184    svc: &Service,
185) -> std::io::Result<Incoming> {
186    let stream = *r.get_ref();
187    let mut buf = Vec::new();
188    let mut id = 0;
189    let mut long = false;
190    let mut refused = None;
191    let mut deadline = None;
192    loop {
193        let wait = deadline.map_or(limits.idle, |d: Instant| {
194            d.saturating_duration_since(Instant::now())
195        });
196        if wait.is_zero() {
197            return Ok(Incoming::TimedOut { started: true });
198        }
199        // Fails only on macOS once the client is gone, and then no read waits:
200        // what it wrote before leaving is still there to read.
201        let _ = stream.set_read_timeout(Some(wait));
202        let chunk = match r.fill_buf() {
203            Ok(chunk) => chunk,
204            Err(e) => return after_failed_read(e, deadline.is_some()),
205        };
206        if chunk.is_empty() {
207            return Ok(match refused {
208                Some(why) => Incoming::Refused(why),
209                None if buf.is_empty() => Incoming::End,
210                None => Incoming::Line(String::from_utf8_lossy(&buf).into_owned()),
211            });
212        }
213        deadline.get_or_insert_with(|| Instant::now() + limits.line);
214        let newline = chunk.iter().position(|&b| b == b'\n');
215        let n = newline.unwrap_or(chunk.len());
216        if refused.is_none() {
217            buf.extend_from_slice(&chunk[..n]);
218        }
219        r.consume(n + usize::from(newline.is_some()));
220        if refused.is_none() && buf.len() > limits.short_line {
221            if !long {
222                let head = head_of(&buf[..limits.short_line]);
223                id = head.id;
224                match svc.long_line(&head) {
225                    Ok(()) => long = true,
226                    Err((code, msg)) => refused = Some(Response::err(id, &code, msg)),
227                }
228            }
229            if long && buf.len() > limits.long_line {
230                let msg = format!("a request line holds at most {} bytes", limits.long_line);
231                refused = Some(Response::err(id, "too_large", msg));
232            } else if long && !held.grow(buf.len()) {
233                let msg = "too many long requests at once; send it again";
234                refused = Some(Response::err(id, "busy", msg));
235            }
236            if let Some(why) = &refused {
237                Counters::add(match why.error.as_ref().map(|e| e.code.as_str()) {
238                    Some("busy") => &svc.counters.busy,
239                    _ => &svc.counters.too_large,
240                });
241                buf = Vec::new();
242            }
243        }
244        if newline.is_some() {
245            return Ok(match refused {
246                Some(why) => Incoming::Refused(why),
247                None => Incoming::Line(String::from_utf8_lossy(&buf).into_owned()),
248            });
249        }
250    }
251}
252
253/// Writes one line by a deadline for all of it: a client that takes one byte
254/// at a time gets no more time than one that takes none.
255fn write_by(mut s: &UnixStream, v: &Response, limit: Duration) -> std::io::Result<()> {
256    let mut bytes = serde_json::to_vec(v)?;
257    bytes.push(b'\n');
258    let deadline = Instant::now() + limit;
259    let mut left = &bytes[..];
260    while !left.is_empty() {
261        let wait = deadline.saturating_duration_since(Instant::now());
262        if wait.is_zero() {
263            return Err(ErrorKind::TimedOut.into());
264        }
265        let _ = s.set_write_timeout(Some(wait)); // as in `read_request`
266        left = &left[s.write(left)?..];
267    }
268    Ok(())
269}
270
271/// The last line of a connection the daemon is closing on a client that may
272/// be gone.
273fn farewell(stream: &UnixStream, limits: &Limits, code: &str, msg: &str) {
274    let _ = write_by(stream, &Response::err(0, code, msg), limits.write);
275}
276
277const NOT_READ: &str = "connection closed while quiet; this request was not run, send it again";
278
279fn handle_conn(stream: &UnixStream, slot: &Slot, svc: &Service) -> std::io::Result<()> {
280    let limits = slot.limits();
281    let mut reader = BufReader::new(stream);
282    loop {
283        let mut held = slot.hold();
284        let resp = match read_request(&mut reader, &mut held, limits, svc)? {
285            Incoming::End if slot.evicted() => {
286                farewell(stream, limits, "closed", NOT_READ);
287                return Ok(());
288            }
289            Incoming::End => return Ok(()),
290            Incoming::TimedOut { started: false } => {
291                Counters::add(&svc.counters.idle);
292                farewell(stream, limits, "closed", NOT_READ);
293                return Ok(());
294            }
295            Incoming::TimedOut { started: true } => {
296                Counters::add(&svc.counters.slow);
297                farewell(stream, limits, "timeout", "the request line took too long");
298                return Ok(());
299            }
300            Incoming::Refused(why) => {
301                use crate::security::{Event, kind};
302                let code = why
303                    .error
304                    .as_ref()
305                    .map(|e| e.code.clone())
306                    .unwrap_or_default();
307                if code == "too_large" {
308                    let e = Event::new(svc.now(), kind::OVERSIZED)
309                        .uid(slot.uid())
310                        .code(&code)
311                        .detail("a request line past its method's limit");
312                    svc.security_event(e);
313                }
314                why
315            }
316            Incoming::Line(line) if line.trim().is_empty() => continue,
317            Incoming::Line(_) if !slot.begin() => {
318                farewell(stream, limits, "closed", NOT_READ);
319                return Ok(());
320            }
321            Incoming::Line(line) => match serde_json::from_str::<Request>(&line) {
322                Ok(req) => svc.handle_from(req, &|| peer_in_sandbox(stream)),
323                Err(e) => {
324                    use crate::security::{Event, kind};
325                    let ev = Event::new(svc.now(), kind::MALFORMED)
326                        .uid(slot.uid())
327                        .code("invalid")
328                        .detail(&format!("not a request: {e}"));
329                    svc.security_event(ev);
330                    Response::err(0, "invalid", format!("bad request: {e}"))
331                }
332            },
333        };
334        if let Err(e) = write_by(stream, &resp, limits.write) {
335            Counters::add(&svc.counters.slow);
336            return Err(e);
337        }
338        slot.end();
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn a_response_nobody_reads_runs_out_of_time() {
348        let (a, _b) = UnixStream::pair().unwrap();
349        let big = Response::err(0, "x", "y".repeat(16 * 1024 * 1024));
350        let started = Instant::now();
351        let e = write_by(&a, &big, Duration::from_millis(100)).unwrap_err();
352        assert!(matches!(
353            e.kind(),
354            ErrorKind::WouldBlock | ErrorKind::TimedOut
355        ));
356        assert!(started.elapsed() < Duration::from_secs(5));
357    }
358
359    #[test]
360    fn only_a_deadline_is_a_timeout() {
361        for (kind, started) in [(ErrorKind::WouldBlock, true), (ErrorKind::TimedOut, false)] {
362            let after = after_failed_read(kind.into(), started);
363            assert!(matches!(after, Ok(Incoming::TimedOut { started: s }) if s == started));
364        }
365        let gone = after_failed_read(ErrorKind::ConnectionReset.into(), true);
366        assert_eq!(
367            gone.err().map(|e| e.kind()),
368            Some(ErrorKind::ConnectionReset)
369        );
370    }
371
372    #[test]
373    fn a_response_is_a_line() {
374        use std::io::Read;
375        let (a, mut b) = UnixStream::pair().unwrap();
376        write_by(&a, &Response::err(3, "x", "y"), Duration::from_secs(5)).unwrap();
377        drop(a);
378        let mut text = String::new();
379        b.read_to_string(&mut text).unwrap();
380        let line = text.strip_suffix('\n').unwrap();
381        assert_eq!(serde_json::from_str::<Response>(line).unwrap().id, 3);
382    }
383
384    #[test]
385    fn a_test_process_is_in_no_sandbox() {
386        let (a, _b) = UnixStream::pair().unwrap();
387        assert!(!peer_in_sandbox(&a));
388    }
389
390    #[test]
391    fn namespaces_are_counted_from_the_status() {
392        assert_eq!(pid_namespaces("Name:\tx\nNSpid:\t1234\nNSsid:\t1\n"), 1);
393        assert_eq!(pid_namespaces("NSpid:\t1234\t2\n"), 2);
394        assert_eq!(pid_namespaces("Name:\tx\n"), 0);
395    }
396
397    #[test]
398    fn peer_uid_is_ours() {
399        let (a, _b) = UnixStream::pair().unwrap();
400        assert_eq!(peer_uid(&a), Some(crate::sys::current_uid()));
401    }
402
403    #[test]
404    fn groups_resolve() {
405        assert_eq!(group_id("0"), Some(0));
406        assert!(group_id("wheel").is_some() || group_id("root").is_some());
407        assert_eq!(group_id("no-such-group-lonca"), None);
408    }
409}