anvilsign in

collin/browser-terminal-extension

1//! Picking up a listening socket that someone else already bound.
2//!
3//! systemd's socket activation lets the *socket* outlive the daemon: systemd
4//! holds `127.0.0.1:7681` open from login, and only execs `termbridge serve`
5//! when the sidebar actually connects. Combined with `--idle-timeout` the
6//! daemon then exits once the last client goes away, and the next connection
7//! starts a fresh one.
8//!
9//! That is only safe because tmux, not this process, is the persistence layer.
10//! Sessions survive the daemon exiting, so "no clients" really is "nothing to
11//! keep alive".
12
13use std::net::TcpListener;
14
15/// The first fd systemd passes. Defined by the protocol, not by us.
16const LISTEN_FDS_START: i32 = 3;
17
18/// Take the listener systemd passed us, if this process was socket-activated.
19///
20/// `Ok(None)` means "started normally, bind your own socket". An `Err` means we
21/// *were* activated but the handoff was wrong, which must not fall back to
22/// binding: the port is already owned by systemd and the bind would fail (or,
23/// worse, succeed on a different port and leave the sidebar talking to nobody).
24pub fn systemd_listener() -> std::io::Result<Option<TcpListener>> {
25 let Some(fds) = listen_fds()? else {
26 return Ok(None);
27 };
28 if fds != 1 {
29 return Err(err(format!(
30 "systemd passed {fds} sockets, expected exactly 1 — check ListenStream in termbridge.socket"
31 )));
32 }
33
34 // Safe to own fd 3: the LISTEN_PID check below/above proved these variables
35 // were meant for this process, and nothing else in the daemon touches it.
36 let listener = unsafe {
37 use std::os::fd::FromRawFd;
38 TcpListener::from_raw_fd(LISTEN_FDS_START)
39 };
40
41 // Same invariant the self-bound path asserts. A unit file with
42 // `ListenStream=0.0.0.0:7681` would otherwise silently expose a shell to
43 // the network, and the unit is a file the user can edit.
44 let addr = listener.local_addr()?;
45 if !addr.ip().is_loopback() {
46 return Err(err(format!(
47 "refusing the socket systemd passed: {addr} is not loopback"
48 )));
49 }
50
51 listener.set_nonblocking(true)?;
52 Ok(Some(listener))
53}
54
55/// `$LISTEN_FDS`, but only if `$LISTEN_PID` says the variables are ours.
56///
57/// The check matters because these variables are inherited by children. Without
58/// it, a shell spawned inside the pty would look socket-activated to any
59/// termbridge it ran.
60fn listen_fds() -> std::io::Result<Option<usize>> {
61 let Ok(pid) = std::env::var("LISTEN_PID") else {
62 return Ok(None);
63 };
64 let fds = std::env::var("LISTEN_FDS").unwrap_or_default();
65 // Clear before anything can fork: the pty spawns a shell, and these must
66 // not be part of its environment. Called from startup, single-threaded,
67 // before any other thread can be reading the environment.
68 unsafe {
69 std::env::remove_var("LISTEN_PID");
70 std::env::remove_var("LISTEN_FDS");
71 std::env::remove_var("LISTEN_FDNAMES");
72 }
73
74 if pid.trim().parse::<u32>().ok() != Some(std::process::id()) {
75 return Ok(None);
76 }
77 match fds.trim().parse::<usize>() {
78 Ok(n) => Ok(Some(n)),
79 Err(_) => Err(err(format!("LISTEN_PID is ours but LISTEN_FDS={fds:?}"))),
80 }
81}
82
83fn err(msg: String) -> std::io::Error {
84 std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 // These mutate process-global environment, so they share one test to avoid
92 // racing each other under the default multi-threaded harness.
93 #[test]
94 fn env_handshake() {
95 // SAFETY (all of these): the harness runs this test alone in its
96 // process for the same reason the assertions below are batched.
97 unsafe { std::env::remove_var("LISTEN_PID") };
98 assert!(
99 listen_fds().unwrap().is_none(),
100 "no LISTEN_PID: not activated"
101 );
102
103 // Addressed to some other process: ignored, and consumed so it cannot
104 // be inherited further.
105 unsafe {
106 std::env::set_var("LISTEN_PID", "1");
107 std::env::set_var("LISTEN_FDS", "1");
108 }
109 assert!(
110 listen_fds().unwrap().is_none(),
111 "LISTEN_PID for another pid"
112 );
113 assert!(std::env::var("LISTEN_PID").is_err(), "consumed anyway");
114 assert!(std::env::var("LISTEN_FDS").is_err(), "consumed anyway");
115
116 unsafe {
117 std::env::set_var("LISTEN_PID", std::process::id().to_string());
118 std::env::set_var("LISTEN_FDS", "2");
119 }
120 assert_eq!(listen_fds().unwrap(), Some(2));
121
122 unsafe {
123 std::env::set_var("LISTEN_PID", std::process::id().to_string());
124 std::env::set_var("LISTEN_FDS", "not-a-number");
125 }
126 assert!(listen_fds().is_err(), "ours but malformed is an error");
127 }
128}