Перейти к содержимому

Behind a TCP front end

Это содержимое пока не доступно на вашем языке.

A front end is not needed. The relay terminates TLS 1.3 itself and gets its own certificates (see the quick start). Use this layout only if another service must share port 443 on the host.

The protocol (v2 and later) binds each client’s key proof, and the relay’s identity signature, to the TLS session between the client and the relay. So:

  • Supported: a TCP (layer-4) passthrough on the same host that forwards the raw bytes to the relay and prefixes each connection with a PROXY protocol v2 header. Two front ends are supported: HAProxy with send-proxy-v2, and Caddy with the caddy-l4 plugin with proxy_protocol v2.
  • Not supported: anything that terminates TLS in front of the relay: Caddy reverse_proxy, nginx proxy_pass on an ssl listener, HAProxy mode http, a CDN in proxy mode, or a TLS-terminating load balancer. The two ends see different TLS sessions and every connection fails; the app shows “This server couldn’t prove its identity”.
  • Not supported: nginx’s stream module. It sends only PROXY protocol version 1, which the relay refuses.

The old layout, where a proxy terminated TLS and the relay ran with --no-tls on loopback, no longer works.

rchat/config/server-behind-proxy.toml in the rchat-core repository is the config template for this layout, and the published unit rchat-server-proxy.service runs it. The essentials:

# /etc/rchat/server.toml, behind a TCP passthrough on public :443
[server]
bind_addr = "127.0.0.1:8443"
max_connections = 10000
# Every connection arrives from the front end's address:
# no per-address caps here (enforce them in the front end).
max_connections_per_ip = 0
max_connections_per_ipv6_48 = 0
# The front end sends PROXY protocol v2 with the client's address.
proxy_protocol = true
websocket_path = "/CHANGE-ME-to-a-long-random-path"
relay_identity_path = "/var/lib/rchat/relay_identity.key"
# The relay terminates TLS 1.3 itself. Send SIGHUP
# (systemctl reload rchat-server) after renewing the certificate.
[server.tls]
cert_path = "/etc/rchat/tls/fullchain.pem"
key_path = "/etc/rchat/tls/privkey.pem"
[metrics]
bind_addr = "127.0.0.1:9090"
[decoy]
enabled = false

Built-in ACME ([server.acme] instead of [server.tls]) also works when the front end forwards every connection for the domain on public :443, because Let’s Encrypt’s TLS-ALPN-01 check then reaches the relay.

Create the relay identity once, then install the unit:

Terminal window
sudo -u rchat /usr/local/bin/rchat-server --config /etc/rchat/server.toml --init-relay-identity
curl -fsSL -o rchat-server.service https://assets.r-chat.net/1.1.0/rchat-server-proxy.service
sudo install -m 0644 rchat-server.service /etc/systemd/system/rchat-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now rchat-server

The unit starts rchat-server --config /etc/rchat/server.toml --bind 127.0.0.1:8443, with TLS from the config file. A loopback port above 1024 needs no capability.

With proxy_protocol = true the relay reads the PROXY v2 header from the raw TCP stream before the TLS handshake, within 5 seconds, and uses its source address for the per-source fair shares only: pending requests, circuits, stored message requests, the daily request quota and shedding (see Resource limits). It is never used for connection caps, authentication or logging.

  • The parser is strict: version 2 only, commands LOCAL and PROXY, TCP over IPv4 or IPv6, at most 16 + 216 bytes. A connection without a valid header is closed (reason="proxy_header").
  • A connection whose TCP peer is not a loopback address is refused before anything is read, with a warning (reason="proxy_not_loopback").
  • The relay refuses to start with proxy_protocol = true unless max_connections_per_ip = 0, max_connections_per_ipv6_48 = 0 and bind_addr is a loopback address (127.0.0.1 or ::1; a wildcard bind is refused).
  • Both sides must agree. If the front end does not send PROXY v2, every connection is closed; if the relay has proxy_protocol = false, the header breaks every TLS handshake.

Trust boundary. The relay believes a PROXY header from any process that can connect to its loopback port, so any local user or process can name any source address. At worst that skews the fair shares. Run the relay on a host you don’t share with untrusted users if fairness matters to you.

Behind a passthrough without PROXY v2, every connection comes from the front end’s address. Set both per-address caps to 0; the per-source fair shares are then off, and one client can crowd out everyone else’s requests.

The relay does not read X-Forwarded-For. The former trusted_proxy setting was removed: a config that still sets it refuses to start and points to proxy_protocol.

Including a per-client connection cap, which takes the place of the relay’s max_connections_per_ip:

frontend rchat
mode tcp
bind :::443 v4v6
# At most 64 concurrent connections per client address.
stick-table type ipv6 size 1m expire 10m store conn_cur
tcp-request connection track-sc0 src
tcp-request connection reject if { sc0_conn_cur gt 64 }
default_backend relay
backend relay
mode tcp
server r1 127.0.0.1:8443 send-proxy-v2

Caddy needs the caddy-l4 plugin (xcaddy build --with github.com/mholt/caddy-l4). rchat/config/Caddyfile in the rchat-core repository:

{
layer4 {
:443 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:8443
}
}
}
}
}

The file has no site blocks, so Caddy’s HTTP app does not claim :443. As far as we know caddy-l4 cannot cap concurrent connections per client address (its throttle handler limits bandwidth only), so in this layout one client can open up to max_connections. If you need a per-client cap, use HAProxy or let the relay own :443.

Terminal window
# The certificate presented must be the relay's own, not the front end's.
openssl s_client -connect chat.example.com:443 -servername chat.example.com </dev/null | head -20
curl -s http://127.0.0.1:9090/metrics | grep -E 'reason="proxy_(header|not_loopback)"'

Then connect once with the app. A growing proxy_header count usually means the front end is not sending PROXY v2.