This is a document that is intended to describe WHAT River is, without discussing the details of how it is or should be implemented.
This document is intended for potential users of the River application, with a secondary goal of serving as a "big picture" view for implementers.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 RFC2119 RFC8174 when, and only when, they appear in all capitals, as shown here.
River is a reverse proxy application under design, utilizing the pingora
reverse proxy engine from
Cloudflare. It will be written in Rust. It will be configurable, allowing for options including
routing, filtering, and modification of proxied requests.
The intent is for River to act as a binary distribution of the pingora
engine - providing
a typical application interface for configuration and customization for operators.
For more information on fundamentals of the pingora
library, please refer to the pingora
overview document. For more detailed information on an implementation level,
please refer to the 'what to build' document.
The remainder of this document describes the intended behavior of the River application, including
the subset of capabilities provided by the pingora
library today.
The primary behavior of a reverse proxy application is to act as an intermediary between downstream clients and upstream servers, including termination of TLS for inbound connections if in use. The reverse proxy application may decide to accept or reject the connection at any point, and may decide to modify messages at any point.
┌────────────┐ ┌─────────────┐ ┌────────────┐
│ Downstream │ ┌ ─│─ Proxy ┌ ┼ ─ │ Upstream │
│ Client │─────────▶│ │ │──┼─────▶│ Server │
└────────────┘ │ └───────────┼─┘ └────────────┘
─ ─ ┘ ─ ─ ┘
▲ ▲
┌──┘ └──┐
│ │
┌ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─
Listeners Connectors│
└ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─
Figure 1: Proxying Behavior
River operates by listening to one or more downstream Listener interfaces, accepting connections from clients.
┌────────────┐
│ Downstream │
│ Client │───┐
└────────────┘ │
┌────────────┐ │ ┌─────────────┐ ┌─────────────┐
│ Downstream │ │ │ Listener │ │ Proxy │
│ Client │───┼──▶│ │──────▶│ │
└────────────┘ │ └─────────────┘ └─────────────┘
┌────────────┐ │
│ Downstream │ │
│ Client │───┘
└────────────┘
Figure 2: Listeners
- River MUST accept connections via:
- Unix Domain Sockets
- TCP Sockets
- IPv4
- IPv6
- River MUST support the termination of TLS sessions
- River MUST support the specification of TLS algorithms used for a given downstream listener as a subset of all supported algorithms
- River MUST support the proxying of:
- HTTP0.x/HTTP1.x connections
- HTTP2.0 connections
- River MAY support the proxying of:
- HTTP3.0 connections.
- River MUST support receiving information from protocols used for pre-proxying, including:
- v1 and v2 of the PROXY protocol
- Cloudflare Spectrum
- Akamai X-Forwarded-For (XFF) HTTP header field
River operates by making and maintaining connections to one or more upstream services, forwarding messages from clients.
┌ ─ ─ ─ ─ ─ ─ ─ ─
┌────────────┐ │
│ │ Upstream │
┌───▶│ Server │ │
│ │ └────────────┘
┌─────────────┐ ┌─────────────┐ │ ┌────────────┐ │
│ Proxy │ │ Connector │ │ │ │ Upstream │
│ │──────▶│ │───┘ │ Server │ │
└─────────────┘ └─────────────┘ │ └────────────┘
┌────────────┐ │
│ │ Upstream │
│ Server │ │
│ └────────────┘
─ ─ ─ ─ ─ ─ ─ ─ ┘
Figure 3: A connector communicating with 1/N upstream servers
- River MUST support a configurable Time To Live (TTL) for DNS lookups
- River MUST support a configurable timeouts for:
- Connections
- Requests
- Successful health checks
- River MUST support pooling of connections, including:
- Reuse of TCP sessions for all HTTP versions
- Reuse of HTTP2.0 streams for HTTP2.0
- River MUST support health checks of upstream servers
- River MUST support the disabling of use of an upstream server based on failed health checks
- River MUST support load balancing of upstream servers
- River MUST support sending information for protocols used for pre-proxying, including:
- v1 and v2 of the PROXY protocol
- Cloudflare Spectrum
- Akamai X-Forwarded-For (XFF) HTTP header field
- River MUST support the configurable selection of a subset of upstream servers based on HTTP URI paths
┌────────────────┐
│Upstream Server │
┌────────────▶│ Listing Source │
│ └────────────────┘
Service
Discovery ┌ ─ ─ ─ ─ ─ ─ ─ ─
Requests ┌────────────┐ │
│ │ │ Upstream │
│ ┌───▶│ Server │ │
▼ │ │ └────────────┘
┌─────────────┐ ┌─────────────┐ │ ┌────────────┐ │
│ Proxy │ │ Connector │ │ │ │ Upstream │
│ │──────▶│ │───┘ │ Server │ │
└─────────────┘ └─────────────┘ │ └────────────┘
│ ┌────────────┐ │
Server List │ │ Upstream │
Update │ Server │ │
│ └────────────┘
└ ─ ─ ─ ─ ─ ─▶ ─ ─ ─ ─ ─ ─ ─ ─ ┘
Figure 4: Using Service Discovery to update the list of upstream servers
River allows for the configurable runtime discovery of upstream servers, in order to dynamically handle changing sets of upstream servers without requiring a restart or reconfiguration of the application.
- River MUST support the use of a fixed list of upstream servers
- River MUST support the use of DNS-Service Discovery to provide a list of upstream servers for a given service
- River MUST support the use of SRV records to provide a list of upstream servers for a given service
- River MUST have a configurable timeout for re-polling poll-based service discovery mechanisms
- River MUST support the use of DNS TTL as timeout value for re-polling poll-based service discovery mechanisms
River allows for configurable behavior modifiers at multiple stages in the request and response process. These behaviors allow for the modification or rejection of messages exchanged in either direction between downstream client and upstream server
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ┐
┌───────────┐ ┌───────────┐ ┌───────────┐
│ │ Request │ │ │ │ Request │ │ │ │
Request ═══════▶│ Arrival │═══▶│Which Peer?│═══▶│ Forwarded │═══════▶
│ │ │ │ │ │ │ │ │ │
└───────────┘ └───────────┘ └───────────┘
│ │ │ │ │ │ │
│ │ │
│ ├───On Error─────┼────────────────┤ │ │ Upstream │
│ │ │
│ │ ┌───────────┐ ┌───────────┐ │ │ │
▼ │ Response │ │ Response │
│ │Forwarding │ │ Arrival │ │ │ │
Response ◀════════════════════════│ │◀═══│ │◀═══════
│ └───────────┘ └───────────┘ │ │ │
┌────────────────────────┐
└ ┤ Simplified Phase Chart │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
└────────────────────────┘
Figure 5: Request Path Lifecycle
- River MUST support modifying or rejecting a connection at any of the following stages:
- Downstream Request Arrival
- Peer Selection
- Upstream Request Forwarding
- Upstream Response Arrival
- Downstream Request Forwarding
- Request Body (partial request fragments)
- Response Body (partial response fragments)
- River MUST support rejecting a connection by returning an error response
- River MUST support CIDR/API range-based filtering allow and deny lists
- River MUST support rate limiting of requests or responses on the basis of one or
more of the following:
- A fixed rate per second
- A "burst" rate - allowing for short increases above the fixed rate
- River MUST support application of rate limiting of requests or responses on the per-endpoint basis.
- River MUST support removal of HTTP headers on a glob or regex matching basis
- River MUST support addition of fixed HTTP headers to a request
- River MUST support the normalization of request and response headers and bodies, including:
- URI normalization
- Text encoding
River allows for configurable observability settings, in order to support the operation and maintenance of the system.
- River MUST support emitting structured logs at a configurable level
- River MUST provide quantitative metrics and performance counters
- River MUST support push- and pull-based methods of obtaining structured logs, metrics, and performance counters
- River MUST support emitting logs and metrics locally to file, stdout, or stderr in a consistently structured format.
River MUST provide methods for configuration in order to control the behavior of the reverse proxy application
- River MUST support the configuration of all configurable options via a human editable text file (e.g. TOML, YAML).
- River MUST support emitting a configuration file containing all configuration items and default configuration settings as a command line option
- River MUST support a subset of configuration options at the command line
- River MUST document all command line configurable options via a help command
- River MUST support a subset of configuration options via environment variables
- River MUST support emitting a list of all configuration options configurable via environment variables as a command line option
- River MUST give the following priority to configuration:
- Command Line Options (highest priority)
- Environment Variable Options
- Configuration File Options (lowest priority)
These requirements relate to the supported execution environment(s) of the application.
- The application MUST support execution in a Linux environment
- The application MAY support execution in operating systems such as MacOS, Windows, or Redox OS.
- The application MUST support execution within a container
- The application MUST support two stages of execution:
- The first stage MUST execute with the user and group used to launch the application, and perform initial setup steps
- The second stage MUST be forked from the first stage, executing with the user and group specified in the application configuration
- The application MUST support execution without "root" or "administrator" privileges, given that:
- The user and group used to launch the application has the capability to fork the second stage
- The user and group used to fork the second stage has capabilities necessary for steady state operation.
These requirements relate to the feature of "graceful reloading" - allowing for stopping one instance (referred to as the "Old" instance) of the application and the starting of a second instance (referred to as the "New" instance), handing off existing connections where possible.
- The application MUST support the passing of open Listeners from one instance of the application to another.
- The application MUST support the configuration of an upgrade socket used for both giving and receiving the current Listeners.
- The application MUST allow for a configurable period of time before the termination of in-flight requests handled by the "Old" instance.
- The application MUST allow for a configurable period of time before the termination of active connections handled by the "Old" instance if unable to transfer to the "New" instance.
- The "Old" instance of the application MUST terminate after all in-flight requests and active connections have been transferred to the "New" instance or have been closed after timing out.
These requirements relate to the features of obtaining or renewing TLS certificates automatically without user interaction.
- The application MUST support the use of the Automatic Certificate Management Environment (ACME) protocol to obtain new TLS certificates.
- The application MUST support the use of ACME protocol to renew TLS certificates.
- The application MUST support the configuration of domain names to be managed (including obtaining and renewal steps) automatically
- The application MUST support both fully qualified and wildcard domains.
- The application MUST support configuration of certificate renewal interval, from either:
- The number of days since the certificate was acquired
- The number of days until the certificate will expired
- The application MUST support API Version 2 of the ACME protocol
- The application MAY support API Version 1 of the ACME protocol
The following are development practice requirements for initial implementers of River.
These requirements relate to the technical documentation of River.
- The implementers MUST maintain complete developer-facing documentation, or "doc comments"
- This MAY be achieved using the
#![deny(missing_docs)]
directive or similar flags in CI testing
- This MAY be achieved using the
- The implementers MUST maintain a separate user-facing documentation, describing usage,
configuration, installation, and other details and examples.
- This MAY be achieved using a tool such as
mdBook
, creating a user facing "Book" for River
- This MAY be achieved using a tool such as
- The implementers MUST automatically publish the developer- and user- facing documentation for all released versions
- The implementers MUST automatically publish the developer- and user- facing documentation for
the main development branch
- This MAY be on a per-pull request basis, or on a scheduled basis e.g. once per day.
- The implementers MUST document how to build developer- and user- facing documentation
These requirements relate to the performance benchmarking of River. No specific performance metrics are required or specified here, instead weight is placed on measurements over time, allowing improvements or regressions to be visible and measurable throughout the development process.
- The implementers MUST maintain a test suite of performance tests, expected to exercise:
- Typical Use Cases
- Unusual or "Worst Case" use cases
- Use cases previously reported as performance regressions
- The implementers MUST run and record the results of performance tests on a regular basis, such as on every pull request, or on a scheduled daily/weekly basis.
- The performance tests MUST track the following metrics:
- Peak and Average CPU usage during test execution
- Peak and Average Memory usage during test execution
- CPU and Wall Clock time of test execution
- The performance tests MAY track the following "perf counter" metrics:
- Branch prediction failures
- Page faults
- Cache Misses
- Context Switches
- The implementers MUST document how to build and execute performance tests
- The implementers MAY provide a suite of comparison tests, executing a subset of performance tests against contemporary reverse proxy applications, such as NGINX or Apache.
These requirements document tooling practices expected for the development of River.
- The implementers MUST provide a set of automated checks that are required to pass prior to merges
to the main development branch. These automated checks MAY include:
- Code Formatting checks, e.g.
cargo fmt
- Code linting checks, e.g.
cargo clippy
- Unit test execution, e.g.
cargo test
- Documentation build steps (for user- and developer- facing documentation)
- Integration test execution
- Performance test execution
- Code Formatting checks, e.g.
- The implementers MUST provide a set of automated checks that are required to run on a periodic
basis. These automated checks MAY include:
- Building against the latest stable, beta, or nightly versions of the Rust compiler and toolchain
- Performance test execution
- Documentation build steps
- Documentation publishing steps
- The implementers MUST provide and document the process for running all automated checks locally, in order to allow contributors to perform these checks prior to submitting a Pull Request.
- The implementers MUST provide and enforce a Code of Conduct for contribution
- The implementors MAY use the Contributor Covenant to achieve this goal
- The implementers MUST provide and maintain a Contribution guide for third party contributions
- The implementers MUST provide and maintain a security policy, to allow for private disclosure of vulnerabilities