Andrew Mercer
on this page

UnrealIRCd and Anope

UnrealIRCd is the most widely deployed IRC server. The current series is 6.2.x; only the latest stable release receives security fixes, so plan to upgrade regularly (./unrealircd upgrade makes this painless). Anope provides NickServ, ChanServ, and friends. Its stable series is 2.0.x; 2.1.x is the development series (renamed modules and modernized config, see notes below).

See also: IRC guide · Command reference · irssi

Historical note: earlier versions of this page covered UnrealIRCd 3.2 and Anope 1.8 on CentOS 5 and FreeBSD 8. Both are long end-of-life and their config formats are incompatible with current releases. The lessons that still apply are kept in Legacy Lessons.


Ports and Firewall

Port Purpose Expose publicly?
6697 Client connections over TLS Yes
6667 Plaintext clients Preferably no (or localhost/LAN only)
6900 Server-to-server links (TLS) Only if you link a second IRCd
7000 (127.0.0.1) Anope services link Never — bind to localhost
sudo firewall-cmd --permanent --zone=public --add-port=6697/tcp
sudo firewall-cmd --reload

On the homelab, forward 6697 on OPNsense to the IRC host and leave everything else closed.


Install UnrealIRCd 6 from Source

1. Dependencies

# Fedora / RHEL / Rocky (EPEL for some libs)
sudo dnf install gcc gcc-c++ make pkgconf-pkg-config openssl-devel \
  pcre2-devel libargon2-devel libsodium-devel c-ares-devel libcurl-devel jansson-devel

# Debian / Ubuntu
sudo apt install build-essential pkg-config gdb libssl-dev libpcre2-dev \
  libargon2-dev libsodium-dev libc-ares-dev libcurl4-openssl-dev

Missing libraries are fine — UnrealIRCd bundles fallbacks — but system libraries get security updates from your distro.

2. Dedicated user (never run an IRCd as root)

sudo useradd -m -s /bin/bash unrealircd
sudo -iu unrealircd

3. Download, verify, build

curl -fsSLO https://www.unrealircd.org/downloads/unrealircd-latest.tar.gz
tar xzf unrealircd-latest.tar.gz
cd unrealircd-6.*/
./Config
make
make install

./Config is interactive; the defaults are sensible (install to ~/unrealircd, TLS on, generate a self-signed cert). It saves your answers to config.settings.

Automating ./Config (an open question in my old notes): after the first run, ./Config -quick reuses the saved answers non-interactively, which is what ./unrealircd upgrade does too. That makes it usable from Ansible: run it once by hand, keep config.settings, then ./Config -quick && make && make install in the playbook.

4. Create the config

cd ~/unrealircd
cp conf/examples/example.conf conf/unrealircd.conf

Essential Configuration

Edit ~/unrealircd/conf/unrealircd.conf. The example file is heavily commented; these are the blocks you must change.

Server identity

me {
    name "irc.andrewmercer.net";
    info "Andrew Mercer IRC";
    sid "001";                      /* unique 3-char server ID: digit + 2 alnum */
}

admin {
    "Andrew Mercer";
    "andrew";
    "[email protected]";
}

Network settings and cloak keys

Generate cloak keys rather than inventing them:

cd ~/unrealircd && ./unrealircd gencloak
set {
    network-name     "umoswg";
    default-server   "irc.andrewmercer.net";
    services-server  "services.irc.andrewmercer.net";
    sasl-server      "services.irc.andrewmercer.net";
    help-channel     "#andrewmercer";
    cloak-prefix     "umoswg";
    kline-address    "[email protected]";
    modes-on-connect "+ixw";
    modes-on-join    "+nt";

    cloak-keys {
        "<key 1 from gencloak>";
        "<key 2 from gencloak>";
        "<key 3 from gencloak>";
    }
}

Keep cloak keys secret, and identical on every server in the network. Changing them changes every user's cloaked host (and invalidates bans written against cloaks).

Listen blocks

listen { ip *;          port 6697; options { tls; } }
listen { ip 127.0.0.1;  port 6667; }                          /* local plaintext only */
listen { ip 127.0.0.1;  port 7000; options { serversonly; } } /* Anope */
/* listen { ip *; port 6900; options { tls; serversonly; } }    only if linking IRCds */

Operators

UnrealIRCd 6 does not want plaintext oper passwords. Hash one:

cd ~/unrealircd && ./unrealircd mkpasswd
# Enter password → prints an $argon2id$... hash
oper andrew {
    class opers;
    mask { 192.168.2.0/24; 10.10.10.0/24; }   /* LAN + WireGuard only */
    password "$argon2id$v=19$m=6144,t=2,p=2$...";
    operclass netadmin-with-override;
    swhois "is a Network Administrator";
    vhost netadmin.andrewmercer.net;
}
  • operclass decides permissions. netadmin-with-override allows /samode and overriding channel restrictions (the old 3.2 can_override flag). Plain netadmin does not.
  • Restricting mask to trusted networks means a leaked password alone isn't enough.
  • Log in from a client with /oper andrew <password>.

Restrict which channels can be created

To make users able to join only specific channels (replaces the old 3.2 chrestrict):

deny channel {
    channel "*";
    reason "Only official channels are available on this server";
}

allow channel {
    channel "#andrewmercer";
}
allow channel {
    channel "#lab";
}

IRC operators with override can still join anything.

Check and apply

./unrealircd configtest     # validate without starting
./unrealircd start
./unrealircd rehash         # reload config after edits (or /rehash as oper)
./unrealircd status
tail -f logs/ircd.log

TLS Certificates

make install generates a self-signed certificate at ~/unrealircd/conf/tls/server.cert.pem and server.key.pem. Clients will reject it unless they disable verification or pin it. Use a real certificate instead.

Let's Encrypt

Issue a certificate for irc.andrewmercer.net with certbot (or the certbot-manager tooling), then install a deploy hook that copies it where UnrealIRCd expects it and reloads TLS without a restart:

# /etc/letsencrypt/renewal-hooks/deploy/unrealircd.sh
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="irc.andrewmercer.net"
DEST="/home/unrealircd/unrealircd/conf/tls"
install -o unrealircd -g unrealircd -m 0644 "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "$DEST/server.cert.pem"
install -o unrealircd -g unrealircd -m 0600 "/etc/letsencrypt/live/$DOMAIN/privkey.pem"   "$DEST/server.key.pem"
sudo -u unrealircd /home/unrealircd/unrealircd/unrealircd reloadtls
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/unrealircd.sh

Verify from outside:

openssl s_client -connect irc.andrewmercer.net:6697 -servername irc.andrewmercer.net </dev/null \
  | openssl x509 -noout -subject -issuer -dates

If you'd rather name the files differently, point the set { tls { certificate "..."; key "..."; } } block at them instead of renaming. The old 3.2 behaviour of only looking for server.cert.pem/server.key.pem is gone.


Run with systemd

Replaces the old SysV /etc/init.d/unreal + chkconfig script.

# /etc/systemd/system/unrealircd.service
[Unit]
Description=UnrealIRCd
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=unrealircd
Group=unrealircd
WorkingDirectory=/home/unrealircd/unrealircd
ExecStart=/home/unrealircd/unrealircd/bin/unrealircd -F
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now unrealircd
sudo systemctl reload unrealircd     # rehash
journalctl -u unrealircd -f

-F keeps UnrealIRCd in the foreground so systemd supervises it directly.


Upgrading

sudo -iu unrealircd
cd ~/unrealircd
./unrealircd upgrade        # downloads, verifies signature, rebuilds with saved settings
./unrealircd restart        # or: sudo systemctl restart unrealircd

Read the release notes first; 6.x minor releases occasionally change config defaults.


Anope Services

Install (2.0 stable)

sudo useradd -m -s /bin/bash anope
sudo -iu anope
curl -fsSLO https://github.com/anope/anope/archive/refs/tags/2.0.20.tar.gz
tar xzf 2.0.20.tar.gz && cd anope-2.0.20
./Config            # install dir defaults to ~/anope; CMake-based
cd build
make && make install

Check https://www.anope.org/ for the current stable version number. Anope also publishes an official Docker image (anope/anope).

Configure Anope

cd ~/anope/conf
cp example.conf services.conf

Key blocks in services.conf:

uplink
{
    host = "127.0.0.1"
    ipv6 = no
    ssl = no            /* fine on localhost; TLS is supported if linking remotely */
    port = 7000
    password = "<long random link password>"
}

serverinfo
{
    name = "services.irc.andrewmercer.net"
    description = "Services for umoswg"
    pid = "data/services.pid"
    motd = "conf/services.motd"
}

module
{
    name = "unreal4"    /* 2.0.x module; supports UnrealIRCd 4, 5 and 6 */
    use_server_side_mlock = yes
    use_server_side_topiclock = yes
}

networkinfo
{
    networkname = "umoswg"
    nicklen = 31
    userlen = 10
    hostlen = 64
    chanlen = 32
}

Make yourself services root (replaces 1.8's ServicesRoot directive):

oper
{
    name = "andrew"
    type = "Services Root"
    require_oper = yes
}

Anope 2.1 differences: the protocol module is renamed unrealircd (from unreal4), uplink:ipv6 becomes uplink:protocol, and insecure password hashing modules can no longer be the primary encryption method. Read the upgrading guide before switching.

Matching UnrealIRCd blocks

Add to unrealircd.conf:

link services.irc.andrewmercer.net {
    incoming {
        mask 127.0.0.1;
    }
    password "<same long random link password>";
    class servers;
}

ulines {
    services.irc.andrewmercer.net;
}

The services-server and sasl-server entries in the set { } block shown earlier are what make SASL login work for clients.

Then:

sudo -u unrealircd ~unrealircd/unrealircd/unrealircd rehash
sudo -iu anope
~/anope/bin/services --nofork --debug     # first run: watch it link
# Ctrl+C once it works, then:
~/anope/bin/anoperc start
tail -f ~/anope/data/logs/services.log.*

A successful link shows Anope introducing NickServ, ChanServ, etc., and /map in your client lists services.irc.andrewmercer.net.

Anope 2.1 ships an example systemd unit; for 2.0, a unit modelled on the UnrealIRCd one above with ExecStart=/home/anope/anope/bin/services --nofork works.


Troubleshooting

Error Cause / fix
Link denied (No matching link configuration) The serverinfo:name in services.conf doesn't exactly match the link <name> in unrealircd.conf, or the incoming IP doesn't match mask.
Link denied (Authentication failed) Link passwords differ.
Services link, but SASL fails Missing sasl-server / services-server in set { }, or services name not in ulines.
Clients get certificate errors Still using the self-signed cert — install Let's Encrypt and ./unrealircd reloadtls.
./unrealircd upgrade warns about a key mismatch Very old 6.1.x builds predate the current signing key; upgrade manually once from the tarball.
OpenSSL version mismatch warning See below — rebuild after a major OpenSSL upgrade.

Legacy Lessons

Kept from the 3.2/1.8 era because the underlying principles still hold:

  • Rebuild after OpenSSL major upgrades. UnrealIRCd 3.2 warned OpenSSL version mismatch: compiled for 0.9.8l ... library is 0.9.8e and could crash. Any IRCd compiled against OpenSSL must be rebuilt when the library's major version changes (on modern installs, ./unrealircd upgrade or ./Config -quick && make && make install). Having two OpenSSL installs (/usr and /usr/local) was the root cause back then.
  • A leftover $INSTALL environment variable broke make install (Error 126 in the bundled TRE library). Check env when a build fails in a strange place.
  • Anope's name must match the IRCd link block exactly. This was the cause of every "Link denied" I hit on CentOS and FreeBSD, and it's still the #1 linking problem.
  • The FreeBSD port expected a dedicated user/group (ircdru in the old port). Ports and packages often assume a run-as user — read the port's Makefile/pkg-message.
  • "Anope will not work over SSL" was true for 1.8; Anope 2.x can link over TLS, though on localhost plaintext is fine.