Configuration
echo reads one TOML file, passed as the first argument (echo config.toml). Copy config.example.toml to start. Keep config.toml out of version control; it holds the link password.
Two sections are required: [uplink] and [server]. Everything else is optional and off when omitted. A few settings reload on OperServ REHASH without a restart (noted below); the rest need a restart.
The smallest working config:
[uplink]
host = "127.0.0.1"
port = 7000
password = "changeme"
[server]
name = "services.example.net"
sid = "42S"
description = "Network Services"
Contents
- Required:
[uplink],[server] - Services and operators:
[modules],[[oper]],[log] - Policy:
[register],[extban],[auth] - Lifecycle and limits:
[expire],[session] - User-facing:
[language],[email] - Integrations:
[health],[grpc],[jsonrpc],[keycard],[dictserv] - Optional multi-node:
[gossip],[[peer]] - Full example
[uplink]
Where echo connects, and how it authenticates the link. Add a matching <link> block on the ircd for the same SID and password.
| Key | Default | Description |
|---|---|---|
host |
required | Uplink address. |
port |
required | Uplink server-to-server port. |
password |
required | Link password; must match the ircd's <link> block. |
tls |
false |
Connect over TLS instead of plaintext. |
spki_fingerprint |
"" |
Required when tls = true. The server's SPKIFP, base64 SHA256 of its SubjectPublicKeyInfo. The link cert is pinned by this fingerprint rather than a CA, so a self-signed ircd cert is fine. |
[uplink]
host = "irc.example.net"
port = 7000
password = "s3cret"
# tls = true
# spki_fingerprint = "Wh/ag/dtBCpXhiYPhRWV66kx3FutmTyNw1dysTbWvao="
Get the fingerprint of a running ircd with:
openssl s_client -connect irc.example.net:7000 </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary | base64
[server]
The services server's own identity, and how its pseudo-clients present themselves.
| Key | Default | Description |
|---|---|---|
name |
required | Services server name (e.g. services.example.net). |
sid |
required | Three-character server ID, unique on the network. |
description |
required | Server description shown in /links and /map. |
protocol |
1206 |
InspIRCd link protocol version (1206 = insp4, 1205 = insp3). |
scram_iterations |
high | PBKDF2 cost baked into new SCRAM verifiers at registration. Lower it only if registration latency matters more than verifier strength. |
guest_nick |
"Guest" |
Prefix a user is renamed to on LOGOUT (the ircd appends a number). Must start with a letter. |
service_host |
server name | Hostname the pseudo-clients wear, so NickServ shows as NickServ!services@<here>. |
service_modes |
"iHkB" |
User modes the pseudo-clients are introduced with: invisible, hideoper, servprotect (unkillable, needs the services server U-lined), bot. Add T to have the ircd block all CTCP. |
service_oper_type |
"Network Service" |
Oper type shown in /whois for the pseudo-clients. Empty leaves them non-opers. |
services_channel |
"#services" |
Channel every pseudo-client joins at startup. Empty keeps them out of any channel. |
standard_replies |
false |
Emit IRCv3 FAIL/WARN/NOTE standard replies instead of plain notices. Needs m_services_stdrpl on the ircd. |
[modules]
Which services to start. Omit the section for the full suite; list names to run a subset. Add "example" to also start the template service.
| Key | Default | Description |
|---|---|---|
services |
full suite | Lowercase service names to load. |
[modules]
services = ["nickserv", "chanserv", "memoserv", "operserv"]
The full suite is nickserv, chanserv, botserv, hostserv, memoserv, operserv, statserv, groupserv, infoserv, reportserv, helpserv, chanfix, diceserv, gameserv, debugserv.
[[oper]]
Services operators. Repeat the block once per operator. Grant a tier with type, or list fine-grained privileges with privs. The three tiers stack in power:
- operator is day-to-day moderation: view hidden info, network bans, kick, kill, session limits, ignores, mode, spam filters, log search.
- administrator is that plus account and channel data: suspend, drop, set flags, forbid, global notices, defcon, memo/info/bot administration.
- root is that plus daemon control: add or remove opers,
SET,REHASH,RESTART,SHUTDOWN, the activity feed.
| Key | Default | Description |
|---|---|---|
account |
required | The account name that holds these privileges. |
type |
none | Tier shortcut: operator, administrator, or root. |
privs |
[] |
Fine-grained privileges: auspex, oper, suspend, admin, root. |
[[oper]]
account = "alice"
type = "root"
[[oper]]
account = "bob"
privs = ["oper", "suspend"]
Runtime operators can also be added without editing the config, with OperServ OPER ADD (optionally time-limited).
[log]
The staff audit feed. Notable service actions (registrations, drops, vhosts, suspensions, akicks, access and bot changes) are announced to a channel so operators can see who did what. Memo bodies, credential material, and cosmetic self-service changes are never shown.
| Key | Default | Description |
|---|---|---|
channel |
required | The log channel. Keep it operator-only; NOTIFY lines can carry real IPs. |
notify_exclude |
[] |
Masks OperServ NOTIFY never announces. #channel mutes a channel, server:<glob> (or via:) mutes a server, anything else mutes a user (nick glob, user@host, or extban). Reloadable with REHASH. |
[log]
channel = "#services"
notify_exclude = ["*/*", "server:relay.example.net", "#staff"]
[register]
Registration policy. Both settings reload with REHASH.
| Key | Default | Description |
|---|---|---|
confusable_check |
true |
Refuse look-alike or mixed-script registration names. Turn off for a community that legitimately uses non-Latin names. |
vouch |
false |
Invite-only registration: a new account stays pending until an existing member runs NickServ VOUCH, instead of confirming by email. |
[extban]
Which extbans echo recognizes. Omit the section (or leave the list empty) to accept every extban the ircd advertises.
| Key | Default | Description |
|---|---|---|
enabled |
all | Extban names to accept (account, realname, country, ...). |
[auth]
Who owns accounts. By default echo owns them and REGISTER / IDENTIFY / SET PASSWORD all work on their own. Set external = true to hand identity to an outside authority (a website): IRC can then only IDENTIFY, everything that changes an account is refused, and the authority pushes accounts in over the gRPC Accounts API. echo still owns all channel, vhost, and ban data, keyed by account name.
| Key | Default | Description |
|---|---|---|
external |
false |
Delegate account identity to an external authority. |
[expire]
Drop inactive accounts and channels on a periodic pass. Operators, live sessions, occupied channels, and NOEXPIRE-pinned records are spared. A zero or omitted field leaves that kind never expiring. Omit the section to disable expiry.
| Key | Default | Description |
|---|---|---|
accounts_days |
0 (off) |
Expire accounts not identified to for this many days. |
channels_days |
0 (off) |
Expire channels not joined for this many days. |
warn_days |
0 (off) |
Email the owner this many days before expiry (needs [email] and an address on file). |
[session]
Per-IP connection limiting. The connection that puts an IP over the limit is killed on connect. OperServ EXCEPTION raises or lowers it per IP-mask (a limit of 0 means unlimited).
| Key | Default | Description |
|---|---|---|
default_limit |
0 (off) |
Connections allowed per IP. |
[language]
Reply language. echo ships English plus French, German, Spanish (with an Argentine variant), and Portuguese (Portugal and Brazil).
| Key | Default | Description |
|---|---|---|
default |
"en" |
Language for users who haven't chosen one. |
dir |
"lang" |
Directory of <code>.json catalogs. |
available |
[] |
Codes a user may pick with NickServ SET LANGUAGE. Empty allows only the default. |
[language]
default = "en"
available = ["en", "fr", "de", "es", "es-ar", "pt", "pt-br"]
[email]
Outbound email for registration confirmation and password recovery. Without it, those features are off.
| Key | Default | Description |
|---|---|---|
from |
required | Sender address. |
command |
required | Shell command the message is piped to, e.g. sendmail -t or msmtp -t. Run via sh -c. |
brand |
"Network Services" |
Display name in the email template. |
accent |
"#4f46e5" |
Accent color (any CSS color). |
logo |
"" |
Hosted logo image URL (email clients don't render inline SVG or data URIs). |
confirm_url |
"" |
Base URL of a one-click confirmation endpoint. When set, confirmation emails include a <url>?account=<name>&code=<code> link alongside the CONFIRM command. |
[email]
from = "services@example.net"
command = "msmtp -t"
brand = "Example Network"
[health]
A read-only, unauthenticated HTTP endpoint for monitoring. GET /health is a JSON liveness probe; GET /metrics is a Prometheus scrape (account, channel, and oper totals, per-service counters, and a monotonic log-progress gauge). Bind it to localhost.
| Key | Default | Description |
|---|---|---|
bind |
required | HTTP listen address, e.g. 127.0.0.1:9099. |
[grpc]
A gRPC directory of accounts and channels for a website to mirror (identity and metadata only, never credentials), and, with the token, to register or confirm accounts. Schema in proto/echo.proto. Off unless configured.
| Key | Default | Description |
|---|---|---|
bind |
required | Listen address, e.g. 127.0.0.1:50051. |
token |
required | Bearer token every RPC must send (authorization: Bearer <token>). |
[grpc.tls] cert / key |
none | Optional server TLS (PEM chain and key). Omit on a private or loopback hop. |
[grpc]
bind = "127.0.0.1:50051"
token = "a-long-shared-secret"
[jsonrpc]
An HTTP JSON-RPC endpoint (for a staff web panel). Keep it on localhost behind a reverse proxy, or terminate TLS here.
| Key | Default | Description |
|---|---|---|
bind |
required | HTTP listen address, e.g. 127.0.0.1:5601. |
token |
required | Bearer token every request must send. |
origins |
[] |
Browser origins allowed cross-site (CORS). Empty means no browser access. |
[jsonrpc.tls] cert / key |
none | Terminate TLS here (enables HTTP/2). Omit if a proxy does TLS. |
[keycard]
Passwordless web login: a member already signed in on the website connects with a one-time kc_... token in place of a password. echo redeems it against the website's localhost login endpoint.
| Key | Default | Description |
|---|---|---|
url |
required | The website's login-token endpoint. |
api_key |
required | Matches the endpoint's X-API-Key. |
[dictserv]
DictServ, dictionary and reference lookups over the DICT protocol (RFC 2229). Opt-in, because it makes outbound network requests. Lookups are globally rate-limited and truncated to one line.
| Key | Default | Description |
|---|---|---|
server |
"dict.org:2628" |
DICT endpoint. |
[dictserv]
server = "dict.org:2628"
Running more than one node
Optional. A single node is the usual setup. If you run several, they replicate their event logs and share one account and channel database. [gossip] turns it on, [[peer]] names the others. See the wiki's federation notes for the trust model and certificate setup.
| Section | Key | Description |
|---|---|---|
[gossip] |
bind |
Where peers reach this node. Omit for a dial-only node. |
[gossip] |
secret |
Shared secret every node presents. |
[gossip.tls] |
cert / key / ca |
Mutual TLS; a peer must present a cert signed by ca. |
[[peer]] |
addr / name |
One block per other node; name must match the peer's certificate. |
Full example
Every section, with the optional ones commented out. Uncomment and edit what you need.
# ---- required ----
[uplink]
host = "127.0.0.1"
port = 7000
password = "changeme" # must match the ircd's <link> block
# tls = false
# spki_fingerprint = "" # base64 SHA256 of the server's SPKI; required when tls = true
[server]
name = "services.example.net"
sid = "42S" # three chars, unique on the network
description = "Network Services"
# protocol = 1206 # 1206 = insp4, 1205 = insp3
# scram_iterations = 210000
# guest_nick = "Guest"
# service_host = "" # empty = the server name
# service_modes = "iHkB"
# service_oper_type = "Network Service"
# services_channel = "#services"
# standard_replies = false # needs m_services_stdrpl on the ircd
# ---- services and operators ----
# [modules]
# services = ["nickserv", "chanserv"] # omit for the full suite
# [[oper]]
# account = "yournick"
# type = "root" # operator | administrator | root
# privs = [] # or fine-grained: auspex, oper, suspend, admin, root
# [log]
# channel = "#services"
# notify_exclude = ["*/*", "server:relay.example.net", "#staff"]
# ---- policy ----
# [register]
# confusable_check = true
# vouch = false
# [extban]
# enabled = [] # empty = accept every extban the ircd offers
# [auth]
# external = false # true = an outside authority owns accounts
# ---- lifecycle and limits ----
# [expire]
# accounts_days = 90
# channels_days = 30
# warn_days = 7
# [session]
# default_limit = 3
# ---- user-facing ----
# [language]
# default = "en"
# available = ["en", "fr", "de", "es", "es-ar", "pt", "pt-br"]
# [email]
# from = "services@example.net"
# command = "msmtp -t"
# brand = "Example Network"
# accent = "#4f46e5"
# logo = ""
# confirm_url = "" # e.g. https://example.net/confirm
# ---- integrations ----
# [health]
# bind = "127.0.0.1:9099"
# [grpc]
# bind = "127.0.0.1:50051"
# token = "a-long-shared-secret"
# [grpc.tls]
# cert = "certs/node.crt"
# key = "certs/node.key"
# [jsonrpc]
# bind = "127.0.0.1:5601"
# token = "a-long-shared-secret"
# origins = ["https://example.net"]
# [keycard]
# url = "http://127.0.0.1:8000/accounts/api/login-token"
# api_key = "shared-key"
# [dictserv]
# server = "dict.org:2628"
# ---- optional multi-node ----
# [gossip]
# bind = "0.0.0.0:16700"
# secret = "shared-secret"
# [gossip.tls]
# cert = "certs/node.crt"
# key = "certs/node.key"
# ca = "certs/ca.crt"
# [[peer]]
# addr = "other-node:16700"
# name = "other-node"