support systemd socket activation
This commit is contained in:
parent
89ee2d0269
commit
a2d243d3a4
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]]`:
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue