docs: rework README + manual — refresh the feature set (services interface, rustls TLS backend, WebSocket, PROXY v1/v2, metrics + JSON-RPC endpoints), correct command/module/cap counts, and document the tls_backend/metrics_bind/rpc/sts config keys in configuration.md and the example

This commit is contained in:
Jean Chevronnet 2026-08-19 14:37:22 +00:00
parent b106b66de5
commit ebd6e29589
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
9 changed files with 121 additions and 98 deletions

107
README.md
View file

@ -2,58 +2,55 @@
# echoIRCd
**A from-scratch, memory-safe IRCv3 server written in Rust.**
**A memory-safe IRCv3 server written in Rust.**
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](Cargo.toml)
[![Language: Rust](https://img.shields.io/badge/rust-stable-orange.svg)](https://www.rust-lang.org)
[![unsafe: forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](src/lib.rs)
[![IRCv3](https://img.shields.io/badge/IRCv3-supported-blueviolet.svg)](https://ircv3.net)
[![dependencies: 2](https://img.shields.io/badge/dependencies-openssl%20%2B%20mio-lightgrey.svg)](Cargo.toml)
</div>
## About
echoIRCd is a full IRC + IRCv3 server built from the ground up in safe Rust
(`#![forbid(unsafe_code)]`) with just two dependencies — `openssl` for TLS and
`mio` for the socket engine. A single lock-free core thread owns all state; a
**pool of epoll reactor threads** (one per core) drives the connections around it
— TLS crypto and all — without an async runtime. It ships **100+ commands**, the
**complete channel & user mode set**,
**28 IRCv3 capabilities**, server-to-server linking, a services interface, TLS,
WebSocket, GeoIP, layered anti-spam, and a JSON-RPC control plane — with every
operational limit configurable and nothing hardcoded.
echoIRCd is a full IRC + IRCv3 server. A single lock-free **core thread** owns all
state; a **pool of epoll reactor threads** (one per core) drives the connections
around it — TLS crypto and all — with no async runtime. It ships **100+ commands**,
the **complete channel & user mode set**, **30+ IRCv3 capabilities**,
server-to-server linking, a services interface, TLS, WebSocket, GeoIP, layered
anti-spam, a Prometheus metrics endpoint, and a JSON-RPC control plane — with every
operational limit exposed as a config key.
## Features
- **Full IRC core** — registration, channels (`JOIN`/`PART`/`KICK`/`INVITE`/
`KNOCK`/`CYCLE`/`REMOVE`/`TOPIC`), messaging (`PRIVMSG`/`NOTICE`/`TAGMSG`, CTCP),
and info (`WHO`/`WHOIS`/`WHOWAS`/`LIST`/`STATS`/`MAP`/`LUSERS`/`MOTD`).
- **Complete mode set** — prefixes `qaohv` (+ a network-staff `!` prefix), list
- **Complete mode set** — prefixes `qaohv` (plus a network-staff `!` prefix), list
modes `beIgXw`, keyed/limit/flood/redirect/history/anticaps params, the full flag
set, all the standard user modes, and matching + acting **extbans**
(`g y r j s G b`, `m c n`).
- **IRCv3** — 28 capabilities including `message-tags`+`msgid`, `server-time`,
`labeled-response`, `batch`, `echo-message`, `account-tag`, **CHATHISTORY**,
**multiline**, **message-redaction**, **read-marker**, **relaymsg**, and
`WATCH`/`MONITOR`/`SILENCE`/callerid.
set, all the standard user modes, and matching + acting **extbans**.
- **IRCv3** — message-tags (+msgid), server-time, labeled-response, batch,
echo-message, account-tag, **CHATHISTORY**, **multiline**, **message-redaction**,
**read-marker**, **relaymsg**, SASL, standard-replies, and `WATCH`/`MONITOR`/
`SILENCE`/caller-id.
- **Operators**`OPER`/`KILL`/`WALLOPS`/`GLOBOPS`, the `SA*`/`CHG*`/`SET*`
override toolbox, x-lines (`K`/`G`/`Z`/`E`/`SHUN`/`QLINE`/`CBAN`) persisted to
disk, staff prefix (`operprefix`/`OJOIN`), rank-gated `hidelist`/`hidemode`, and
a reload-safe `REHASH`.
- **Services & accounts** — SASL PLAIN/EXTERNAL relayed over S2S, the `SVS*` /
override toolbox, x-lines (`K`/`G`/`Z`/`E`/`SHUN`/`QLINE`/`CBAN`/`RLINE`)
persisted to disk, staff prefix (`operprefix`/`OJOIN`), oper levels, rank-gated
`hidelist`/`hidemode`, and a reload-safe `REHASH`.
- **Services & accounts** — SASL PLAIN/EXTERNAL relayed over the link, the `SVS*` /
`ENCAP` / `METADATA` interface, account-gated modes, and optional ircd-side
account registration.
account registration (`REGISTER`/`VERIFY`).
- **Server-to-server linking**`UID`/`FJOIN` netburst, cross-server users and
channels, multi-hop routing, nick-collision handling and clean netsplit.
- **Security & anti-spam** — TLS with cert fingerprints, keyed host cloaking,
DNSBL, connection/message flood limits, mixed-script & gibberish detection,
CAPTCHA / PONG-cookie / arithmetic gates, and DCC filtering.
- **GeoIP** — a native MaxMind `.mmdb` reader with a `G:<cc>` geoban, `GEOIP`
command, and WHOIS country line.
- **Transports & control** — a native WebSocket layer (`ws://` / `wss://`), a
from-scratch forward-confirmed DNS resolver, and a token-authenticated JSON-RPC
control plane over HTTP.
channels, multi-hop routing, TS-based nick-collision handling, and clean
netsplit/rejoin.
- **Security & anti-spam** — TLS with client-cert fingerprints, keyed host
cloaking, DNSBL, per-IP connection/message flood limits, mixed-script & random
(drone) detection, CAPTCHA / PONG-cookie / arithmetic gates, and DCC filtering.
- **Transports** — plaintext, TLS (OpenSSL or rustls backend), a native WebSocket
layer (`ws://` / `wss://`), and the PROXY protocol (v1/v2) behind a load balancer.
- **GeoIP** — a MaxMind `.mmdb` reader with a `G:<cc>` geoban, `GEOIP` command, and
a WHOIS country line.
- **Control & observability** — a token-authenticated JSON-RPC plane over HTTP, and
an optional OpenMetrics/Prometheus endpoint.
## Quick start
@ -74,14 +71,17 @@ The full manual lives in [`docs/`](docs/):
- [Building & running](docs/building.md) · [Configuration](docs/configuration.md) · [Architecture](docs/architecture.md)
- [Channel & user modes](docs/modes.md) · [Operators](docs/operators.md) · [Server linking & services](docs/linking.md)
- [IRCv3](docs/ircv3.md) · [Anti-abuse & flood protection](docs/anti-abuse.md) · [Deployment](docs/deployment.md)
- [Module developer API](docs/api/) — write your own commands, modes, and modules.
## Configuration
Configuration is a plain `key = value` file; see
[`echoircd.conf.example`](echoircd.conf.example) for the full, documented set of
keys. Your live `echoircd.conf` is gitignored — it holds secrets (oper password,
cloak key, link password), so never commit it. Generate a TLS certificate into
`tls/` with the one-liner in the example config.
[`echoircd.conf.example`](echoircd.conf.example) for the full, annotated set of
keys. Every operational limit is a config key with a built-in default, and most
settings apply on `REHASH` without a restart. Your live `echoircd.conf` is
gitignored — it holds secrets (oper password, cloak key, link password), so never
commit it. Generate a TLS certificate into `tls/` with the one-liner in the example
config.
## Architecture
@ -92,13 +92,13 @@ anywhere. The I/O edge feeds it events over channels:
- **A pool of `mio` epoll reactors** drives client sockets — an acceptor
round-robins each connection onto a worker (one per core by default), and each
worker frames lines and runs **TLS handshakes and record crypto non-blocking**
in-thread. So the socket work and the crypto spread across cores while the state
core stays single-threaded and lock-free. (Proxied TLS and server links keep a
thread each; there are few of them.)
- **Resilience is built in.** Slow work (KDF hashing, DNS, disk snapshots) runs
off the core so a flood can't freeze it; each event and each connection's I/O is
panic-isolated so one bad client can't crash the server; a watchdog flags a
stuck core; and half-open/stalled connections are reaped on a timer.
in-thread. Socket work and crypto spread across cores while the state core stays
single-threaded and lock-free. (Proxied TLS and server links keep a thread each;
there are few of them.)
- **Resilience is built in.** Slow work (KDF hashing, DNS, disk snapshots) runs off
the core so a flood can't freeze it; each event and each connection's I/O is
panic-isolated so one bad client can't crash the server; a watchdog flags a stuck
core; and half-open/stalled connections are reaped on a timer.
**Why a raw reactor and not async?** IRC is one large shared mutable graph, and
almost every command mutates it and then broadcasts. With one thread owning all of
@ -106,9 +106,9 @@ it, handlers are plain synchronous code — no locks, no `.await`, no `Send + 's
bounds. A multi-threaded async runtime would force that shared state behind mutexes
or an actor mailbox, and a channel broadcast is serialized anyway, so you'd pay for
parallelism the workload can't use. `mio` is the same readiness layer async runtimes
build on, so you keep the scaling without the runtime. What *does* parallelize —
the socket syscalls and TLS crypto — runs in the reactor pool; scaling past one
machine is done by linking servers, not threading one harder.
build on, so you keep the scaling without the runtime. What *does* parallelize — the
socket syscalls and TLS crypto — runs in the reactor pool; scaling past one machine
is done by linking servers, not threading one harder.
Memory safety is structural: `Uid` handles instead of raw pointers, an `Extensible`
typemap instead of `void*` module data (freed on drop), and compiled-in trait
@ -117,13 +117,13 @@ objects instead of a fragile plugin ABI. **Full design notes:**
## Extending
Three small extension points, each one file + one table line — full reference and
a tutorial in [`docs/api/`](docs/api/):
Three small extension points, each one file + one table line — full reference and a
tutorial in [`docs/api/`](docs/api/):
- **Commands** (`src/command.rs`, `src/coremods/`) — a handler with `name`,
`min_params`, `before_reg`, `handle(&mut Server, uid, params)`.
- **Modes** (`src/mode.rs`) — channel/user modes as `ChanMode` / `UserMode`
handler objects; adding one never touches the parser.
- **Modes** (`src/mode.rs`) — channel/user modes as `ChanMode` / `UserMode` handler
objects; adding one never touches the parser.
- **Modules** (`src/module.rs`, `src/modules/`) — lifecycle hooks; pre-hooks can
**Deny** a register/command/message, notify-hooks fire after.
@ -136,7 +136,4 @@ a tutorial in [`docs/api/`](docs/api/):
## License
echoIRCd is released under the [MIT License](Cargo.toml). It is original Rust —
no code is copied or translated from any other project, enforced on every edit by
`scripts/native-rust-guard.sh` (no `unsafe`, no C/FFI, dependencies limited to
`openssl` + `mio`).
echoIRCd is released under the [MIT License](Cargo.toml).

View file

@ -1,15 +1,14 @@
# echoIRCd documentation
echoIRCd is a from-scratch IRC + IRCv3 server written in safe Rust
(`#![forbid(unsafe_code)]`) with two dependencies — `openssl` for TLS and `mio`
for the socket engine. A single lock-free core thread owns all state; a pool of
reactor threads drives the connections around it.
echoIRCd is a memory-safe IRC + IRCv3 server written in Rust. A single lock-free
core thread owns all state; a pool of epoll reactor threads drives the connections
around it — TLS crypto and all — with no async runtime.
This folder is the reference manual. Start with whichever fits what you're doing:
| Doc | What's in it |
|-----|--------------|
| [Building & running](building.md) | Prerequisites, debug/release builds, tests, the originality guard, project layout. |
| [Building & running](building.md) | Prerequisites, debug/release builds, tests, project layout. |
| [Configuration](configuration.md) | The config file format and a grouped reference of every setting. |
| [Architecture](architecture.md) | The single-threaded core + reactor-pool design, the I/O models, resilience, and memory safety. Read this to understand *why* it's built the way it is. |
| [Channel & user modes](modes.md) | Every prefix, list, parameter, and flag mode, plus extbans. |
@ -23,7 +22,7 @@ This folder is the reference manual. Start with whichever fits what you're doing
## At a glance
- **Full IRC core** — registration, channels, messaging, and the informational
command set (~130 commands total).
command set (100+ commands total).
- **Complete mode set** — the standard prefixes plus a staff prefix, list modes,
keyed/limit/flood/redirect/history/anticaps parameters, the full flag set, and
matching + acting extbans. See [modes](modes.md).
@ -33,10 +32,13 @@ This folder is the reference manual. Start with whichever fits what you're doing
- **Operators & services** — a rich oper toolbox with oper levels, X-lines
persisted to disk, and a services interface (SASL over links, the `SVS*` /
`ENCAP` / `METADATA` set, account-gated modes).
- **Transports** — plaintext, TLS (with client-cert fingerprints), a native
WebSocket layer, and the PROXY protocol behind a load balancer.
- **Transports** — plaintext, TLS (OpenSSL or rustls backend, with client-cert
fingerprints), a native WebSocket layer, and the PROXY protocol behind a load
balancer.
- **Security** — keyed host cloaking, DNSBL, GeoIP, layered connection/message
flood limits, and script/gibberish spam detection.
- **Control & observability** — a token-authenticated JSON-RPC plane over HTTP and
an optional OpenMetrics/Prometheus endpoint. See [configuration](configuration.md).
## Design in one paragraph

View file

@ -2,7 +2,7 @@
This is the reference for extending echoIRCd. The server has five extension
points, all ordinary Rust trait objects compiled into the binary — there is no
plugin ABI, no dynamic loading, and no `unsafe`:
plugin ABI and no dynamic loading:
| You want to… | Implement | Registered in | Reference |
|--------------|-----------|---------------|-----------|
@ -41,9 +41,9 @@ Modules in this codebase follow a few hard rules — match them:
literal default.
4. **Register in the table**, don't touch the parser or the dispatcher. Adding a
command / mode / module is one new file plus one line in a registration table.
5. **Original Rust only.** No `unsafe`, no C/FFI, no new dependencies, and no code
copied or translated from another project. `scripts/native-rust-guard.sh`
enforces this on every edit.
5. **Stay self-contained.** A module shouldn't pull in a heavy new dependency —
the primitives you'll reach for (an HTTP client, a regex engine, base64, the
hashing/KDF helpers) already live in the tree; reuse them.
## Write your first module in five steps
@ -92,11 +92,10 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
**4. (Only if it adds a command)** expose a `commands()` function from your module
and chain it into `module_commands()` — see [commands](commands.md).
**5. Build, test, and check:**
**5. Build and test:**
```sh
cargo build && cargo test
bash scripts/native-rust-guard.sh src/modules/hello.rs
```
That's it — `hello` is now a first-class part of the server.

View file

@ -91,7 +91,7 @@ cares which one a connection uses:
| Transport | Model | Notes |
|-----------|-------|-------|
| **Plaintext clients** | reactor pool | The common case; one worker frames many sockets. |
| **Direct TLS clients** | reactor pool | The handshake and record crypto run **non-blocking inside the worker** (OpenSSL driven off a `mio` socket). TLS work spreads across cores like everything else. |
| **Direct TLS clients** | reactor pool | The handshake and record crypto run **non-blocking inside the worker**, driven off a `mio` socket by the configured TLS backend (OpenSSL by default, or rustls via `tls_backend = rustls`). TLS work spreads across cores like everything else. |
| **Proxied TLS** (a PROXY header before the handshake) | thread per connection | Reading the pre-handshake header wants the simpler blocking path; there are few of these. |
| **Server links** | thread per connection | A handful of long-lived peers; not worth multiplexing. |
@ -131,11 +131,12 @@ kernel-level filtering.
## Memory safety
Safety is structural, not just `#![forbid(unsafe_code)]`:
Safety is structural, not just a matter of avoiding raw pointers:
- **Handles, not pointers.** Users and channels are referenced by `Uid` /
channel-key handles looked up in maps, so there are no dangling references and
no use-after-free.
no use-after-free — a `Uid` is monotonic and never reused, so a stale handle
resolves to nothing rather than to the wrong user.
- **A typemap, not `void*`.** Modules attach per-user / per-channel / per-server
state through an `Extensible` typemap keyed by Rust type; it's dropped
automatically with its owner, so module state can't leak or be freed twice.
@ -143,11 +144,6 @@ Safety is structural, not just `#![forbid(unsafe_code)]`:
compiled-in trait objects. There is no dynamic-loading FFI boundary to get
wrong.
The two dependencies (`openssl`, `mio`) keep their own `unsafe` internal, so the
daemon itself never writes any. The `scripts/native-rust-guard.sh` check enforces
all of this on every edit: no `unsafe`, no C/FFI, dependencies limited to those
two, and no code copied or translated from another project.
## Tuning knobs
| Setting | Effect |

View file

@ -3,10 +3,10 @@
## Prerequisites
- A stable **Rust** toolchain (`cargo`, `rustc`).
- **OpenSSL** development headers (the `openssl` crate links against the system
library) — e.g. `libssl-dev` on Debian/Ubuntu.
That's it. There are exactly two dependencies: `openssl` and `mio`.
- **OpenSSL** development headers for the default TLS backend (the `openssl` crate
links against the system library) — e.g. `libssl-dev` on Debian/Ubuntu. The
optional pure-Rust rustls backend (`tls_backend = rustls`) needs no system
library.
## Build
@ -52,20 +52,8 @@ cargo test --test integration # just the end-to-end suite
The integration suite spawns the real binary on ephemeral ports and drives it as
a client — covering reactor-pool cross-worker delivery, TLS-in-reactor handshakes,
the stalled-handshake reap, the accept-rate limiter, and nick collisions. It
tracks and kills its child processes by PID, never by name.
## The originality guard
Every source edit is checked by `scripts/native-rust-guard.sh`, which enforces the
project's invariants:
- no `unsafe` (the crate is `#![forbid(unsafe_code)]`),
- no C / FFI,
- dependencies limited to `openssl` + `mio`,
- and no code copied or translated from any other project — everything is
original Rust.
Run it on a file directly with `bash scripts/native-rust-guard.sh <file>`.
tracks and kills its child processes by PID, never by name. The parser and the S2S
convergence logic also carry property-based (`proptest`) suites.
## Project layout
@ -82,7 +70,7 @@ src/
command.rs the Command trait
module.rs the module lifecycle-hook trait
coremods/ built-in commands (registration, channels, messaging, oper, …)
modules/ optional, pluggable modules (~70 of them)
modules/ optional, pluggable modules (70+ of them)
tests/ end-to-end integration tests
deploy/ production systemd units + firewall script
docs/ this manual

View file

@ -29,6 +29,8 @@ Most settings apply on `REHASH` without a restart.
| `bind` | Plaintext client listener, `ip:port` (e.g. `0.0.0.0:6667`). |
| `bind_tls` | TLS client listener (e.g. `0.0.0.0:6697`). |
| `tls_cert` / `tls_key` | PEM certificate + private key for TLS (and `wss://`). |
| `tls_sni` | Serve a different cert for a given hostname (`<host> <cert> <key>`, repeatable). |
| `tls_backend` | `openssl` (default) or `rustls` (pure-Rust, no system OpenSSL). |
| `bind_server` | Server-to-server link listener (see [linking](linking.md)). |
Generate a self-signed cert to start:
@ -148,6 +150,14 @@ See [anti-abuse](anti-abuse.md) for how these layer together.
| `log_json` | Append the notice/log stream to a file as JSON lines. |
| `chanlog` | Mirror the oper server-notice stream into a channel. |
## Control & observability
| Key | Meaning |
|-----|---------|
| `metrics_bind` | Bind an OpenMetrics/Prometheus scrape endpoint (`ip:port`, plaintext HTTP GET). Off unless set — expose it privately or behind a proxy. |
| `rpc` + `rpc_bind` | Enable the JSON-RPC control plane and bind its HTTP listener. Both required to turn it on. |
| `rpc_user` / `rpc_token` | Credentials for the control plane — sent as HTTP Basic (`user:token`) or Bearer. Bind privately; the token is a shared secret. |
## Transports
| Key | Meaning |

View file

@ -96,7 +96,7 @@ A connection from a trusted proxy must lead with a PROXY (v1 or v2) header. This
applies to the plaintext and TLS client listeners; for browser clients over
WebSocket, use `ws_proxyranges` with `X-Forwarded-For` instead.
## Log shipping
## Log shipping & metrics
| Setting | Sends the notice/log stream to |
|---------|--------------------------------|
@ -107,6 +107,13 @@ WebSocket, use `ws_proxyranges` with `X-Forwarded-For` instead.
The core also surfaces its own health: `slow_command_ms` raises a notice when an
event runs long, and `watchdog_ms` logs if the core is stuck.
For a metrics pipeline, bind the OpenMetrics/Prometheus endpoint with
`metrics_bind = 127.0.0.1:9109` and scrape it (counters for commands / messages /
connects, gauges for users / channels / servers / links). For scripted control,
the JSON-RPC plane (`rpc` / `rpc_bind` / `rpc_token`) exposes admin operations over
HTTP. Bind both privately — on loopback or behind the reverse proxy, never on a
public interface.
## TLS certificates
Point `tls_cert` / `tls_key` at your PEM files (the same pair serves `bind_tls`

View file

@ -29,6 +29,9 @@ informed as they change. This page groups what's supported.
| `multi-prefix` | See all of a member's status prefixes at once. |
| `userhost-in-names` | Full `nick!user@host` in `NAMES`. |
| `invite-notify` | Channel ops see invites to their channel. |
| `draft/pre-away` | Send `AWAY` during registration so away state is set before the first `JOIN`. |
| `no-implicit-names` | Suppress the automatic `NAMES` reply on `JOIN` (the client asks when it wants it). |
| `draft/channel-rename` | `RENAME` a channel in place, keeping membership. |
## Authentication
@ -60,6 +63,7 @@ control who may message them.
| `draft/metadata-2` | `METADATA` key/value data on users and channels. |
| `draft/extended-isupport` | Re-request the current ISUPPORT tokens on demand. |
| `draft/json-log` | Stream the server log to an oper as JSON. |
| `sts` | Strict Transport Security — tell a client to upgrade to TLS and pin that for a duration (opt-in via `sts_duration` / `sts_port` / `sts_preload`). |
| `EXTJWT` | A short-lived, server-signed HS256 token a client can present elsewhere. |
| network icon / profile link | Advertise a network icon and per-account profile URLs. |

View file

@ -22,8 +22,16 @@ tls_key = ./tls/key.pem
# The cert is re-read on REHASH, so a renewed cert applies without a restart.
# SNI: serve a different cert for a given hostname. Repeatable.
# tls_sni = irc.example.net ./tls/example.crt ./tls/example.key
# TLS backend: openssl (default) or rustls (pure-Rust, no system OpenSSL needed).
# tls_backend = rustls
# STS (Strict Transport Security): tell CAP 302 clients on the plaintext port to
# upgrade to TLS and pin it. Off unless sts_duration > 0. Set sts_port to your TLS
# port. Only enable once TLS is solid — clients will refuse plaintext for the pin.
# sts_duration = 2592000 # seconds clients should stick to TLS (0 = off)
# sts_port = 6697 # the TLS port to upgrade to
# sts_preload = no # advertise preload eligibility
motd = Welcome to echoIRCd — a from-scratch IRC daemon in Rust.
motd = Welcome to echoIRCd.
motd = Edit the MOTD in your echoircd.conf.
# --- server-to-server linking ---
@ -192,6 +200,18 @@ amu_target = both
# --- log_json: append the server-notice / log stream to a file as JSONL ---
# log_json = /var/log/echoircd/events.jsonl
# --- metrics: OpenMetrics/Prometheus scrape endpoint (plaintext HTTP GET). Off
# unless bound; expose it only on a private/loopback address or behind a proxy. ---
# metrics_bind = 127.0.0.1:9109
# --- rpc: token-authenticated JSON-RPC control plane over HTTP (admin tooling).
# Off unless rpc = yes AND rpc_bind is set. Bind privately; the token is a
# shared secret sent as HTTP Basic (rpc_user:rpc_token) or Bearer. ---
# rpc = yes
# rpc_bind = 127.0.0.1:8080
# rpc_user = admin
# rpc_token = CHANGE_THIS_RPC_TOKEN
# --- PROXY protocol: trust the HAProxy/nginx PROXY header (v1 or v2) from these
# sources (glob or CIDR, repeatable), so the real client IP is used instead of
# the proxy's. A connection from a trusted proxy MUST lead with a PROXY header.