This project implements a TLS reverse proxy that supports routing traffic via
the TLS connection parameters and supports ECH header decryption. This work was
inspired by the NGINX ssl_preread
module but I wanted something that could be
a little bit more flexible in routing traffic via the TLS headers.
When ECH is used, this project operates as the Client-Facing server in the Split Mode toplogy. The inner client hello will be decrypted and forwarded to the matching backend server.
This project is written in Go, and can be built simply by calling go build
in
the top level directory. This should produce the tls-router
binary.
TODO: Describe Me!
To setup ECH support, we must first generate an HPKE private key. This can be
accomplished by using the -g
argument and providing a HPKE KEM algorithm to
use. For example
[email protected]:~$ ./tls-router -g x25519-hkdf-sha256 > ech-private.pem
[email protected]:~$ cat ech-private.pem
-----BEGIN HPKE PRIVATE KEY-----
Qs4r1UBZp+oSh1cEQ1UgZ5Gk6pJFBmrZSIdntskcnbc=
-----END HPKE PRIVATE KEY-----
We must then provide an ech
configuration in the YAML document. The path to
the private key can be used as the private_key_file
parameter for an ECH
configuration.
Next, we must configure our DNS records to include the ech
parameter in an
HTTPS
record. We can generate the ech
record usig the -s
argument as
follows:
[email protected]:~$ ./tls-router -c example.yaml -s
ech=AEb+DQBCAAAgACDrQ8oK9hyIktFIfT6WRrJSoNHloohHqZr+PnvNK5hUcwAMAAEAAQACAAIAAQADAAtleGFtcGxlLmNvbQAA
The configuration file is provided in YAML format and consists of three sections:
listen
describes the client-facing port that will listen for incoming connections.routes
describes the backend servers that will handle requests, and the rules for matching the connections that should route to thme.ech
describes the configuration to support a TLS1.3 encrypted client hello.
An example configuration file might look as follows:
listen:
- :8443
routes:
main-page:
sni: www.example.com
targets: [ 192.168.1.123:8443 ]
static-content:
sni: static.example.com
targets:
- address: 192.168.1.111:443
weight: 100
- address: 192.168.1.222:443
weight: 10
acme-responder:
alpn: acme-tls/1
targets:
- 192.168.1.42:10443
ech:
- config_id: 0
kem_id: x25519-hkdf-sha256
cipher_suites:
- hkdf-sha256,aes128gcm
- hkdf-sha384,aes256gcm
- hkdf-sha256,chacha20poly1035
public_name: example.com
private_key_file: ech-private.pem
listen
should contain an array of YAML strings. The expected syntax is
[address]:port
, where address
is an optional IPv4 or IPv6 address on the
local machine, and port
is the TCP port number to listen for connections on.
routes
should contain a YAML dictionary. The key of the dictionary serves as
a label for the route, and the value contains a dictionary of TLS connection
parameters to match. Each parameter can be either a string, regular expression
or an array of strings or regular expressions.
The supported parameters include:
sni
to match the Server Name extension.alpn
to match the Application Protocol Negotiation extension.ciphers
to match the client's supported cipher suites.
Each route must also contain a targets
array. The targets
should list the
backend servers to which the matching connections will be routed. Each target
can either be a string, or a YAML dictionary containing containing the following
values:
address
: Destination address of the backend server.weight
: Weighting for load balancing (default 100, if not provided).
ech
should contain an array of YAML dictionaries. Each dict describes one
ECHConfig
structure, and contains the following values:
config_id
: An 8-bit unsigned integer identifying the configuration.kem_id
: The HPKE Key Encapsulation Mechanism to use for this config.cipher_suites
: A list of HPKE KDF and AEAD ciphers supported by this config.public_name
: The server name to expect in the ClientHelloOuter.maximum_name_length
: The maximum expected server name in the ClientHelloInner.private_key
: The HPKE private key, either in base64 or in PEM encoding.private_key_file
: A path to the HPKE private key.
Only one of private_key
or private_key_file
should be provided.