Golang reverse proxy with support of ESNI 01-draft (https://tools.ietf.org/html/draft-ietf-tls-esni-01) on top of TLS 1.3
Motovation: As of April 2020 ESNI, is still a draft extension for TLS 1.3 and not officialy supported by OpenSSL and major projects like nginx, apache, etc (however unofficial forks exists). This project porvides a tiny golang reverse proxy that can terminate TLS 1.3 wint ESNI and forward plain HTTP to upstream. This covers the gap, and if you want to experiment with ESNI draft you can use it right now with your vanilla nginx/apache etc. Also your browser should support ESNI, and for now it is only firefox (https://www.elliotjreed.com/post/security/2019-07-08_Enable_DNS_over_HTTPS_and_Encrypted_SNI_in_Firefox).
This project is extension of tris-localserver example code : https://github.com/cloudflare/tls-tris/tree/pwu/esni-consolidated/_dev/tris-localserver and was inspired by discussion: https://serverfault.com/questions/976377/how-can-i-set-up-encrypted-sni-on-my-own-servers
How to build: As declared here https://github.com/cloudflare/tls-tris/tree/pwu/esni-consolidated, since crypto/tls is very deeply (and not that elegantly) coupled with the Go stdlib, it is impossible to vendor it as crypto/tls because stdlib packages would import the standard one and mismatch. Approach here would be to build custom GOROOT (that has patched standard libraries), and then on top of it, build the current code.
Build esni reverse proxy:
git clone https://github.com/devopsext/esni-rev-proxy.git
cd esni-rev-proxy && git checkout v1.0.3
prepareGoRoot.sh
- this script create patched GOROOT folder (.GOROOT/
) in current directory (applicable to Linux/MacOS)export GOROOT=$(pwd)/.GOROOT
go mod vendor
go build
As alternative: you can use precompiled binaries from release page: https://github.com/devopsext/esni-rev-proxy/releases
Build esnitool (in case use need to generate esni keys): Esnitool source code is copied from cloudflare repo: https://github.com/cloudflare/tls-tris/tree/pwu/esni-consolidated/_dev/esnitool
- Copy
GOROOT
folder, that was prepared by script (see steps above) into esnitool/:cp -r GOROOT/* esnitool/
cd esnitool/ && export GOROOT=$(pwd)/GOROOT
go build
How to run:
- Generate esni keys pair (public and private) with
esnitool
: Just follow inline help of esnitool:
Usage of ./esnitool:
-esni-keys-file string
Write base64-encoded ESNI keys to file instead of stdout
-esni-private-file string
Write ESNI private key to file instead of stdout
-validity duration
Validity period of the keys (default 24h0m0s)
As example:
./esnitool -esni-keys-file ./esni.pub -esni-private-file ./esni -validity 32h
Pay attention to validity period. Specify as long as you need. In case you set it short,
you need to rotate key pair.
-
Setup specific DNS record for your server (in addition to A or AAAA record), that you are going to access with ESNI: Create TXT record for host name
_esni.<YOUR-DOMAIN>
with the contents ofesni.pub
file (esni public key) -
Run
esni-reverse-proxy
in front of your web server, to accept and terminate TLS 1.3 with ESNI, and forward decrypted traffic to your web-server.esni-rev-proxy
has self explanatory flags:
Usage of esni-rev-proxy:
-b string
Address:port used for binding (default "0.0.0.0:443")
-cert value
Triplet of SNI:PrivateKey.File:CertChain.File
-cliauth
Performs client authentication (RequireAndVerifyClientCert used)
-cpuprof string
CPU profile output file
-esni-keys string
File with base64-encoded ESNIKeys
-esni-private string
Private key file for ESNI
-memprof string
Memory profile output file
-pq string
Enable quantum-resistant algorithms [c: Support classical and Quantum-Resistant, q: Enable Quantum-Resistant only]
-rtt0 string
0-RTT, accepts following values [n: None, a: Accept, o: Offer, oa: Offer and Accept] (default "n")
-showaccesslog
Show access log
-stats-metrics-bind string
Address:port used for binding. Metrics available at /metrics (prometheus format), health-check at /helathz (default "0.0.0.0:8181")
-upstream string
Upstream URL to forward traffic to
-version
Print version
Short comments:
-cert
flag used to set SNI name and corresponding cert/key pair, in case your proxy severe several domains, you can specify several values.-upstream
is an upstream to forward plain traffic to.
Example:
esni-rev-proxy -b 0.0.0.0:443 \
-esni-keys esni.pub -esni-private esni \
-cert "www.example.com:/mycerts/www.example.com.key:/mycerts/www.example.com.crt" \
-cert "other-domain.com:/mycerts/other-domain.com.key:/mycerts/other-domain.com.crt" \
-upstream http://internal-endpoint \
-showaccesslog
This would start up the reverse proxy that:
- Accept incoming connections on all interfaces on port 443
- Decrypt ESNI (using
esni.pub
andesni
public and private key pair) and choose appropriate certificate for host namewww.example.com
orother-domain.com
, stored in/mycerts
. The key/cert pair specified in first-cert
flag is also treated as default cert/key pair - so it will be used if no match to SNI host detected. - Forward decrypted traffic to
http://internal-endpoint
- Print incoming and forwarded requests in stdout (
-showaccesslog
flag) - Export 2 endpoints
0.0.0.0:8181/metrics
- metrics in prometheus format and0.0.0.0:8181/healthz
- simple http health-check.
Sample list of metrics in prometheus format:
# HELP esnirevproxy_http_average_last_min_rps Incoming HTTP rps average for the last minute
# TYPE esnirevproxy_http_average_last_min_rps gauge
esnirevproxy_http_average_last_min_rps 0
# HELP esnirevproxy_http_upstream_latency_msec Upstream latency in milliseconds
# TYPE esnirevproxy_http_upstream_latency_msec histogram
# Bucket distribution deleted...
esnirevproxy_http_upstream_latency_msec_sum{upstream="www.google.com"} 14243
esnirevproxy_http_upstream_latency_msec_count{upstream="www.google.com"} 20
# HELP esnirevproxy_http_upstream_response_codes Upstream response HTTP codes
# TYPE esnirevproxy_http_upstream_response_codes counter
esnirevproxy_http_upstream_response_codes{code="200",upstream="www.google.com"} 7
esnirevproxy_http_upstream_response_codes{code="204",upstream="www.google.com"} 13
# HELP esnirevproxy_tcp_connections_total Total active/idle connections
# TYPE esnirevproxy_tcp_connections_total gauge
esnirevproxy_tcp_connections_total 1
# HELP esnirevproxy_tls_failed_handshakes Total number of failed TLS handshakes
# TYPE esnirevproxy_tls_failed_handshakes counter
esnirevproxy_tls_failed_handshakes 1
# HELP esnirevproxy_tls_handshake_duration_msec Handshake time in milliseconds
# TYPE esnirevproxy_tls_handshake_duration_msec histogram
# Bucket distribution deleted...
esnirevproxy_tls_handshake_duration_msec_sum 3
esnirevproxy_tls_handshake_duration_msec_count 1
# HELP esnirevproxy_tls_successful_handshakes Total number of successful TLS handshakes
# TYPE esnirevproxy_tls_successful_handshakes counter
esnirevproxy_tls_successful_handshakes{host="localhost"} 1
#Standard go_ metrics is not printed here.