Confused beginner's ZNC quickstart

So GnuTLS had a pretty bad CVE a while ago, and I was told by some distrobuilder friends that it doesn't otherwise have a great track record.

I use WeeChat as my IRC client, which links against GnuTLS for IRC-over-TLS functionality. However, I didn't have a great deal of confidence in GnuTLS as a result of this CVE, so I wasn't comfortable having it as the thing standing between my IRC conversations and the rest of the internet. Unfortunately, WeeChat doesn't directly support any other TLS libraries, however that doesn't mean that you can't put something between WeeChat and the rest of the world to do TLS on WeeChat's behalf.

One option is to just use a TCP proxy such as stunnel or an assemblage of tools such as s6-networking to encapsulate an IRC connection from WeeChat to the IRC server in TLS. Alternatively, one could use a fully-fledged IRC bouncer as a more sophisticated application-layer proxy, which is what I chose to do.

The bouncer of choice here is ZNC, which until recently I'd never really used before. As packaged in Debian, however, ZNC is linked against OpenSSL, which meets my requirements. ZNC has been around for a while, and has accumulated a pretty broad amount of functionality: while it functions just fine as a simple IRC client proxy, more sophisticated configurations allow you to set it up as a limited store-and-forward relay (by buffering messages in ZNC and then replaying them to a client), or as a client multiplexor, to allow multiple clients to connect to IRC over a single connection (e.g. a mobile and desktop client).

While I technically only needed this to work as a simple IRC proxy, I set this up as a client muxer, as insurance against me wanting to use that kind of setup in the future. The ZNC wiki helpfully has a handy page on how to set this up, which is a useful starting point and the basis of what's described below. Note that I don't intend to use the web admin interface, and instead want to configure it solely through the IRC-based controlpanel module (which is otherwise enabled by default).

ZNC setup

I installed ZNC and the tools required to build third party ZNC modules, and created a new user under which to run the ZNC instance.

# apt-get install znc znc-dev
# useradd -m -s /bin/bash znc

Switching to the new znc user, I then cloned the the git repositories for the clientbuffer and chanfilter third party modules (as linked on the aforementioned wiki page). The znc-buildmod command is used to compile the modules into loadable shared library objects.

# su -l znc
$ mkdir src; cd src
$ git clone
$ git clone
$ (cd znc-clientbuffer; znc-buildmod clientbuffer.cpp)
$ (cd znc-chanfilter; znc-buildmod chanfilter.cpp)

ZNC requires some first-time interactive setup, which I did by running znc -f -d ~/bouncerdata --makeconf as the znc user:

I then copied the compiled third-party modules into a modules subdirectory of the configuration directory.

$ mkdir ~/bouncerdata/modules
$ cp ~/src/znc-clientbuffer/ ~/src/znc-chanfilter/ \
>     ~/bouncerdata/modules

Then, I opened the ZNC configuration file at ~znc/bouncerdata/configs/znc.conf, found the Listener section, and then added the line Host = localhost so that ZNC only listens on the loopback address and is not otherwise externally reachable.

I configured ZNC to be run in a systemd user session, using the unit file definition below as ~znc/.local/share/systemd/user/znc.service:


ExecStart=/usr/bin/znc -d /home/znc/bouncerdata -f


systemd user sessions are convenient for per-user process supervision. This requires enabling "lingering" mode for the ZNC user with loginctl enable-linger znc as root, which means that any processes owned by the ZNC user will persist after they log out. This service definition can then be started with the following dance:

# su -l znc
$ export XDG_RUNTIME_DIR=/run/user/$(id -u)
$ systemctl --user start znc

I haven't set up ZNC to start at boot time (with systemctl --user enable) however, as I want to manually control when it gets started, so I can connect my WeeChat to it at the same time.

ZNC configuration

Switching tabs over to WeeChat, I then added a server for the ZNC admin user.

/server add znc localhost/6667
/set irc.server.znc.nicks zncadmin,zncadmin_
/set irc.server.znc.username zncadmin
/set irc.server.znc.realname zncadmin
/set irc.server.znc.password <password>
/set irc.server.znc.autoconnect on

ZNC uses the contents of the "username" field to associate clients with users, and networks those users are connected to; this follows the format of username, slash, network name. For example, connecting a client to the ZNC with the username set to sysvinit/freenode would result in the client being recognised as belonging to the user "sysvinit" and connected to the "freenode" network defined for that user. In this case, I didn't specify a network in the username, as I'm just interested at getting access to the IRC control interface.

I then set the default IRCv3 client capabilities to request when connecting in my WeeChat to the values recommended in the ZNC wiki page.

I connected to the "znc" network I just defined in WeeChat, which greeted me with a NOTICE from the *status pseudo-user saying that I don't have any networks configured. ZNC exposes an interface to internal functions through virtual IRC users; ZNC intercepts messages sent to these users instead of proxying them to the IRC server, so one can control ZNC in-band from an IRC client. By default, these virtual users' names start with an asterisk (though this is configurable). The *status user is the main ZNC control interface — you can get a listing of usage information using /query *status help (or similar), formatted in a convenient ASCII art table (though the table is too wide to display properly on my laptop's screen). Each individual module exposes functionality via a virtual user with the same name as the module; for example, the nickserv module can be configured by sending messages to the *nickserv user.

Next, I performed some preliminary setup. I got rid of the web admin module, as I don't intend to use it, by sending unloadmod webadmin to *status. I then added and set up a user for connecting to external networks, using the controlpanel module.

First I added a user for myself, which involves setting the password which I have to present upon connecting to ZNC.

<zncadmin>        adduser sysvinit REDACTEDPASSWORD
<*controlpanel>   User sysvinit added!

I then set various defaults for parameters used when making connections to IRC servers.

<zncadmin>        set nick sysvinit sysvinit
<*controlpanel>   Nick = sysvinit
<zncadmin>        set realname sysvinit Molly Miller
<*controlpanel>   RealName = Molly Miller
<zncadmin>        set ident sysvinit sysvinit
<*controlpanel>   Ident = sysvinit
<zncadmin>        set altnick sysvinit sysxinit
<*controlpanel>   AltNick = sysxinit
<zncadmin>        set quitmsg sysvinit bye
<*controlpanel>   QuitMsg = bye

I set the bind address (used when making outbound connections) to the IPv4 wildcard address, as the server I'm running on this box only has proper reverse DNS set on its IPv4 address (there are reasons for this), and I care about having a pretty hostmask being visible to other users on IRC.

<zncadmin>        set bindhost sysvinit
<*controlpanel>   BindHost =

There are a couple of settings which need to be changed from the defaults.

<zncadmin>        set autoclearchanbuffer sysvinit false
<*controlpanel>   AutoClearChanBuffer = false
<zncadmin>        set autoclearquerybuffer sysvinit false
<*controlpanel>   AutoClearQueryBuffer = false

I set the AutoClearChanBuffer and AutoClearQueryBuffer settings to false, because ZNC buffers lines received from a server before sending them on to a client (in case it's not connected when the lines are received), and by default clears lines in the buffer once they've been relayed. This breaks with multiple clients, as clearing lines from the buffer once they've been sent to one client means that they're not available for another client, so both of these settings need to be turned off.

Additionally, I enabled the chansaver module for my user, as this means that ZNC will remember the channels that I'm in between client disconnects and reconnects, and ZNC restarts. Many IRC clients have autojoin functionality which does much the same thing, but ZNC can manage this too, which is useful in the multi-client use case.

<zncadmin>        loadmodule sysvinit chansaver
<*controlpanel>   Loaded module chansaver

I then added a network for this newly created user, and enable some network modules which provide useful functionality.

<zncadmin>        addnetwork sysvinit freenode
<*controlpanel>   Network freenode added to user sysvinit.
<zncadmin>        loadnetmodule sysvinit freenode route_replies
<*controlpanel>   Loaded module route_replies
<zncadmin>        loadnetmodule sysvinit freenode chanfilter
<*controlpanel>   Loaded module chanfilter
<zncadmin>        loadnetmodule sysvinit freenode clientbuffer autoadd
<*controlpanel>   Loaded module clientbuffer
<zncadmin>        loadnetmodule sysvinit freenode nickserv REDACTEDPASSWORD
<*controlpanel>   Loaded module nickserv

This defined a network called "freenode", and enabled the route_replies, chanfilter, clientbuffer, and nickserv modules. These are all described in full on the ZNC wiki, but in brief:

The two modules which perform client tracking require you to connect to ZNC with a unique client identifier in your IRC username; to identify my WeeChat to ZNC when connecting as the user "sysvinit" to the network "freenode", I would set my username to sysvinit@weechat/freenode.

Now, at this point ZNC wasn't yet connected to "freenode", as no servers have been defined yet, but it automatically connects once this happens. So I then set up my WeeChat to connect via ZNC before switching things over.

WeeChat (re)configuration

Most of the settings I had in my WeeChat didn't need to be modified, but there were a couple of connection settings which needed to be modified for connecting to ZNC instead of a remote IRC server.

First, I needed to connect to localhost instead of an external address for this network.

/set irc.server.freenode.addresses localhost/6667
/set irc.server.freenode.local_hostname

I then changed the username to an identifying string which ZNC will understand, and set the password required to access the ZNC user.

/set irc.server.freenode.username sysvinit@weechat/freenode
/set irc.server.freenode.password REDACTEDPASSWORD

I also didn't need SSL enabled, as I was connecting in plaintext over the loopback, I cleared the autojoin list, as that part's handled by ZNC, and I removed the post-connection command which I used for authenticating to NickServ.

/set irc.server.freenode.ssl null
/set irc.server.freenode.autojoin null
/set irc.server.freenode.command null

At this point, everything was ready to go. So the last thing to do was to reconnect through ZNC. I first changed nick (so that I wouldn't collide with the nick I set ZNC to use), and I then added the server to the network defined in ZNC for my user:

<zncadmin>        addserver sysvinit freenode +6697
<*controlpanel>   Added IRC Server +6697 to network freenode for user sysvinit.

Eventually, a new "sysvinit" user showed up on IRC, so ZNC managed to connect successfully, so I then ran a /reconnect in WeeChat, to make it pick up the new connection settings. Conveniently enough, it all worked, which is pretty neat given that I'd never used this software before.


Having ZNC around is reasonably convenient for little things like appearing to stay connected to IRC when restarting my WeeChat for updates. There are also applications for putting IRC bots behind a bouncer, for similar means of concealing restarts, especially during development and testing. (I believe I've seen one IRC bot once where each piece of functionality was implemented in separate client processes behind a ZNC, and used ZNC to mux these all over a single client connection to the IRC server.)

A minor nit is that ZNC gets a bit noisy if it disconnects and reconnects, and it gets particularly upset if the connection to the server becomes unstable, though I've only had that happen once or twice when the IRC servers in question were having a particularly bad (and netsplit-ful) day.