From 21bdf8c26abd5d11b704b9345e539abadf4ff18a Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Mon, 31 May 2021 15:04:21 +0200 Subject: [PATCH] Merge pull request #40 from tim-seoss/listenfd feat: support inheriting tcp listener from parent process via fd0 --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 1 + README.md | 18 ++++++++++++++++++ src/config.rs | 15 +++++++++++++++ src/logger.rs | 2 ++ src/server.rs | 43 ++++++++++++++++++++++++++++++++++--------- systemd/etc_default_static-web-server | 5 +++++ systemd/static-web-server.service | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ systemd/static-web-server.socket | 18 ++++++++++++++++++ 9 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 systemd/etc_default_static-web-server create mode 100644 systemd/static-web-server.service create mode 100644 systemd/static-web-server.socket diff --git a/Cargo.lock b/Cargo.lock index 1e50970..94ff916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" [[package]] +name = "listenfd" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "492158e732f2e2de81c592f0a2427e57e12cd3d59877378fe7af624b6bbe0ca1" +dependencies = [ + "libc", + "uuid", + "winapi", +] + +[[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -858,6 +869,7 @@ dependencies = [ "humansize", "hyper", "jemallocator", + "listenfd", "mime_guess", "nix", "num_cpus", @@ -1117,6 +1129,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] +name = "uuid" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index b3dc9f0..a51c49b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ pin-project = "1.0" tokio-rustls = { version = "0.22" } humansize = "1.1" time = "0.1" +listenfd = "0.3.3" [target.'cfg(not(windows))'.dependencies.nix] version = "0.14" diff --git a/README.md b/README.md index 5876c54..0287a26 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - Configurable using CLI arguments or environment variables. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available. - MacOs binary support thanks to [Rust Linux / Darwin Builder](https://github.com/joseluisq/rust-linux-darwin-builder). +- The ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications. ## Releases @@ -45,6 +46,7 @@ Server can be configured either via environment variables or their equivalent co | --- | --- | --- | | `SERVER_HOST` | Host address (E.g 127.0.0.1). | Default `[::]`. | | `SERVER_PORT` | Host port. | Default `80`. | +| `SERVER_LISTEN_FD` | Optional file descriptor number (e.g. `0`) to inherit an already-opened TCP listener on (instead of using `SERVER_HOST` and/or `SERVER_PORT` ). | | `SERVER_ROOT` | Root directory path of static | Default `./public`. | | `SERVER_LOG_LEVEL` | Specify a logging level in lower case. (Values `error`, `warn`, `info`, `debug`, `trace`). | Default `error` | | `SERVER_ERROR_PAGE_404` | HTML file path for 404 errors. | If path is not specified or simply don't exists then server will use a generic HTML error message. Default `./public/404.html`. | @@ -82,6 +84,12 @@ OPTIONS: -z, --directory-listing Enable directory listing for all requests ending with the slash character (β€˜/’) [env: SERVER_DIRECTORY_LISTING=] [default: false] + -f, --fd + Instead of binding to a TCP port, accept incoming connections to an already-bound TCP socket listener on the + specified file descriptor number (usually zero). Requires that the parent process (e.g. inetd, launchd, or + systemd) binds an address and port on behalf of static-web-server, before arranging for the resulting file + descriptor to be inherited by static-web-server. Cannot be used in conjunction with the port and host + arguments [env: SERVER_LISTEN_FD=] -a, --host Host address (E.g 127.0.0.1 or ::1) [env: SERVER_HOST=] [default: ::] @@ -115,6 +123,16 @@ OPTIONS: [default: 1] ``` +## Use of file descriptor socket passing + +Example `systemd` unit files for socket activation are included in the [`systemd/`](systemd/) directory. If +using `inetd`, its "`wait`" option should be used in conjunction with static-web-server's `--fd 0` +option. + +Alternatively, the light-weight [`systemfd`](https://github.com/mitsuhiko/systemfd) utility may be +useful - especially for testing e.g. +`systemfd --no-pid -s http::8091 -- path/to/static-web-server --fd 0` + ## Docker stack Example using [Traefik Proxy](https://traefik.io/): diff --git a/src/config.rs b/src/config.rs index f77f09e..5966f1a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,21 @@ pub struct Config { #[structopt( long, + short = "f", + env = "SERVER_LISTEN_FD", + conflicts_with_all(&["host", "port"]) + )] + /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP + /// socket listener on the specified file descriptor number (usually zero). Requires that the + /// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of + /// static-web-server, before arranging for the resulting file descriptor to be inherited by + /// static-web-server. Cannot be used in conjunction with the port and host arguments. The + /// included systemd unit file utilises this feature to increase security by allowing the + /// static-web-server to be sandboxed more completely. + pub fd: Option, + + #[structopt( + long, short = "n", default_value = "1", env = "SERVER_THREADS_MULTIPLIER" diff --git a/src/logger.rs b/src/logger.rs index b2b1f75..b2e6271 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,3 +1,4 @@ +use std::io; use tracing::Level; use tracing_subscriber::fmt::format::FmtSpan; @@ -7,6 +8,7 @@ use crate::Result; pub fn init(level: &str) -> Result { let level = level.parse::()?; match tracing_subscriber::fmt() + .with_writer(io::stderr) .with_max_level(level) .with_span_events(FmtSpan::CLOSE) .try_init() diff --git a/src/server.rs b/src/server.rs index 4751a7e..97637a1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,8 @@ use hyper::server::conn::AddrIncoming; use hyper::server::Server as HyperServer; use hyper::service::{make_service_fn, service_fn}; -use std::net::{IpAddr, SocketAddr}; +use listenfd::ListenFd; +use std::net::{IpAddr, SocketAddr, TcpListener}; use std::sync::Arc; use structopt::StructOpt; @@ -58,8 +59,26 @@ impl Server { tracing::info!("runtime worker threads: {}", self.threads); - let ip = opts.host.parse::()?; - let addr = SocketAddr::from((ip, opts.port)); + let (tcplistener, addr_string); + match opts.fd { + Some(fd) => { + addr_string = format!("@FD({})", fd); + tcplistener = ListenFd::from_env() + .take_tcp_listener(fd)? + .expect("Failed to convert inherited FD into a a TCP listener"); + tracing::info!( + "Converted inherited file descriptor {} to a TCP listener", + fd + ); + } + None => { + let ip = opts.host.parse::()?; + let addr = SocketAddr::from((ip, opts.port)); + tcplistener = TcpListener::bind(addr)?; + addr_string = format!("{:?}", addr); + tracing::info!("Bound to TCP socket {}", addr_string); + } + } // Check for a valid root directory let root_dir = helpers::get_valid_dirpath(&opts.root)?; @@ -111,7 +130,12 @@ impl Server { } }); - let mut incoming = AddrIncoming::bind(&addr)?; + tcplistener + .set_nonblocking(true) + .expect("Cannot set non-blocking"); + let listener = tokio::net::TcpListener::from_std(tcplistener) + .expect("Failed to create tokio::net::TcpListener"); + let mut incoming = AddrIncoming::from_listener(listener)?; incoming.set_nodelay(true); let tls = TlsConfigBuilder::new() @@ -124,9 +148,9 @@ impl Server { HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(make_service); tracing::info!( - parent: tracing::info_span!("Server::start_server", ?addr, ?threads), + parent: tracing::info_span!("Server::start_server", ?addr_string, ?threads), "listening on https://{}", - addr + addr_string ); server.await @@ -153,14 +177,15 @@ impl Server { } }); - let server = HyperServer::bind(&addr) + let server = HyperServer::from_tcp(tcplistener) + .unwrap() .tcp_nodelay(true) .serve(make_service); tracing::info!( - parent: tracing::info_span!("Server::start_server", ?addr, ?threads), + parent: tracing::info_span!("Server::start_server", ?addr_string, ?threads), "listening on http://{}", - addr + addr_string ); server.await diff --git a/systemd/etc_default_static-web-server b/systemd/etc_default_static-web-server new file mode 100644 index 0000000..1e0e952 --- /dev/null +++ b/systemd/etc_default_static-web-server @@ -0,0 +1,5 @@ +SERVER_ROOT=/var/www/html +SERVER_HTTP2_TLS=true +SERVER_HTTP2_TLS_CERT=/etc/static-web-server/example_org_fullchain.pem +SERVER_HTTP2_TLS_KEY=/etc/static-web-server/example_org_privkey.pem +SERVER_LOG_LEVEL=warn diff --git a/systemd/static-web-server.service b/systemd/static-web-server.service new file mode 100644 index 0000000..e9a7f02 --- /dev/null +++ b/systemd/static-web-server.service @@ -0,0 +1,107 @@ +# Example systemd service unit file (see systemd.service(5) man page) for use +# with the --fd option of static-web-server. This allows e.g. binding the +# server to a TCP port number 0 - 1023 without running the server as root, +# and/or running sws in an isolated network name space. +# +# This also allows sws to be started on-demand. If sws is restart (e.g. after +# updating its SSL certificates, or reconfiguring its content directory), new +# inbound connections will be queued until sws is up and running again. +# +# A comprehensive description can be found in: +# http://0pointer.de/blog/projects/socket-activation.html +# ...and the linked articles. + +[Unit] +Description=Static Web Server +Wants=static-web-server.socket +After=static-web-server.socket + +# The options below reflect a reasonably comprehensive sandboxing based on the +# features available in systemd v247. Newer versions of systemd may offer +# additional options for sandboxing. +# +# The options below focus on security, when making changes to this unit file +# you may wish to evaluated the output of: +# systemd-analyze security static-web-server.service +# +# Beyond the limits used here, additional limits can be placed on CPU, memory, +# and disk I/O, as well as network traffic filters (via eBPF and other +# mechanisms), and implemented for this server using the systemd override +# facilities. See systemd.resource-control(5) for details. + +[Service] +Type=simple + +# An example environment file for static-web-server is included in the file: +# systemd/etc_default_static-web-server +EnvironmentFile=/etc/default/static-web-server + +# File descriptor 0 corresponds to the standard input... +ExecStart=/usr/local/bin/static-web-server --fd 0 + +# ...so the following line attaches fd 0 of the static web server process to +# the socket defined by the corresponding `static-web-server.socket` unit file. +# Each instance of static-web-server currently only supports listening on a +# single socket. +StandardInput=fd:static-web-server.socket + +# Debug and tracing output goes to stderr, and can be viewed with e.g. +# `journalctl -u static-web-server.service`. +StandardError=journal + +Restart=always +RestartSec=5 +DynamicUser=true +SupplementaryGroups=www-data +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +CapabilityBoundingSet= +RestrictNamespaces=true + +#RestrictAddressFamilies=none +# ☟ workaround to implement ☝in older versions of systemd. +# see: https://github.com/systemd/systemd/issues/15753 +RestrictAddressFamilies=AF_UNIX +RestrictAddressFamilies=~AF_UNIX + +PrivateDevices=true +PrivateUsers=true +PrivateNetwork=true +ProtectClock=true +ProtectControlGroups=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +SystemCallArchitectures=native +RestrictRealtime=true +LockPersonality=true +RemoveIPC=true +MemoryDenyWriteExecute=true +UMask=077 +ProtectHostname=true + +# Restrict the use of exotic system calls (bugs in seldom-used syscalls are a +# historical source of kernel vulnerabilities)... +SystemCallFilter=@system-service +# ... It may be possible to restrict this further. e.g. +#SystemCallFilter=@signal @basic-io @io-event @network-io @process statx fstat sched_getaffinity getrandom +# but a process to discover the set of system calls used (e.g. as part of the +# unit tests) will probably be needed to avoid regressions e.g. due to changes +# in crates which are used by static-web-server. The following may be useful to +# record system calls performed: +# "/usr/bin/strace --summary-only -o sws.syscallstats -- static-web-server [...]" +# You can view the sets of system calls defined by systemd using: +# "systemd-analyze syscall-filter" + +DevicePolicy=strict +DeviceAllow=/dev/null rw +DeviceAllow=/dev/random r +DeviceAllow=/dev/urandom r + +[Install] +WantedBy=multi-user.target diff --git a/systemd/static-web-server.socket b/systemd/static-web-server.socket new file mode 100644 index 0000000..d2f8b26 --- /dev/null +++ b/systemd/static-web-server.socket @@ -0,0 +1,18 @@ +# Example systemd socket unit file (see systemd.socket(5) man page) for use +# with the --fd option of static-web-server. This allows e.g. binding the +# server to a TCP port number 0 - 1023 without running the server as root, +# and/or running sws in an isolated network name space. +# +# A comprehensive description can be found in: +# http://0pointer.de/blog/projects/socket-activation.html +# ...and the linked articles. + +[Unit] +Description=Static Web Server Socket + +[Socket] +ListenStream=443 +Accept=no + +[Install] +WantedBy=sockets.target -- libgit2 1.7.2