Skip to content

Commit 21bdf8c

Browse files
authored
Merge pull request #40 from tim-seoss/listenfd
feat: support inheriting tcp listener from parent process via fd0
2 parents c3389cc + e02f5bc commit 21bdf8c

File tree

9 files changed

+221
-9
lines changed

9 files changed

+221
-9
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pin-project = "1.0"
4545
tokio-rustls = { version = "0.22" }
4646
humansize = "1.1"
4747
time = "0.1"
48+
listenfd = "0.3.3"
4849

4950
[target.'cfg(not(windows))'.dependencies.nix]
5051
version = "0.14"

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- Configurable using CLI arguments or environment variables.
2828
- 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.
2929
- MacOs binary support thanks to [Rust Linux / Darwin Builder](https://github.com/joseluisq/rust-linux-darwin-builder).
30+
- The ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications.
3031

3132
## Releases
3233

@@ -45,6 +46,7 @@ Server can be configured either via environment variables or their equivalent co
4546
| --- | --- | --- |
4647
| `SERVER_HOST` | Host address (E.g 127.0.0.1). | Default `[::]`. |
4748
| `SERVER_PORT` | Host port. | Default `80`. |
49+
| `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` ). |
4850
| `SERVER_ROOT` | Root directory path of static | Default `./public`. |
4951
| `SERVER_LOG_LEVEL` | Specify a logging level in lower case. (Values `error`, `warn`, `info`, `debug`, `trace`). | Default `error` |
5052
| `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:
8284
-z, --directory-listing <directory-listing>
8385
Enable directory listing for all requests ending with the slash character (‘/’) [env:
8486
SERVER_DIRECTORY_LISTING=] [default: false]
87+
-f, --fd <fd>
88+
Instead of binding to a TCP port, accept incoming connections to an already-bound TCP socket listener on the
89+
specified file descriptor number (usually zero). Requires that the parent process (e.g. inetd, launchd, or
90+
systemd) binds an address and port on behalf of static-web-server, before arranging for the resulting file
91+
descriptor to be inherited by static-web-server. Cannot be used in conjunction with the port and host
92+
arguments [env: SERVER_LISTEN_FD=]
8593
-a, --host <host>
8694
Host address (E.g 127.0.0.1 or ::1) [env: SERVER_HOST=] [default: ::]
8795
@@ -115,6 +123,16 @@ OPTIONS:
115123
[default: 1]
116124
```
117125

126+
## Use of file descriptor socket passing
127+
128+
Example `systemd` unit files for socket activation are included in the [`systemd/`](systemd/) directory. If
129+
using `inetd`, its "`wait`" option should be used in conjunction with static-web-server's `--fd 0`
130+
option.
131+
132+
Alternatively, the light-weight [`systemfd`](https://github.com/mitsuhiko/systemfd) utility may be
133+
useful - especially for testing e.g.
134+
`systemfd --no-pid -s http::8091 -- path/to/static-web-server --fd 0`
135+
118136
## Docker stack
119137

120138
Example using [Traefik Proxy](https://traefik.io/):

src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ pub struct Config {
1111
/// Host port
1212
pub port: u16,
1313

14+
#[structopt(
15+
long,
16+
short = "f",
17+
env = "SERVER_LISTEN_FD",
18+
conflicts_with_all(&["host", "port"])
19+
)]
20+
/// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP
21+
/// socket listener on the specified file descriptor number (usually zero). Requires that the
22+
/// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of
23+
/// static-web-server, before arranging for the resulting file descriptor to be inherited by
24+
/// static-web-server. Cannot be used in conjunction with the port and host arguments. The
25+
/// included systemd unit file utilises this feature to increase security by allowing the
26+
/// static-web-server to be sandboxed more completely.
27+
pub fd: Option<usize>,
28+
1429
#[structopt(
1530
long,
1631
short = "n",

src/logger.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::io;
12
use tracing::Level;
23
use tracing_subscriber::fmt::format::FmtSpan;
34

@@ -7,6 +8,7 @@ use crate::Result;
78
pub fn init(level: &str) -> Result {
89
let level = level.parse::<Level>()?;
910
match tracing_subscriber::fmt()
11+
.with_writer(io::stderr)
1012
.with_max_level(level)
1113
.with_span_events(FmtSpan::CLOSE)
1214
.try_init()

src/server.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use hyper::server::conn::AddrIncoming;
22
use hyper::server::Server as HyperServer;
33
use hyper::service::{make_service_fn, service_fn};
4-
use std::net::{IpAddr, SocketAddr};
4+
use listenfd::ListenFd;
5+
use std::net::{IpAddr, SocketAddr, TcpListener};
56
use std::sync::Arc;
67
use structopt::StructOpt;
78

@@ -58,8 +59,26 @@ impl Server {
5859

5960
tracing::info!("runtime worker threads: {}", self.threads);
6061

61-
let ip = opts.host.parse::<IpAddr>()?;
62-
let addr = SocketAddr::from((ip, opts.port));
62+
let (tcplistener, addr_string);
63+
match opts.fd {
64+
Some(fd) => {
65+
addr_string = format!("@FD({})", fd);
66+
tcplistener = ListenFd::from_env()
67+
.take_tcp_listener(fd)?
68+
.expect("Failed to convert inherited FD into a a TCP listener");
69+
tracing::info!(
70+
"Converted inherited file descriptor {} to a TCP listener",
71+
fd
72+
);
73+
}
74+
None => {
75+
let ip = opts.host.parse::<IpAddr>()?;
76+
let addr = SocketAddr::from((ip, opts.port));
77+
tcplistener = TcpListener::bind(addr)?;
78+
addr_string = format!("{:?}", addr);
79+
tracing::info!("Bound to TCP socket {}", addr_string);
80+
}
81+
}
6382

6483
// Check for a valid root directory
6584
let root_dir = helpers::get_valid_dirpath(&opts.root)?;
@@ -111,7 +130,12 @@ impl Server {
111130
}
112131
});
113132

114-
let mut incoming = AddrIncoming::bind(&addr)?;
133+
tcplistener
134+
.set_nonblocking(true)
135+
.expect("Cannot set non-blocking");
136+
let listener = tokio::net::TcpListener::from_std(tcplistener)
137+
.expect("Failed to create tokio::net::TcpListener");
138+
let mut incoming = AddrIncoming::from_listener(listener)?;
115139
incoming.set_nodelay(true);
116140

117141
let tls = TlsConfigBuilder::new()
@@ -124,9 +148,9 @@ impl Server {
124148
HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(make_service);
125149

126150
tracing::info!(
127-
parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
151+
parent: tracing::info_span!("Server::start_server", ?addr_string, ?threads),
128152
"listening on https://{}",
129-
addr
153+
addr_string
130154
);
131155

132156
server.await
@@ -153,14 +177,15 @@ impl Server {
153177
}
154178
});
155179

156-
let server = HyperServer::bind(&addr)
180+
let server = HyperServer::from_tcp(tcplistener)
181+
.unwrap()
157182
.tcp_nodelay(true)
158183
.serve(make_service);
159184

160185
tracing::info!(
161-
parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
186+
parent: tracing::info_span!("Server::start_server", ?addr_string, ?threads),
162187
"listening on http://{}",
163-
addr
188+
addr_string
164189
);
165190

166191
server.await

systemd/etc_default_static-web-server

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SERVER_ROOT=/var/www/html
2+
SERVER_HTTP2_TLS=true
3+
SERVER_HTTP2_TLS_CERT=/etc/static-web-server/example_org_fullchain.pem
4+
SERVER_HTTP2_TLS_KEY=/etc/static-web-server/example_org_privkey.pem
5+
SERVER_LOG_LEVEL=warn

systemd/static-web-server.service

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Example systemd service unit file (see systemd.service(5) man page) for use
2+
# with the --fd option of static-web-server. This allows e.g. binding the
3+
# server to a TCP port number 0 - 1023 without running the server as root,
4+
# and/or running sws in an isolated network name space.
5+
#
6+
# This also allows sws to be started on-demand. If sws is restart (e.g. after
7+
# updating its SSL certificates, or reconfiguring its content directory), new
8+
# inbound connections will be queued until sws is up and running again.
9+
#
10+
# A comprehensive description can be found in:
11+
# http://0pointer.de/blog/projects/socket-activation.html
12+
# ...and the linked articles.
13+
14+
[Unit]
15+
Description=Static Web Server
16+
Wants=static-web-server.socket
17+
After=static-web-server.socket
18+
19+
# The options below reflect a reasonably comprehensive sandboxing based on the
20+
# features available in systemd v247. Newer versions of systemd may offer
21+
# additional options for sandboxing.
22+
#
23+
# The options below focus on security, when making changes to this unit file
24+
# you may wish to evaluated the output of:
25+
# systemd-analyze security static-web-server.service
26+
#
27+
# Beyond the limits used here, additional limits can be placed on CPU, memory,
28+
# and disk I/O, as well as network traffic filters (via eBPF and other
29+
# mechanisms), and implemented for this server using the systemd override
30+
# facilities. See systemd.resource-control(5) for details.
31+
32+
[Service]
33+
Type=simple
34+
35+
# An example environment file for static-web-server is included in the file:
36+
# systemd/etc_default_static-web-server
37+
EnvironmentFile=/etc/default/static-web-server
38+
39+
# File descriptor 0 corresponds to the standard input...
40+
ExecStart=/usr/local/bin/static-web-server --fd 0
41+
42+
# ...so the following line attaches fd 0 of the static web server process to
43+
# the socket defined by the corresponding `static-web-server.socket` unit file.
44+
# Each instance of static-web-server currently only supports listening on a
45+
# single socket.
46+
StandardInput=fd:static-web-server.socket
47+
48+
# Debug and tracing output goes to stderr, and can be viewed with e.g.
49+
# `journalctl -u static-web-server.service`.
50+
StandardError=journal
51+
52+
Restart=always
53+
RestartSec=5
54+
DynamicUser=true
55+
SupplementaryGroups=www-data
56+
NoNewPrivileges=yes
57+
PrivateTmp=yes
58+
ProtectSystem=strict
59+
ProtectHome=yes
60+
CapabilityBoundingSet=
61+
RestrictNamespaces=true
62+
63+
#RestrictAddressFamilies=none
64+
# ☟ workaround to implement ☝in older versions of systemd.
65+
# see: https://github.com/systemd/systemd/issues/15753
66+
RestrictAddressFamilies=AF_UNIX
67+
RestrictAddressFamilies=~AF_UNIX
68+
69+
PrivateDevices=true
70+
PrivateUsers=true
71+
PrivateNetwork=true
72+
ProtectClock=true
73+
ProtectControlGroups=true
74+
ProtectKernelLogs=true
75+
ProtectKernelModules=true
76+
ProtectKernelTunables=true
77+
ProtectProc=invisible
78+
ProcSubset=pid
79+
RestrictSUIDSGID=true
80+
SystemCallArchitectures=native
81+
RestrictRealtime=true
82+
LockPersonality=true
83+
RemoveIPC=true
84+
MemoryDenyWriteExecute=true
85+
UMask=077
86+
ProtectHostname=true
87+
88+
# Restrict the use of exotic system calls (bugs in seldom-used syscalls are a
89+
# historical source of kernel vulnerabilities)...
90+
SystemCallFilter=@system-service
91+
# ... It may be possible to restrict this further. e.g.
92+
#SystemCallFilter=@signal @basic-io @io-event @network-io @process statx fstat sched_getaffinity getrandom
93+
# but a process to discover the set of system calls used (e.g. as part of the
94+
# unit tests) will probably be needed to avoid regressions e.g. due to changes
95+
# in crates which are used by static-web-server. The following may be useful to
96+
# record system calls performed:
97+
# "/usr/bin/strace --summary-only -o sws.syscallstats -- static-web-server [...]"
98+
# You can view the sets of system calls defined by systemd using:
99+
# "systemd-analyze syscall-filter"
100+
101+
DevicePolicy=strict
102+
DeviceAllow=/dev/null rw
103+
DeviceAllow=/dev/random r
104+
DeviceAllow=/dev/urandom r
105+
106+
[Install]
107+
WantedBy=multi-user.target

systemd/static-web-server.socket

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Example systemd socket unit file (see systemd.socket(5) man page) for use
2+
# with the --fd option of static-web-server. This allows e.g. binding the
3+
# server to a TCP port number 0 - 1023 without running the server as root,
4+
# and/or running sws in an isolated network name space.
5+
#
6+
# A comprehensive description can be found in:
7+
# http://0pointer.de/blog/projects/socket-activation.html
8+
# ...and the linked articles.
9+
10+
[Unit]
11+
Description=Static Web Server Socket
12+
13+
[Socket]
14+
ListenStream=443
15+
Accept=no
16+
17+
[Install]
18+
WantedBy=sockets.target

0 commit comments

Comments
 (0)