mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-07-14 11:21:54 -04:00
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
|
## 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)
|
## v0.7.8 (2023-10-18)
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ lines, meant to be more easily edited by humans.
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
### Starter config
|
||||||
|
|
||||||
The following is a starter config which allows connecting and viewing video with no authentication:
|
The following is a starter config which allows connecting and viewing video with no authentication:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@ -26,6 +28,8 @@ unix = "/var/lib/moonfire-nvr/sock"
|
|||||||
ownUidIsPrivileged = true
|
ownUidIsPrivileged = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authenticated config
|
||||||
|
|
||||||
The following is for a more secure setup with authentication and a TLS proxy
|
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).
|
server in front, as in [guide/secure.md](../guide/secure.md).
|
||||||
|
|
||||||
@ -39,6 +43,57 @@ unix = "/var/lib/moonfire-nvr/sock"
|
|||||||
ownUidIsPrivileged = true
|
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
|
## Reference
|
||||||
|
|
||||||
At the top level, before any `[[bind]]` lines, the following
|
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;
|
* `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.
|
`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.
|
`[[::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
|
* `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
|
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
|
Moonfire NVR instance on `nvr-host` via https://localhost:8080/. If
|
||||||
`ownUidIsPrivileged` is specified (see below), it will additionally
|
`ownUidIsPrivileged` is specified (see below), it will additionally
|
||||||
have all permissions.
|
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]]`:
|
Additional options within `[[binds]]`:
|
||||||
|
|
||||||
|
@ -111,6 +111,10 @@ pub enum AddressConfig {
|
|||||||
|
|
||||||
/// Unix socket path such as `/var/lib/moonfire-nvr/sock`.
|
/// Unix socket path such as `/var/lib/moonfire-nvr/sock`.
|
||||||
Unix(PathBuf),
|
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 db::{dir, writer};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use itertools::Itertools;
|
||||||
use retina::client::SessionGroup;
|
use retina::client::SessionGroup;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@ -23,7 +24,7 @@ use tracing::error;
|
|||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use libsystemd::daemon::{NotifyState, notify};
|
use libsystemd::daemon::{notify, NotifyState};
|
||||||
|
|
||||||
use self::config::ConfigFile;
|
use self::config::ConfigFile;
|
||||||
|
|
||||||
@ -132,6 +133,53 @@ struct Syncer {
|
|||||||
join: thread::JoinHandle<()>,
|
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> {
|
fn read_config(path: &Path) -> Result<ConfigFile, Error> {
|
||||||
let config = std::fs::read(path)?;
|
let config = std::fs::read(path)?;
|
||||||
let config = toml::from_slice(&config).map_err(|e| err!(InvalidArgument, source(e)))?;
|
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);
|
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 {
|
let sa: SocketAddr = match addr {
|
||||||
config::AddressConfig::Ipv4(a) => (*a).into(),
|
config::AddressConfig::Ipv4(a) => (*a).into(),
|
||||||
config::AddressConfig::Ipv6(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())),
|
|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,
|
// 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).
|
// Start the web interface(s).
|
||||||
let own_euid = nix::unistd::Uid::effective();
|
let own_euid = nix::unistd::Uid::effective();
|
||||||
|
let mut preopened = get_preopened_sockets()?;
|
||||||
let web_handles: Result<Vec<_>, Error> = config
|
let web_handles: Result<Vec<_>, Error> = config
|
||||||
.binds
|
.binds
|
||||||
.iter()
|
.iter()
|
||||||
@ -397,13 +469,19 @@ async fn inner(
|
|||||||
move |req| Arc::clone(&svc).serve(req, conn_data)
|
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 = ::hyper::Server::builder(listener).serve(make_svc);
|
||||||
let server = server.with_graceful_shutdown(shutdown_rx.future());
|
let server = server.with_graceful_shutdown(shutdown_rx.future());
|
||||||
Ok(tokio::spawn(server))
|
Ok(tokio::spawn(server))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let web_handles = web_handles?;
|
let web_handles = web_handles?;
|
||||||
|
if !preopened.is_empty() {
|
||||||
|
warn!(
|
||||||
|
"ignoring systemd sockets not referenced in config: {}",
|
||||||
|
preopened.keys().join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user