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}
172
173impl Listener {
174 pub async fn bind(path: impl AsRef<Path>) -> Result<Self> {
180 let path = path.as_ref().to_path_buf();
181
182 match fs::symlink_metadata(&path) {
186 Ok(meta) if meta.file_type().is_socket() => fs::remove_file(&path)?,
187 Ok(_) => return Err(Error::NotASocket(path)),
188 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
189 Err(err) => return Err(err.into()),
190 }
191
192 let listener = tokio::net::UnixListener::bind(&path)?;
193 Ok(Self {
194 listener,
195 path,
196 protocols: Vec::new(),
197 })
198 }
199
200 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
203 where
204 I: IntoIterator<Item = S>,
205 S: Into<String>,
206 {
207 self.protocols = protocols.into_iter().map(Into::into).collect();
208 self
209 }
210
211 pub fn set_mode(&self, mode: u32) -> Result<()> {
213 fs::set_permissions(&self.path, fs::Permissions::from_mode(mode))?;
214 Ok(())
215 }
216
217 pub fn path(&self) -> &Path {
219 &self.path
220 }
221
222 pub async fn accept(&self) -> Option<Result<(qmux::Session, PeerCred)>> {
227 match self.listener.accept().await {
228 Ok((stream, _addr)) => {
229 let cred = match stream.peer_cred() {
230 Ok(cred) => PeerCred {
231 uid: cred.uid(),
232 gid: cred.gid(),
233 pid: cred.pid(),
234 },
235 Err(err) => return Some(Err(err.into())),
236 };
237 let session = qmux::uds::Config::new(WIRE_VERSION)
238 .protocols(self.protocols.iter().map(String::as_str))
239 .accept(stream)
240 .await
241 .map_err(Error::Accept);
242 Some(session.map(|session| (session, cred)))
243 }
244 Err(err) => Some(Err(err.into())),
245 }
246 }
247}
248
249impl Drop for Listener {
250 fn drop(&mut self) {
251 let _ = fs::remove_file(&self.path);
253 }
254}