Skip to content

Commit

Permalink
Merge pull request #40 from tim-seoss/listenfd
Browse files Browse the repository at this point in the history
feat: support inheriting tcp listener from parent process via fd0
  • Loading branch information
joseluisq authored May 31, 2021
2 parents c3389cc + e02f5bc commit 21bdf8c
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 9 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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`. |
Expand Down Expand Up @@ -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: ::]
Expand Down Expand Up @@ -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/):
Expand Down
15 changes: 15 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ pub struct Config {
/// Host port
pub port: u16,

#[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",
Expand Down
2 changes: 2 additions & 0 deletions src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::io;
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;

Expand All @@ -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()
Expand Down
43 changes: 34 additions & 9 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions systemd/etc_default_static-web-server
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions systemd/static-web-server.service
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions systemd/static-web-server.socket
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 21bdf8c

Please sign in to comment.