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