Znc /

Chroot ZNC

In this guide, we will install ZNC inside a chroot.

ZNC is an IRC bouncer. It runs on a server and stays connected after you turn off your computer or phone. It saves chat messages and replays them when you reconnect, and also helps to hide your IP address. If you are running it on IRCNow, your server may also offer DDoS protection to keep you online.

Before You Begin

Read the introduction to chroots to get familiar with the concept of chroots. In this guide, we use a chroot for extra security.

You will want to set up ZNC once without a chroot, as a normal user without root powers. This will give you a better understanding of how ZNC works before you attempt a more complex, chrooted install.

Public ZNCs

If you are offering a public ZNC service for many users, you will want to get a ddos-filtered IPv4 address and an IPv6 subnet from your internet provider (BuyVM guide).

Installing Dependencies

The below installation was tested on OpenBSD 7.0 stable. ZNC will run inside a chroot at /home/znc.

Before beginning the installation, check to make sure you are using OpenBSD 7.0 and that your system is patched:

$ uname -a
OpenBSD example.com 7.0 GENERIC#224 amd64
$ doas syspatch

Next, we install needed dependencies:

$ doas pkg_add icu4c boost cmake gettext-runtime gettext-tools

Installing from Packages

WARNING: OpenBSD 6.9 (and later) may contain a bug that causes ZNC to crash with a segmentation fault. To avoid this, we recommend applying a custom patch to ZNC as described below in Patched Source.

If you are running on a single core server, you can install znc from packages:

$ doas pkg_add znc

Compiling from Source

Patched Source

Due to a bug in OpenBSD 6.9, we have applied a custom patch to ZNC to avoid segfaults on multicore servers:

diff -ru znc-1.8.2-old/src/main.cpp znc-1.8.2-new/src/main.cpp
--- znc-1.8.2-old/src/main.cpp	Mon Sep  7 18:57:50 2020
+++ znc-1.8.2-new/src/main.cpp	Thu Dec 24 17:04:37 2020
@@ -292,6 +292,7 @@

 int main(int argc, char** argv) {
+    pthread_attr_t a; pthread_attr_init(&a);
     CString sConfig;
     CString sDataDir = "";

The schat module also needs patching for libreSSL?:

--- modules/schat.cpp.orig
+++ modules/schat.cpp
@@ -25,8 +25,8 @@
 #include <znc/User.h>
 #include <znc/IRCNetwork.h>

-    OPENSSL_VERSION_NUMBER < 0x10100007
R < 0x10100007 || \
BER < 0x3040000fL)
 /* SSL_SESSION was made opaque in OpenSSL 1.1.0, cipher acces
sor was added 2
 weeks before the public release.
 See openssl/openssl@e92813234318635639dba0168c7ef5568757449b.

IRCNow provides a patched version of ZNC:

$ cd ~
$ ftp https://ircnow.org/software/znc-1.8.2a.tar.gz

On OpenBSD, ftp? can also be used to download files from the web.

For tar?, the options xvzf stand for e(x)tract, (v)erbose, un(z)ip, and (f)ile.

$ tar xvzf znc-1.8.2a.tar.gz

Next, we build ZNC:

$ cd znc-1.8.2
$ mkdir build
$ cd build
$ cmake ..
$ make
$ doas make install

Upstream Source

You can choose to use the upstream source code, but you must apply patches to get it to build properly.

WARNING: If you are on OpenBSD 7.0, you must patch znc-1.8.2 to avoid a threading bug that causes segfaults and to fix a bug in the schat module.

First, download the latest stable release:

$ cd ~
$ ftp https://znc.in/releases/znc-1.8.2.tar.gz

We recommend you verify the gpg signature:

$ doas pkg_add gnupg
$ ftp https://znc.in/releases/znc-1.8.2.tar.gz.sig
$ gpg2 --recv-key D5823CACB477191CAC0075555AE420CC0209989E
$ gpg2 --verify znc-1.8.2.tar.gz.sig znc-1.8.2.tar.gz

Next, extract and unzip? the files:

$ tar xvzf znc-1.8.2.tar.gz 

Configuring the Chroot

We want to add a new user: create a new unprivileged user account for security:

$ doas useradd -m -d /home/znc -s /sbin/nologin znc

-m tells useradd to create a home folder; -d says the home folder is /home/znc; -s specifies the default shell is /sbin/nologin; meaning no user can login. The username created is znc.

Login Class

Add the following lines to the end of /etc/login.conf:


WARNING: Use tabs and not spaces. Spaces will fail and as a result, znc will not get the file resources it needs.

The login.conf guide explains the attributes in more detail.

Each time znc creates a new connection for a user, it requires at least one file descriptor. So, if your file descriptor limit is set too low, znc will be unable to make new connections to networks. openfiles sets the maximum number of open file descriptors per process. -cur specifies the current limit and -max specifies the maximum limit.

The current and maximum stack size controls how much stack memory a user can use. We set it at 48M to give ZNC plenty of room.

maxproc limits how many processes a user in this class can create. We set the maximum to infinity and the current amount to 4096. tc=daemon means that the default values will come from the daemon login class.

Now we change znc's default login class to znc:

$ doas usermod -L znc znc

This can also be edited with a text editor using vipw.

To confirm that the login class has been changed, check /etc/passwd.

$ doas grep '^znc' /etc/passwd

grep? searches for the line that begins with znc in /etc/passwd.

The 5th field needs to say znc:

$ doas grep '^znc' /etc/passwd | cut -d : -f 5

NOTE: If /etc/login.conf.db exists, make sure to delete it, otherwise login.conf changes won't apply:

$ doas rm /etc/login.conf.db

Next, we temporarily change znc's shell to ksh:

$ doas chsh -s /bin/ksh znc

Next, we login with the username znc, with the login class znc:

$ doas su -c znc znc
$ ulimit -a
time(cpu-seconds)    unlimited
file(blocks)         unlimited
coredump(blocks)     unlimited
data(kbytes)         33554432
stack(kbytes)        32768
lockedmem(kbytes)    329478
memory(kbytes)       985092
nofiles(descriptors) 4096
processes            1310

ulimit -a displays all process limits for our current user.

WARNING: If nofiles(descriptors) is not 4096, you may have an error in your configuration!

Press ctrl+d to signal the end of file to logout, then run as root:

$ ^D
$ doas su

Then run this list of commands:

mkdir -p /home/znc/usr/lib/
mkdir -p /home/znc/usr/local/lib/pkgconfig
mkdir -p /home/znc/usr/local/bin/
mkdir -p /home/znc/usr/local/share/
mkdir -p /home/znc/usr/local/man/man1/
mkdir -p /home/znc/usr/libexec/
mkdir -p /home/znc/etc/ssl
mkdir -p /home/znc/dev/
mkdir -p /home/znc/var/run/
mkdir -p /home/znc/home/znc/.znc
mknod -m 644 /home/znc/dev/random c 45 0
mknod -m 644 /home/znc/dev/urandom c 45 2
mknod -m 666 /home/znc/dev/null c 2 2
cp /usr/lib/lib{c++,c++abi,c,crypto,m,pthread,ssl,z}.so.*      /home/znc/usr/lib/
cp /usr/libexec/ld.so          /home/znc/usr/libexec/
cp /etc/resolv.conf            /home/znc/etc/
cp /etc/ssl/cert.pem           /home/znc/etc/ssl/
cp /var/run/ld.so.hints        /home/znc/var/run/
cp /usr/local/bin/znc          /home/znc/usr/local/bin/
cp /usr/local/man/man1/znc.1          /home/znc/usr/local/man/man1/
cp /usr/local/man/man1/znc-buildmod.1 /home/znc/usr/local/man/man1/
cp /usr/local/bin/znc-buildmod        /home/znc/usr/local/bin/
cp /usr/local/lib/pkgconfig/znc.pc    /home/znc/usr/local/lib/pkgconfig/
cp /usr/local/lib/libicu{uc,data,i18n}.so.*    /home/znc/usr/local/lib/
cp /usr/local/lib/libboost_{locale,chrono,system,thread}-mt.so.*    /home/znc/usr/local/lib/
cp -R /usr/local/share/znc               /home/znc/usr/local/share/
cp -R /usr/local/lib/znc                 /home/znc/usr/local/lib/
cp -R /usr/local/share/icu /home/znc/usr/local/share/
cp -R /usr/local/lib/icu   /home/znc/usr/local/lib/
cp -R /usr/local/include/znc             /home/znc/usr/local/include/
for LANG in pt_BR bg_BG de_DE el_GR es_ES fr_FR id_ID it_IT nl_NL pl_PL ru_RU
	mkdir -p /home/znc/usr/local/share/locale/$LANG/LC_MESSAGES
	cp -R /usr/local/share/locale/$LANG/LC_MESSAGES/znc*.mo /home/znc/usr/local/share/locale/$LANG/LC_MESSAGES
chown -R znc:znc /home/znc/
chown -R root:wheel /home/znc/dev /home/znc/etc /home/znc/usr /home/znc/var
chmod -R o-rx /home/znc/home/znc/.znc/
usermod -G znc _identd

!! Configuring ZNC

!!! Makeconf

At first, you will need to create a conf file:

# export HOME=/home/znc/
# chroot -u znc -g znc /home/znc znc --makeconf
[ .. ] Checking for list of available modules...
[ ** ]
[ ** ] -- Global settings --
[ ** ]
[ ?? ] Listen on port (1025 to 65534): 31337
[ ?? ] Listen using SSL (yes/no) [no]: yes
[ ?? ] Listen using both IPv4 and IPv6 (yes/no) [yes]: no
[ .. ] Verifying the listener...
[ ** ] Unable to locate pem file: [/home/znc/.znc/znc.pem], creating it
[ .. ] Writing Pem file [/home/znc/.znc/znc.pem]...
[ ** ] Enabled global modules [webadmin]
[ ** ]

We're going to configure ZNC to listen on ports 1337 and 31337. Port 1337 will be plaintext, port 31337 will be SSL. This convention is followed by IRCNow's public servers.

WARNING: Do not listen to both IPv4 and IPv6. There appears to be a bug in ZNC v1.8.2 on OpenBSD 6.9 where selecting 'yes' will cause IPv4 to stop working. Instead, as shown later in the guide, you can create separate listeners to allow listening to both IPv4 and IPv6.

You will want to enable connecting both with and without SSL?. Although SSL helps to encrypt messages, some older IRC programs don't support it, so it is important to offer ZNC in plaintext.

ZNC will automatically create its own SSL certificate. This certificate, however, will be self-signed and hence will show up as invalid or untrusted by your IRC client. To get a properly signed SSL certificate, you will need to configure openhttpd and request the certificate with acme-client.

[ ** ] -- Admin user settings --
[ ** ]
[ ?? ] Username (alphanumeric): username
[ ?? ] Enter password:
[ ?? ] Confirm password:
[ ?? ] Nick [username]:
[ ?? ] Alternate nick [username_]:
[ ?? ] Ident [username]:
[ ?? ] Real name (optional):


Next, you'll be asked to specify an optional bindhost. For now, we will leave this blank.

[ ?? ] Bind host (optional):

WARNING: You cannot pick any arbitrary bindhost that you want. If you bindhost does not work, your vhost will not show up properly, and you may not be able to connect at all!

The bindhost is used to pick which IP address to use for a server with multiple IP addresses. It can allow you to pick a nice-looking vhost, like username@user name.example.com, once DNS has been properly set up.

A vhost must have proper forward and reverse DNS records to work. If these have not been configured properly, first leave the bindhost blank.

To test if your bindhost works properly, check the host guide. You will want to run host on your hostname and IP address to make sure they both match.

[ ** ] Enabled user modules [chansaver, controlpanel]
[ ** ]
[ ?? ] Set up a network? (yes/no) [yes]:
[ ** ]

We will turn on the chansaver and controlpanel modules and set up a network.

[ ** ] -- Network settings --
[ ** ]
[ ?? ] Name [freenode]: example
[ ?? ] Server host (host only): irc.example.com
[ ?? ] Server uses SSL? (yes/no) [no]: yes
[ ?? ] Server port (1 to 65535) [6697]:
[ ?? ] Server password (probably empty):
[ ?? ] Initial channels: #channel
[ ** ] Enabled network modules [simple_away]

Configure a network. The network name is usually just a single word; it is not the same as the server host. IRC normally uses port 6667 if there is no SSL, and 6697 if there is SSL.

[ ** ]
[ .. ] Writing config [/home/znc//.znc/configs/znc.conf]...
[ ** ]
[ ** ] To connect to this ZNC you need to connect to it as your IRC server
[ ** ] using the port that you supplied.  You have to supply your login info
[ ** ] as the IRC server password like this: user/network:pass.
[ ** ]
[ ** ] Try something like this in your IRC client...
[ ** ] /server <znc_server_ip> +31337 username:<pass>
[ ** ]
[ ** ] To manage settings, users and networks, point your web browser to
[ ** ] https://<znc_server_ip>:31337/
[ ** ]
[ ?? ] Launch ZNC now? (yes/no) [yes]: no

For now, avoid setting up the web server on port 31337. We will instead use relayd so that the web server can viewed on the default port for https: port 443.

Configuring SSL

In order to provide SSL for the bouncer, you must first configure OpenHTTPd and request a certificate with acme-client.

Once that is done, you can copy the certificate:

# cp /etc/ssl/bnc.example.com.fullchain.pem /home/znc/home/znc/.znc/
# cp /etc/ssl/private/bnc.example.com.key /home/znc/home/znc/.znc/
# chown znc:znc /home/znc/home/znc/.znc/bnc.example.com.*

In the above commands, replace bnc.example.com with your real hostname.

Next, we create dhparam:

# openssl dhparam -out dhparam.pem 2048
# chown znc:znc dhparam.pem
# mv dhparam.pem /home/znc/home/znc/.znc/

Next, edit /home/znc/home/znc/.znc/configs/znc.conf so that the configuration resembles:

// Do NOT edit this file while ZNC is running!
// Use webadmin or *controlpanel instead.
// Altering this file by hand will forfeit all support.
// But if you feel risky, you might want to read help on /znc saveconfig and /znc rehash.
// Also check https://wiki.znc.in/Configuration

AnonIPLimit = 10000
AuthOnlyViaModule = false
ConfigWriteDelay = 60
ConnectDelay = 1
HideVersion = false
LoadModule = chansaver
LoadModule = lastseen
LoadModule = adminlog
LoadModule = identfile
LoadModule = webadmin
LoadModule = certauth
MaxBufferSize = 10000
ProtectWebSessions = true
SSLCertFile = /home/znc/.znc/bnc.example.com.fullchain.pem
SSLDHParamFile = /home/znc/.znc/dhparam.pem
SSLKeyFile = /home/znc/.znc/bnc.example.com.key
PidFile = /home/znc/.znc/znc.pid
ServerThrottle = 1
Version = 1.8.2

<Listener listener0>
        AllowIRC = true
        AllowWeb = false
        Host =
        IPv4 = true
        IPv6 = false
        Port = 1337
        SSL = false
        URIPrefix = /

<Listener listener1>
        AllowIRC = true
        AllowWeb = false
        Host =
        IPv4 = true
        IPv6 = false
        Port = 31337
        SSL = true
        URIPrefix = /

<Listener listener2>
        AllowIRC = true
        AllowWeb = false
        Host = 2001:db8::
        IPv4 = false
        IPv6 = true
        Port = 1337
        SSL = false
        URIPrefix = /

<Listener listener3>
        AllowIRC = true
        AllowWeb = false
        Host = 2001:db8::
        IPv4 = false
        IPv6 = true
        Port = 31337
        SSL = true
        URIPrefix = /

<Listener listener4>
        AllowIRC = true
        AllowWeb = false
        Host =
        IPv4 = true
        IPv6 = false
        Port = 1337
        SSL = false
        URIPrefix = /

<Listener listener5>
        AllowIRC = false
        AllowWeb = true
        Host =
        IPv4 = true
        IPv6 = false
        Port = 1338
        SSL = false
        URIPrefix = /

<User username>
	Pass       = sha256#014eab533f25fe65621963f712ecaf3b86fe840859ab6f4c675200c73d02e6d1#ytZ8/L,:MtZ5cMCe5IJX#
	Admin      = true
	Nick       = username
	AltNick    = username_
	Ident      = username
	LoadModule = chansaver
	LoadModule = controlpanel

	<Network example>
		LoadModule = simple_away
		Server     = irc.example.com +6697 

		<Chan #channel>

Please read the ZNC wiki to understand the meaning of each option.

You will need to replace bnc.example.com with your actual hostname.

In the listeners, you need to replace and 2001:db8:: with your server's public IPv4 and IPv6 address.

NOTE: Do not replace This is localhost? and must not be changed.

It's recommended to keep the ports 1337 for plaintext, 31337 for SSL, and 1338 for web. This convention is followed on the public servers on IRCNow. Note that znc binds to port 1338 without SSL for the web server. We later use relayd to provide TLS acceleration on port 443.

WARNING: Do not listen to both IPv4 and IPv6. As mentioned above, you need separate listeners.

For the username block, use the defaults that ZNC's makeconf generated. Don't edit or delete the values that ZNC generated automatically.

Please read the ZNC wiki to understand the meaning of each option.


Notice the above config loads the identfile module by default. This is necessary to provide proper ident using oidentd. You will need to configure oidentd for the module to work.

Packet Filter

If packet filter? is set to deny all incoming connects, you can add this rule to /etc/pf.conf:

pass in log quick proto tcp to port {http https} keep state (max-src-conn 300, max-src-conn-rate 300/60) #relayd web
pass in log quick proto tcp to port {1337 31337} keep state (max 3000, max-src-conn 300) #bnc

To load the new ruleset:

# pfctl -f /etc/pf.conf

Starting ZNC

To run znc:

# export HOME=/home/znc
# /usr/sbin/chroot -u znc -g znc /home/znc znc >>/var/log/znc.log 2>&1 &

Editing Config

The best way to edit the conf file is by logging in to znc with your IRC client, then using the *status or *controlpanel module. You can also use the webpanel? once it has been set up.

Sometimes, however, it may be necessary to edit your configuration file directly and then reload it (rehash).

Shutting down and restarting

If the only person using ZNC is yourself, you can easily shut down ZNC, edi the conf, then restart ZNC.

# pkill -U znc

This will kill all processes run by the user znc, which should kill the bouncer. Then edit the znc.conf file, and restart ZNC as described above.

Live rehash

If you have other users connected to a public ZNC, or you cannot afford to take ZNC offline, you may need to rehash ZNC. First, log in to znc with your IRC client, then save the config:

/msg *status saveconfig

Next, edit /home/znc/home/znc/.znc/configs/znc.conf. Finally, send a rehash message over IRC to ZNC:

/msg *status rehash

We add a cron job? to have ZNC restart every 5 minutes. ZNC will only start if no other ZNC instance is running, so this is safe:

# echo "HOME=/home/znc" >> /var/cron/tabs/root
# echo "*/5     *       *       *       *       /usr/sbin/chroot -u znc -g znc /home/znc znc >>/var/log/znc.log 2>&1 &" >> /var/cron/tabs/root

Testing ZNC

To test the connection in plaintext (warning: insecure), consult the netcat irc guide.

To test the connection using SSL, run:

$ openssl s_client -connect bnc.example.com:31337

Replace bnc.example.com with your actual hostname. Check to make sure you have the proper SSL cert configured. Once an SSL session has been established, consult the netcat irc guide.

Web Panel

Use relayd to allow visitors to use port 443 to access the ZNC web panel.

Control Panel

See usage for help on how to use the controlpanel.

Custom vhosts

WARNING: do not set individual IPv6 addresses for a user's bindhost. Do not set a user's bindhost to be something like 2605:6400:10::. If you specify an IPv6 address for the bindhost, that user can *only* connect to networks that support IPv6. IPv4-only networks completely fail.

It is better to set the bindhost to be username.fruit.ircnow.org. Then, for the nameserver, create an AAAA record and an A record. That way, the bindhost will use IPv6 if the network is IPv6-only, and IPv4 if it is an IPv4-only network. This solution is more flexible and allows your user to fall back to IPv4 when IPv6 is not supported.

If a user requests to change their vhost, do not edit the bindhost -- all you need to do is update the rDNS record (see the buyvm web panel. Don't delete the bindhost or the DNS A/AAAA records.

For example, suppose the vhost was formerly user.fruit.ircnow.org. If the user wants to change it to example.com, you just need to update the rDNS to example.com, but leave the znc bindhost as username.fruit.ircnow.org and keep the A/AAAA records for username.fruit.ircnow.org.

Once example.com's AAAA record is working, and the rDNS has been configured properly, you can reconnect the user (either the user types /znc connect or you type /msg *controlpanel reconnect <username> <network>), and the vhost will update properly.

Keeping the old A/AAAA record and bindhost working will make it easier if the user changes vhosts or if their 3rd party dns server for example.com fails for whatever reason. This method will allow the user to still connect. Otherwise, if the 3rd party dns server fails for example.com (which frequently happens with cheap, free dns services), users will be unable to connect and blame you.


If your user is getting disconnected, these are the most likely causes:

  1. mismatch of ports or SSL (using plaintext on 6697 or SSL on 6667)
  2. SSL is not supported
  3. user has a server password where none belongs (most likely he confused server password with nickserv password)
  4. ident is not working
  5. ircd bans a certain username or ident for no good reason (the ircd mistakenly assumes your connection is a bot and glines it)
  6. typo of server name or IP address
  7. dns lookup error

You may be need to install icu4c-68.2v0.

IPv4/IPv6 mismatch

If a znc user has a bindhost that is IPv6 only but the network is IPv4 only, or vice versa, it will not connect.

To prevent this, you must use a symbolic hostname (such as username.example.com) for the bindhost. Each hostname needs a single A record and a single AAAA record in the DNS zone. If any part is misconfigured, users will be unable to connect.

To check if any bindhosts are incorrectly using fixed IPv6 or IPv4 addresses, run:

# sed -nE '/.*(\<BindHost|\<DCCBindHost) = (.*:.*|[0-9.]+)/s//\2/p' /home/znc/home/znc/.znc/configs/znc.conf

If you see IP addresses like the following, then your bindhosts are incorrectly set to use IPv4 or IPv6 addreses directly instead of symbolic hostnames:

Bindhost Errors

A quick way to check if there are DNS errors is to run the following two commands:

# sed -nE '/.*(\<BindHost|\<DCCBindHost) = (.*)/s//\2/p' /home/znc/home/znc/.znc/configs/znc.conf | xargs -n 1 -t host

Every hostname should have both an IPv4 and IPv6 address. The IPv4 address should match your DDoS-filtered public address. Each IPv6 address should be unique.

If you see duplicate entries (the same hostname returns multiple IPv6 addresses), you must delete the duplicate DNS entries.

Here are error messages that indicate there are DNS problems with the bindhost:

host username.example.com
;; connection timed out; no servers could be reached

There is an error because means that the DNS server could not be reached.

host username.example.com
Host username.example.com not found: 3(NXDOMAIN)

This indicates there are no DNS records for the bindhost.

$ doas grep -i host /home/znc/home/znc/.znc/configs/znc.conf | grep -v > ~/bindhost
$ vi ~/bindhost

Then with vi:

:%s_.* = _host _g


$ sh ~/bindhost

If you see any records there with only a single IPv4 address but no IPv6, or a single IPv6 but no IPv4, or any NXDOMAIN responses, you need to fix your DNS records. There should be exactly one shared IPv4 and one unique IPv6 for each hostname, and zero NXDOMAIN responses.

Missing libraries

If you are get errors such as:

ld.so: znc: can't load library 'libc++abi.so.5.0'

Then you may be on the wrong OpenBSD version (6.9 or earlier); or you did not apply syspatch; or you did not upgrade all dependencies:

$ doas syspatch
$ doas pkg_add -Uu

Delete the build folder and compile again.