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

Configuration guide

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

The relay reads one TOML file, /etc/rchat/server.toml by default (change it with --config). This page covers the settings most operators touch. The configuration reference lists every key with its type and default.

  • A missing, unreadable or invalid file is a startup error. The relay never quietly falls back to defaults.
  • Server settings can sit under [server] or at the top level. Sub-tables work either way too: [server.acme] and [acme] mean the same.
  • Unknown keys are a startup error in every table, so a typo stops the server with unknown field .... Keys left over from earlier templates must be removed before upgrading; Troubleshooting names the common ones.
  • Removed keys are refused with an explanation: message_queue_persist_path (see Offline queue) and trusted_proxy (see Behind a TCP front end).
  • Resource limits are checked at startup. A value of 0 or out of range stops the server, and there is no “unlimited” setting.
[server]
bind_addr = "0.0.0.0:443"
websocket_path = "/Zk3q-replace-with-32-random-url-safe-chars"
relay_identity_path = "/var/lib/rchat/relay_identity.key"
[server.acme]
enabled = true
domains = ["chat.example.com"]
contacts = ["mailto:[email protected]"]
cache_dir = "/var/lib/rchat/acme"
staging = false
[server.offline_queue]
ttl_secs = 259200 # 72 hours
persist = false # memory only
[server.decoy]
enabled = true
# root = "/var/www/decoy"

rchat/config/server.toml in the rchat-core repository is a commented template with every limit and both certificate options. rchat/config/server-behind-proxy.toml is the template for a relay behind a TCP front end.

Configure exactly one certificate source: [server.acme] (built-in Let’s Encrypt) or a static certificate (cert_path and key_path under [server.tls]). TLS always terminates at the relay. See TLS and certificates.

TLS 1.3 resumption tickets are on by default ([server.tls] session_tickets = true), for both sources; with ACME, a [server.tls] table may hold only that key. See TLS resumption.

relay_identity_path sets where the relay’s long-term identity key lives (default: relay_identity.key next to the config file). Create it once with --init-relay-identity. See Relay identity.

Only a WebSocket upgrade to exactly websocket_path reaches the relay. Any query string is ignored when matching. Every other request gets the decoy website, including plain HTTP requests, upgrades to other paths and garbage bytes. Request paths are never logged.

Set a long random path:

Terminal window
echo "/$(openssl rand -base64 24 | tr '+/' '-_')"

Then put the same path in every client’s server URL: wss://chat.example.com/<path>. The path must start with / and be plain ASCII with no ?, #, spaces or control characters.

The decoy ([server.decoy]):

KeyDefaultMeaning
enabledtrueServe a decoy website to non-relay requests. With false, those requests get a plain 404. The path check still applies.
rootnoneAbsolute path to a static site to serve. It must exist at startup. Symlinks that point outside it are not followed. With no root, a built-in site is served that varies with host.
hostfirst [server.acme] domainHost name that seeds the built-in site, together with the relay’s secret identity key. Never shown on the page.
server_header"nginx/1.24.0"Value of the Server response header.
write_timeout_secs30Time limit for writing one decoy response.

The decoy answers one request per connection. It is a mitigation, not a disguise: it keeps a casual scan from seeing what runs on the port, but a determined prober can tell it apart from real nginx by its behaviour (no keep-alive, TLS and timing details).

Range requests follow nginx 1.24: one satisfiable range gets a 206; several ranges get a 206 with a multipart/byteranges body; a malformed or unsatisfiable set such as bytes=abc or bytes=5-3 gets a 416; ranges adding up to more than the file get the whole file. A 206 carries no Accept-Ranges header.

rchat-server --config <path> --render-decoy <dir> [--host <hostname>] writes a small static website into <dir> and exits. The site is a login page for an invented product (webmail, a NAS admin page, a photo library, a cloud drive or code hosting). The product, its brand name, accent colour and logo are chosen from the relay’s secret identity key and its host name, so two relays never serve identical pages and nobody can work out a relay’s page from its host name and pin. The same identity and host always give the same site, the one the relay serves itself. Re-provisioning a relay with the same identity key reproduces its site.

The command reads the relay identity key, so it needs --config and must run as the service user, like --print-invite. Without the key it refuses.

Terminal window
sudo -u rchat rchat-server --config /etc/rchat/server.toml --render-decoy /var/lib/rchat/decoy --host chat.example.com

Then point the relay at it:

[server.decoy]
root = "/var/lib/rchat/decoy"
  • Host name. The host is lower-cased and any trailing dot is dropped before it is used, so Chat.Example.com. and chat.example.com give the same site. Without --host, the command uses [server.decoy] host, or else the first [server.acme] domain from the config file, or else no host at all (the identity key alone seeds the site).
  • Directory. <dir> must be empty or missing, or a directory this command rendered before. Anything else is refused and left untouched. Symlinks are not followed out of <dir>, and each file is written through a temporary file and a rename.
  • Self-contained. All styles, scripts and logos are in the site’s own files, with no outside fetches. The login form never submits; it shows an “incorrect username or password” message.
  • Missing pages. Unknown paths get the site’s own 404.html page with status 404.
  • Nothing identifying. The site never contains the WebSocket path, the relay pin, a version number or the project name.
  • Without a root. The relay serves a built-in site of the same kind, seeded by HKDF-SHA256(secret identity key, salt = host name, info = "rchat/decoy-seed/v3"), with the host from [server.decoy] host, else the first [server.acme] domain, else none. The page is unique to the relay and cannot be worked out from its host name or public pin. Running --rotate-relay-identity changes it, because the key changes. A site already rendered to disk stays as it is until you render it again.
  • Upgrading to 2.0.0. Earlier builds seeded the page from the host name alone, so every relay with a host name gets a new decoy design once, on upgrade. Relays without a host keep theirs. Re-run --render-decoy if you serve a rendered site and want it to match.

The decoy makes the relay look like an ordinary website to casual probes. It is not a guarantee against a determined prober.

Messages and message requests for a recipient who is not connected are held by the relay until they are delivered or expire. They are filed under the recipient only; the sender’s identity is inside the encrypted payload.

[server.offline_queue]
ttl_secs = 259200 # 72 h; 3600 to 259200 (72 h is the maximum)
knock_budget_bytes = 16777216 # 16 MiB for message requests, separate from messages
persist = false # memory only (default)
# persist_path = "/var/lib/rchat/queue.rcq"
# persist_key_path = "/run/rchat/queue.key"
# allow_persistent_key = false
  • By default nothing is written to disk. Queued items live in memory and are lost on restart.

  • ttl_secs is how long a queued message lives, at most 72 hours: a larger value stops the relay at startup, so the app’s promise that waiting copies expire within 72 hours holds on every relay. Message requests live at most 72 hours, whatever the setting.

  • Steady-size entries. Messages of exactly the steady traffic size (1,226 bytes sealed) may fill at most half of one recipient’s queue (500 of 1,000 messages); beyond that they are refused like a full queue, not dropped.

  • Encrypted persistence is opt-in. With persist = true the queue is written encrypted to persist_path every 5 minutes and on shutdown, and read back (then deleted) at start. The relay refuses to start unless the key file at persist_key_path is exactly 32 bytes, mode 0600 or narrower, owned by the service user, in a different directory and on a different filesystem from the data, and on a memory filesystem (Linux tmpfs or ramfs; FreeBSD tmpfs or mfs). Queued data then survives a service restart but not a reboot.

  • The shipped systemd units keep /run/rchat (a tmpfs, preserved across restarts but not reboots) and create /run/rchat/queue.key once per boot if it is missing, so the template paths work as they are.

  • allow_persistent_key = true skips only the memory-filesystem check and logs a warning. A relay that sets it can no longer say that its queue key is lost on reboot.

  • What the sender’s “Sent” means. The relay acknowledges a message only once it has handed it to the recipient’s connection or holds it in the recipient’s queue. A message that does not fit the caps is refused with no acknowledgement; queued messages are never dropped to make room. A message for someone who is connected but has not yet picked up the conversation again (just after an app restart, say) is handed straight to their connection instead of waiting for their next connection.

Size caps for the queue are under Limits and memory budgets below and on the resource limits page.

Behind a TCP front end (PROXY protocol v2)

Section titled “Behind a TCP front end (PROXY protocol v2)”

By default the relay owns port 443 and sees each client’s address itself. If another service must share port 443, put a TCP (layer-4) passthrough on the same host in front of it and turn on proxy_protocol:

[server]
bind_addr = "127.0.0.1:8443"
max_connections_per_ip = 0
max_connections_per_ipv6_48 = 0
proxy_protocol = true
  • Every connection must then start with a PROXY protocol v2 header (HAProxy send-proxy-v2, caddy-l4 proxy_protocol v2), read before TLS. Connections without a valid v2 header, or from a non-loopback peer, are refused. nginx sends only v1, which is refused.
  • The relay refuses to start with proxy_protocol = true unless bind_addr is a loopback address and both per-address caps (max_connections_per_ip, max_connections_per_ipv6_48) are 0.
  • The address in the header is used for the per-source fair shares only, never for authentication, logging or connection caps: any local process can send a header naming any source.
  • The front end must not terminate TLS. The app’s connection proof is bound to the TLS session, so TLS has to end at the relay.

rchat/config/server-behind-proxy.toml in rchat-core is the template. Behind a TCP front end has HAProxy and Caddy examples.

  • Idle timeout. connection_timeout_secs (default 300, allowed 60–86400) closes a connection that has sent nothing for that long. The app sends a keepalive every 22 to 38 seconds, so connected apps are never affected.
  • The relay’s pings. The relay sends its own keepalive ping every 25 to 35 seconds and closes a connection whose pong is more than 60 seconds late. The 60 seconds count from when the ping was actually written to the socket, not from when it was queued, so a ping waiting behind a large message that the client is still reading slowly does not cut the connection. A connection whose socket accepts nothing for 30 seconds is closed as stalled instead.
  • The app’s pings. The relay answers each keepalive ping from the app with a pong. These answers start no timer of their own.
  • Identification. A new connection must complete a signed key publish within identification_timeout_secs (default 15). A hello alone does not count.

None of the ping timings can be configured.

Every limit has a safe default, and a value of 0 or out of range stops the relay at startup (the per-address caps accept 0 to turn them off). The ones most operators look at:

KeyDefaultWhat it limits
max_connections10000Simultaneous connections
max_connections_per_ip64Connections from one IPv4 address or IPv6 /64 (0 = off)
rate_limit_bps1048576Inbound bytes per second per connection (throttled, not closed)
max_message_bytes4194304Largest message from a client
offline_queue_max_total_bytes268435456Offline queue, all recipients
offline_queue_max_bytes_per_recipient16777216Offline queue, one recipient (also at most 1000 messages)
outbound_budget_bytes1073741824Bytes queued for sending, all connections
buffer_budget_bytes536870912WebSocket buffers, all connections

When outbound_budget_bytes or buffer_budget_bytes is spent, the relay closes the heaviest connection of the source holding the most. Size both budgets and the offline queue to the host’s memory: the defaults suit hosts with 4 GB or more; on a 2 GB host use about 256 MiB and 128 MiB.

The full list, with allowed ranges, fair shares and metrics, is on the resource limits page.

--no-tls serves plaintext WebSocket and is for local development only. It requires [server] dev_origin, the origin clients dial, and refuses a non-loopback bind:

[server]
bind_addr = "127.0.0.1:18443"
dev_origin = "ws://127.0.0.1:18443"
Terminal window
rchat-server --config ./dev.toml --init-relay-identity
rchat-server --config ./dev.toml --no-tls

Without TLS there is no channel binding: client proofs are bound to a hash of dev_origin, which only stops replay to a relay with a different origin. Never expose a --no-tls relay.

  • --log-level (or RCHAT_LOG_LEVEL) sets verbosity. The default is info. Logs go to standard output; under systemd they land in the journal.
  • [logging] log_client_ips = true adds client IP addresses to debug lines only. The default is false.
  • [logging] format and output are accepted but not currently used.

What each level contains is described in What the relay logs and stores.

[metrics] serves Prometheus counters on 127.0.0.1:9090 by default. The endpoint is unauthenticated: a non-loopback bind_addr stops the relay at startup unless you also set allow_public = true, and then you must firewall it. See Metrics.

  • circuit_ttl_secs is accepted and ignored, with a startup warning. Circuits live while both endpoints are connected and end when either disconnects or closes them. There is no time-based expiry.
  • redis_url, [logging] format and [logging] output parse but have no effect.