index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2021-05-31 13:04:21.0 +00:00:00
committer GitHub <noreply@github.com> 2021-05-31 13:04:21.0 +00:00:00
commit
21bdf8c26abd5d11b704b9345e539abadf4ff18a [patch]
tree
5a337f160bca877ef724037d6356562c67b4239c
parent
c3389cc173e969a54b46b9f16f1e49cd2a822343
parent
e02f5bceb59c10bc71b959b548a2bcf103b0b68c
download
21bdf8c26abd5d11b704b9345e539abadf4ff18a.tar.gz

Merge pull request #40 from tim-seoss/listenfd

feat: support inheriting tcp listener from parent process via fd0

Diff

 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(-)

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 <directory-listing>
            Enable directory listing for all requests ending with the slash character (β€˜/’) [env:
            SERVER_DIRECTORY_LISTING=]  [default: false]
    -f, --fd <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>
            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<usize>,

    #[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::<Level>()?;
    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::<IpAddr>()?;
        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::<IpAddr>()?;
                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