support systemd socket activation

This commit is contained in:
Scott Lamb 2023-10-19 22:31:39 -07:00
parent 89ee2d0269
commit a2d243d3a4
4 changed files with 151 additions and 7 deletions

View File

@ -10,7 +10,11 @@ even on minor releases, e.g. `v0.7.5` -> `v0.7.6`.
## unreleased
* On Linux, notify `systemd` of starting/stopping.
* `systemd` integration on Linux
* notify `systemd` on starting/stopping. To take advantage of this, you'll
need to modify your `/etc/systemd/moonfire-nvr.service`. See
[`guide/install.md`](guide/install.md).
* socket activation. See [`ref/config.md`](ref/config.md).
## v0.7.8 (2023-10-18)

View File

@ -14,6 +14,8 @@ lines, meant to be more easily edited by humans.
## Examples
### Starter config
The following is a starter config which allows connecting and viewing video with no authentication:
```toml
@ -26,6 +28,8 @@ unix = "/var/lib/moonfire-nvr/sock"
ownUidIsPrivileged = true
```
### Authenticated config
The following is for a more secure setup with authentication and a TLS proxy
server in front, as in [guide/secure.md](../guide/secure.md).
@ -39,6 +43,57 @@ unix = "/var/lib/moonfire-nvr/sock"
ownUidIsPrivileged = true
```
### `systemd` socket activation
`systemd` socket activation (Linux-only) expects `systemd` to create the sockets
on behalf of Moonfire NVR. This can speed startup of services that depend on them and allow
Moonfire to bind to privileged ports (80 or 443) without root privileges. The latter is
expected to be more useful once
[moonfire-nvr#27](https://github.com/scottlamb/moonfire-nvr/issues/27) is
complete and Moonfire is suitable for direct use as an Internet-facing webserver.
To set this up, you'll need an additional systemd unit file for each socket and
to reference them from `/etc/moonfire-nvr.toml`. Be sure to run `sudo systemctl
daemon-reload` to tell `systemd` to read in the new unit files. Your
`moonfire-nvr.service` file should also `Requires=` each socket file.
#### `/etc/moonfire-nvr.toml`
```toml
[[binds]]
systemd = "moonfire-nvr-tcp.socket"
allowUnauthenticatedPermissions = { viewVideo = true }
[[binds]]
systemd = "moonfire-nvr-unix.socket"
ownUidIsPrivileged = true
```
### `/etc/systemd/system/moonfire-nvr.service`
```ini
[Unit]
Requires=moonfire-nvr-tcp.socket
Requires=moonfire-nvr-unix.socket
# ...rest as before...
```
### `/etc/systemd/system/moonfire-nvr-tcp.socket`
```ini
[Socket]
ListenStream=80
Service=moonfire-nvr.service
```
### `/etc/systemd/system/moonfire-nvr-unix.socket`
```ini
[Socket]
ListenStream=/var/lib/moonfire-nvr/sock
Service=moonfire-nvr.service
```
## Reference
At the top level, before any `[[bind]]` lines, the following
@ -55,7 +110,7 @@ should start with a `[[binds]]` line and specify one of the following:
* `ipv4`: an IPv4 socket address. `0.0.0.0:8080` would allow connections from outside the machine;
`127.0.0.1:8080` would allow connections only from the local host.
* `ipv6`: an IPv6 socket address. [::0]:8080` would allow connections from outside the machine;
* `ipv6`: an IPv6 socket address. `[::0]:8080` would allow connections from outside the machine;
`[[::1]:8080` would allow connections from only the local host.
* `unix`: a path in the local filesystem where a UNIX-domain socket can be created. Permissions on the
enclosing directories control which users are allowed to connect to it. Web browsers typically don't
@ -68,6 +123,9 @@ should start with a `[[binds]]` line and specify one of the following:
Moonfire NVR instance on `nvr-host` via https://localhost:8080/. If
`ownUidIsPrivileged` is specified (see below), it will additionally
have all permissions.
* `systemd` (Linux-only): a name of a socket passed from `systemd`. See
[`systemd.socket(5)`](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html)
for more information, or the example above.
Additional options within `[[binds]]`:

View File

@ -111,6 +111,10 @@ pub enum AddressConfig {
/// Unix socket path such as `/var/lib/moonfire-nvr/sock`.
Unix(PathBuf),
// TODO: SystemdFileDescriptorName(String), see
// https://www.freedesktop.org/software/systemd/man/systemd.socket.html
/// `systemd` socket activation.
///
/// See [systemd.socket(5) manual
/// page](https://www.freedesktop.org/software/systemd/man/systemd.socket.html).
Systemd(String),
}

View File

@ -12,6 +12,7 @@ use bpaf::Bpaf;
use db::{dir, writer};
use fnv::FnvHashMap;
use hyper::service::{make_service_fn, service_fn};
use itertools::Itertools;
use retina::client::SessionGroup;
use std::net::SocketAddr;
use std::path::Path;
@ -23,7 +24,7 @@ use tracing::error;
use tracing::{info, warn};
#[cfg(target_os = "linux")]
use libsystemd::daemon::{NotifyState, notify};
use libsystemd::daemon::{notify, NotifyState};
use self::config::ConfigFile;
@ -132,6 +133,53 @@ struct Syncer {
join: thread::JoinHandle<()>,
}
#[cfg(target_os = "linux")]
fn get_preopened_sockets() -> Result<FnvHashMap<String, Listener>, Error> {
use libsystemd::activation::IsType as _;
use std::os::fd::{FromRawFd, IntoRawFd};
// `receive_descriptors_with_names` errors out if not running under systemd or not using socket
// activation.
if std::env::var_os("LISTEN_FDS").is_none() {
info!("no LISTEN_FDs");
return Ok(FnvHashMap::default());
}
let sockets = libsystemd::activation::receive_descriptors_with_names(false)
.map_err(|e| err!(Unknown, source(e), msg("unable to receive systemd sockets")))?;
sockets
.into_iter()
.map(|(fd, name)| {
if fd.is_unix() {
// SAFETY: yes, it's a socket we own.
let l = unsafe { std::os::unix::net::UnixListener::from_raw_fd(fd.into_raw_fd()) };
l.set_nonblocking(true)?;
Ok(Some((
name,
Listener::Unix(tokio::net::UnixListener::from_std(l)?),
)))
} else if fd.is_inet() {
// SAFETY: yes, it's a socket we own.
let l = unsafe { std::net::TcpListener::from_raw_fd(fd.into_raw_fd()) };
l.set_nonblocking(true)?;
Ok(Some((
name,
Listener::Tcp(tokio::net::TcpListener::from_std(l)?),
)))
} else {
warn!("ignoring systemd socket {name:?} which is not unix or inet");
Ok(None)
}
})
.filter_map(Result::transpose)
.collect()
}
#[cfg(not(target_os = "linux"))]
fn get_preopened_sockets() -> Result<FnvHashMap<String, Listener>, Error> {
Ok(FnvHashMap::default())
}
fn read_config(path: &Path) -> Result<ConfigFile, Error> {
let config = std::fs::read(path)?;
let config = toml::from_slice(&config).map_err(|e| err!(InvalidArgument, source(e)))?;
@ -217,7 +265,13 @@ fn prepare_unix_socket(p: &Path) {
let _ = nix::unistd::unlink(p);
}
fn make_listener(addr: &config::AddressConfig) -> Result<Listener, Error> {
fn make_listener(
addr: &config::AddressConfig,
#[cfg_attr(not(target_os = "linux"), allow(unused))] preopened: &mut FnvHashMap<
String,
Listener,
>,
) -> Result<Listener, Error> {
let sa: SocketAddr = match addr {
config::AddressConfig::Ipv4(a) => (*a).into(),
config::AddressConfig::Ipv6(a) => (*a).into(),
@ -227,6 +281,23 @@ fn make_listener(addr: &config::AddressConfig) -> Result<Listener, Error> {
|e| err!(e, msg("unable bind Unix socket {}", p.display())),
)?));
}
#[cfg(target_os = "linux")]
config::AddressConfig::Systemd(n) => {
return preopened.remove(n).ok_or_else(|| {
err!(
NotFound,
msg(
"can't find systemd socket named {}; available sockets are: {}",
n,
preopened.keys().join(", ")
)
)
});
}
#[cfg(not(target_os = "linux"))]
config::AddressConfig::Systemd(_) => {
bail!(Unimplemented, msg("systemd sockets are Linux-only"))
}
};
// Go through std::net::TcpListener to avoid needing async. That's there for DNS resolution,
@ -375,6 +446,7 @@ async fn inner(
// Start the web interface(s).
let own_euid = nix::unistd::Uid::effective();
let mut preopened = get_preopened_sockets()?;
let web_handles: Result<Vec<_>, Error> = config
.binds
.iter()
@ -397,13 +469,19 @@ async fn inner(
move |req| Arc::clone(&svc).serve(req, conn_data)
}))
});
let listener = make_listener(&b.address)?;
let listener = make_listener(&b.address, &mut preopened)?;
let server = ::hyper::Server::builder(listener).serve(make_svc);
let server = server.with_graceful_shutdown(shutdown_rx.future());
Ok(tokio::spawn(server))
})
.collect();
let web_handles = web_handles?;
if !preopened.is_empty() {
warn!(
"ignoring systemd sockets not referenced in config: {}",
preopened.keys().join(", ")
);
}
#[cfg(target_os = "linux")]
{