1use std::os::unix::fs::{FileTypeExt, PermissionsExt};
9use std::path::{Path, PathBuf};
10use std::{fs, io};
11
12use url::Url;
13
14use crate::RedactedUrl;
15
16const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
19
20#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
27#[group(id = "server-unix")]
28#[serde(deny_unknown_fields, default)]
29#[non_exhaustive]
30pub struct Config {
31 #[arg(long = "server-unix-bind", id = "server-unix-bind", env = "MOQ_SERVER_UNIX_BIND")]
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub bind: Option<PathBuf>,
35
36 #[command(flatten)]
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub allow: Option<Allow>,
41}
42
43#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
49#[group(id = "server-unix-allow")]
50#[serde(deny_unknown_fields, default)]
51#[non_exhaustive]
52pub struct Allow {
53 #[arg(
55 long = "server-unix-allow-uid",
56 env = "MOQ_SERVER_UNIX_ALLOW_UID",
57 value_delimiter = ','
58 )]
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub uid: Vec<u32>,
61
62 #[arg(
64 long = "server-unix-allow-gid",
65 env = "MOQ_SERVER_UNIX_ALLOW_GID",
66 value_delimiter = ','
67 )]
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub gid: Vec<u32>,
70
71 #[arg(
74 long = "server-unix-allow-pid",
75 env = "MOQ_SERVER_UNIX_ALLOW_PID",
76 value_delimiter = ','
77 )]
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub pid: Vec<i32>,
80}
81
82impl Allow {
83 pub(crate) fn is_empty(&self) -> bool {
85 self.uid.is_empty() && self.gid.is_empty() && self.pid.is_empty()
86 }
87
88 pub(crate) fn permits(&self, cred: &PeerCred) -> bool {
92 let uid_ok = self.uid.is_empty() || self.uid.contains(&cred.uid);
93 let gid_ok = self.gid.is_empty() || self.gid.contains(&cred.gid);
94 let pid_ok = self.pid.is_empty() || cred.pid.is_some_and(|pid| self.pid.contains(&pid));
95 uid_ok && gid_ok && pid_ok
96 }
97}
98
99#[derive(Debug, thiserror::Error)]
101#[non_exhaustive]
102pub enum Error {
103 #[error(transparent)]
105 Io(#[from] io::Error),
106
107 #[error("missing socket path in unix:// URL")]
109 MissingPath,
110
111 #[error("qmux connect failed")]
113 Connect(#[source] qmux::Error),
114
115 #[error("qmux accept failed")]
117 Accept(#[source] qmux::Error),
118
119 #[error("refusing to replace existing non-socket file at {0}")]
121 NotASocket(PathBuf),
122}
123
124type Result<T> = std::result::Result<T, Error>;
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct PeerCred {
132 pub uid: u32,
134 pub gid: u32,
136 pub pid: Option<i32>,
138}
139
140pub(crate) async fn connect(url: Url, protocols: &[&str]) -> Result<qmux::Session> {
146 let path = socket_path(&url).ok_or(Error::MissingPath)?;
147 tracing::debug!(url = %RedactedUrl::new(&url), "connecting via Unix socket");
148 qmux::uds::Config::new(WIRE_VERSION)
149 .protocols(protocols.iter().copied())
150 .connect(path)
151 .await
152 .map_err(Error::Connect)
153}
154
155fn socket_path(url: &Url) -> Option<PathBuf> {
156 let path = url.path();
157 if path.is_empty() {
158 None
159 } else {
160 Some(PathBuf::from(path))
161 }
162}
163
164pub struct Listener {
170 listener: tokio::net::UnixListener,
171 path: PathBuf,
172 protocols: Vec<String>,
173 health: crate::accept::Health,
174}
175
176impl Listener {
177 pub async fn bind(path: impl AsRef<Path>) -> Result<Self> {
183 let path = path.as_ref().to_path_buf();
184
185 match fs::symlink_metadata(&path) {
189 Ok(meta) if meta.file_type().is_socket() => fs::remove_file(&path)?,
190 Ok(_) => return Err(Error::NotASocket(path)),
191 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
192 Err(err) => return Err(err.into()),
193 }
194
195 let listener = tokio::net::UnixListener::bind(&path)?;
196 Ok(Self {
197 listener,
198 path,
199 protocols: Vec::new(),
200 health: crate::accept::Health::new("unix"),
201 })
202 }
203
204 pub fn accept_health(&self) -> crate::accept::Health {
207 self.health.clone()
208 }
209
210 pub fn with_accept_health(mut self, health: crate::accept::Health) -> Self {
216 self.health = health;
217 self
218 }
219
220 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
223 where
224 I: IntoIterator<Item = S>,
225 S: Into<String>,
226 {
227 self.protocols = protocols.into_iter().map(Into::into).collect();
228 self
229 }
230
231 pub fn set_mode(&self, mode: u32) -> Result<()> {
233 fs::set_permissions(&self.path, fs::Permissions::from_mode(mode))?;
234 Ok(())
235 }
236
237 pub fn path(&self) -> &Path {
239 &self.path
240 }
241
242 pub async fn accept(&self) -> Option<Result<(qmux::Session, PeerCred)>> {
251 let stream = self.accept_socket().await;
252 let cred = match stream.peer_cred() {
253 Ok(cred) => PeerCred {
254 uid: cred.uid(),
255 gid: cred.gid(),
256 pid: cred.pid(),
257 },
258 Err(err) => return Some(Err(err.into())),
259 };
260 let session = qmux::uds::Config::new(WIRE_VERSION)
261 .protocols(self.protocols.iter().map(String::as_str))
262 .accept(stream)
263 .await
264 .map_err(Error::Accept);
265 Some(session.map(|session| (session, cred)))
266 }
267
268 async fn accept_socket(&self) -> tokio::net::UnixStream {
270 loop {
271 match self.listener.accept().await {
272 Ok((stream, _addr)) => {
273 self.health.accepted();
274 return stream;
275 }
276 Err(err) => {
277 if let Some(delay) = self.health.failed(&err) {
278 tokio::time::sleep(delay).await;
279 }
280 }
281 }
282 }
283 }
284}
285
286impl Drop for Listener {
287 fn drop(&mut self) {
288 let _ = fs::remove_file(&self.path);
290 }
291}