make demo # or: demo/demo.sh make demo-down See demo/README.md.
The container image is prologic/parley,
and docker-compose.example.yml is a starting
point. An instance for example.com, reachable at chat.example.com, needs:
A place to run it, with /data on persistent storage (it holds the
instance key, the peer cache, the channel logs and the accounts):
docker run -d --name parley -v parley-data:/data \ -p 6667:6667 -p 8443:8443 \ -e PARLEY_ADMIN_TOKEN="s3cret" \ prologic/parley -domain example.com -endpoint https://chat.example.com Then create accounts with parleyctl (it is in the image too) or the
admin page:
docker exec parley parleyctl -token s3cret accounts create alice -password hunter2 docker exec parley parleyctl -token s3cret tokens create alice -label irssi Without an admin token and with no accounts yet, parleyd logs a one-time
/setup URL that creates the first admin in the browser. Single sign-on
through OpenID Connect or a reverse proxy’s identity headers is described
in docs/AUTH.md; SSO users mint IRC tokens for their
clients on their settings page.
HTTPS in front of port 8443 on chat.example.com. Any reverse proxy
that terminates TLS will do; parleyd itself serves plain HTTP unless you
give it -tls-cert and -tls-key.
An SRV record so other instances can find you:
_parley._tcp.example.com. IN SRV 0 0 443 chat.example.com. Without it, peers fall back to https://example.com/.well-known/parley/.
IRC over TLS for your clients. parleyd’s IRC listener is plaintext, so terminate TLS in front of it. With Caddy’s layer4 module, for example:
layer4 { 0.0.0.0:6697 { @parley tls sni chat.example.com route @parley { tls proxy 10.1.2.3:6667 } } } Then, in irssi: /connect -tls -tls_verify chat.example.com 6697 <password-or-token> alice.
The port here is whatever your proxy listens on. If it is not 6697, start
parleyd with -irc-port (and -irc-host, if IRC is on a different name
to the endpoint), or set PARLEY_IRC_PORT / PARLEY_IRC_HOST:
parleyd -domain example.com -irc-port 6687 The landing page, the settings page, parleyctl and the instance document
all print a connect line from it, and the settings page prints a live token
on that line. A wrong port under a right hostname still passes certificate
verification, so the token would go to whatever else is listening there.
Check it from the outside. parleyctl check probes an instance the
way a peer does — SRV record, well-known documents, the advertised
endpoint, the inbox, and the IRC TLS port — and says what to fix:
$ parleyctl check alice@example.com ok dns _parley._tcp.example.com -> chat.example.com:443 ok well-known https://chat.example.com/.well-known/parley/instance.json (parleyd/v0.2.0) ok domain example.com ok key ed25519 uPMn03bl2/qHh9X0B3Fn98yiWo7VS6Z9UHXJ7P8bO2E= ok endpoint https://chat.example.com is live ok inbox https://chat.example.com/inbox rejects unsigned events ok user alice@example.com exists (account) ok irc-tls chat.example.com:6697: chat.example.com, issued by E7, 89 days left It needs no token and works against anyone’s instance, so it is also how
you tell a peer what is wrong with theirs. The common failure is a
missing SRV record: the instance is perfectly reachable at its own host,
but nobody resolving the identity domain can find it. Add -resolve https://chat.example.com to probe the host directly while DNS is still
wrong, and -json for a machine-readable report. It exits non-zero if
any check fails.
irssi ──IRC──▶ parleyd (foo.com) ◀──signed HTTPS──▶ parleyd (bar.com) ◀──IRC── irssi │ /.well-known/parley/*.json │ │ /inbox /channels/<name>/feed │ └──── DNS: _parley._tcp.bar.com SRV ──────────┘
- Bob’s client sends PRIVMSG alice@foo.com :hi.
- bar.com looks up _parley._tcp.foo.com, fetches the instance document from the host it names, and caches the key.
- bar.com signs the event and POSTs it to foo.com’s inbox.
- foo.com discovers bar.com the same way to verify the signature, delivers the message to Alice’s clients, and since bar.com is a stranger, sends a hello back. Both sides now exchange channel rosters and peer lists.
The wire format is documented in docs/PROTOCOL.md.
There is no config file. Configuration is in two places and each thing is in exactly one of them.
Flags, each with an environment variable, for what the process needs
before it can open its database, what describes the machine and network it
sits on, and the secrets and trust decisions about who may assert an
identity. A flag wins over its variable. Only -domain is required.
Settings, in the database, for everything an administrator might
change while the instance runs. They take effect the moment they are
saved, with no restart, and have no flag and no environment variable.
Change them on the admin page, through PUT /api/v1/settings
(docs/API.md), or with parleyctl:
parleyctl settings list # every setting, * where it differs from its default parleyctl settings set history_replay 50 max_conns_per_addr 8 parleyctl settings reset motd # back to the default parleyctl settings export > settings.json parleyctl settings import settings.json # also reads an old config.json Only what differs from the defaults is stored, so an upgrade that changes a
default changes it for every instance that never touched that key.
People set their own picture under Settings in the web interface, or it
comes from their identity provider’s picture claim at login and is re-hosted
here. It is other instances in the well-known user document; see
docs/PROTOCOL.md.
The instance’s logo is not in this table, because it is not text: upload one PNG of at least 512 pixels along its longest side under Settings -> Logo in the admin page and Parley derives the favicon, the home-screen icon and the square an IRC client shows beside the network. A square is ideal, and a wordmark is fine – anything up to three times as long as it is tall is centred on a transparent square rather than refused. Until then every instance shows Parley’s own icon, which is why two of them look alike in a client’s network list.
Chat help. /help (or /quote HELP <topic>) serves the pages in
help/*.txt, which are compiled into the binary. Edit a page and rebuild to
change it; the first line is its title. A new file is a new topic – list it
in help/index.txt, which make test checks.
Endpoints: /healthz for liveness, /api/v1/status for a JSON status of
peers and channels, /metrics for Prometheus, and the API in
docs/API.md.
Upgrading from a config file. parleyd -config config.json no longer
reads the file: it prints, for every key in it, the flag, variable or
setting that key has become, and exits. Move the process-level keys to
your unit file or compose file, start the instance, then
parleyctl settings import config.json applies the rest in one go and
names what it skipped.
With TLS terminated in front of the IRC listener, every client arrives from
the proxy: the logs name the wrong host, and per-address limits either do
nothing or lock everybody out at once. List the proxy in irc_proxies and
it must then prepend a PROXY protocol header (v1 or v2) to each connection.
This is two changes, and neither works alone. Listing a proxy that does not send a header drops every connection from it; sending a header from an address that is not listed feeds it to the IRC parser as garbage. There is no safe order, so change both together and be ready to put both back.
The default is empty, which is the whole thing switched off. 127.0.0.1/32
below is an example, not a default – substitute the address your own
connections actually arrive from:
parleyd -irc-proxy 127.0.0.1/32 # repeatable PARLEY_IRC_PROXIES=127.0.0.1/32 # comma- or space-separated Note the name: PARLEY_TRUSTED_PROXIES is the web listener’s equivalent
and a different decision entirely.
To find the address to list, connect once and read the log. Every client address is reported the first time it is seen:
level=INFO msg="irc: new client address" addr=203.0.113.9 trusted_proxy=false Behind a proxy every client shares one address, so that is a single line
naming exactly what belongs in irc_proxies. It is always the address on
the socket, never the one a header carries – the header address could
never match the list, so reporting it would hand you a value guaranteed to
fail. Once the proxy is listed and sending headers, the line reappears for
it with trusted_proxy=true, which is the confirmation that the two halves
now agree.
Only listed addresses are believed, and a connection from one of them that
does not carry a header is dropped rather than treated as a direct
connection – accepting both shapes from the same place hands back the
address forgery the header exists to stop. Those drops are counted in
parley_irc_proxy_rejected_total, which is the metric to watch while
making the change.
The web listener has the same blindness and its own setting. Behind a
reverse proxy every request arrives from that proxy, so the per-address
gates on web login, the federation inbox and feed reads all share one
bucket – a global rate limit wearing a per-address name. List the proxy in
http_proxies and X-Forwarded-For is believed from it, and nothing else
changes.
Use that rather than auth.trusted_headers.proxies, which looks like it
would do the job and does far more: a proxy on that list may assert who
the user is, so a request to /login carrying Remote-User is logged in
without a password. Fixing a rate limit is no reason to turn on
passwordless login. The identity list does imply the address one, since a
proxy trusted that far is not one to doubt about an address.
The step-by-step version, including the order that costs one outage instead of two, is in docs/PROXY.md.
With real addresses in hand, max_conns_per_addr becomes safe to turn on,
and turning it on also applies the per-address login bucket to IRC. Leave it
at 0 while the listener sits behind an unlisted proxy: every client shares
one address there, so the cap would not limit anybody in particular, it
would lock out everybody at once. It is a setting, so turning it on is
parleyctl settings set max_conns_per_addr 8 and needs no restart. The
per-account login backoff applies either way – see
docs/AUTH.md.
/metrics serves the Prometheus text format: connections, accounts online,
channels, peers linked, and counters for pushes, inbox events, feed reads,
tag messages and what was refused. It needs an admin token by default, since
how many people are on an instance is the operator’s business; set the
metrics_public setting to serve it openly. Every sample is a count – no nicks, no
channel names, no peer domains.
<data_dir>/identity.key is the one file that cannot be regenerated. The
domain’s identity is the keypair: lose it and every peer that has cached
the old public key refuses the instance, and the only fix is a new key that
everyone has to re-trust. Back it up on the host, off the host:
parleyctl key show # public key and fingerprint parleyctl key export -y > instance.key # the secret; keep it somewhere safe parleyctl key import < instance.key # restore into an empty data dir If the key ever leaks, replacing it is a supported operation rather than a
disaster: