IRCNow

You will want to get a ddos-filtered IPv4 and an IPv6 subnet from your internet provider.

Create the user znc:

$ doas adduser
Name:        znc
Password:    ****
Fullname:    znc
Uid:         10
Gid:         1017 (znc)
Groups:      znc 
Login Class: default
HOME:        /home/znc
Shell:       /sbin/nologin
OK? (y/n) [y]: y

I am not sure if this is necessary, but in /etc/login.conf, I add the following:

znc:\
  :openfiles-cur=4096:\
  :openfiles-max=8182:\
  :openfiles=4096:\
  :stacksize-cur=48M:\
  :stacksize-max=48M:\
  :maxproc-max=infinity:\
  :maxproc-cur=4096:\
  :tc=daemon:

I check to make sure znc is set to the right login class.

$ doas vipw

There should be a line with znc that looks like this (I check to make sure znc has the right login class; the '1001' is the uid, which you may find to be different from this example, but it should not be changed):

znc:*:1001:1001:znc:0:0:znc:/home/znc:/sbin/nologin

You will want to run cap_mkdb:

$ doas cap_mkdb /etc/login.conf

Now change znc shell to /bin/ksh , then continue with the steps below.

$ 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

I then set the default shell to /sbin/nologin (note: the '1001' is the uid, which you may find to be different from this example, but it should not be changed.):

$ doas vipw

...

znc:*:1001:1001:znc:0:0:znc:/home/znc:/sbin/nologin

Run this install script (tested for OpenBSD 6.7 and znc-1.7.5) as root to put znc inside the chroot at /home/znc:

mkdir -p /home/znc/usr/lib/
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/
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/libc++.so.4.0      /home/znc/usr/lib/libc++.so.4.0
cp /usr/lib/libc++abi.so.2.1   /home/znc/usr/lib/libc++abi.so.2.1
cp /usr/lib/libc.so.96.0       /home/znc/usr/lib/libc.so.96.0
cp /usr/lib/libcrypto.so.46.1  /home/znc/usr/lib/libcrypto.so.46.1
cp /usr/lib/libm.so.10.1       /home/znc/usr/lib/libm.so.10.1
cp /usr/lib/libpthread.so.26.1 /home/znc/usr/lib/libpthread.so.26.1
cp /usr/lib/libssl.so.48.1     /home/znc/usr/lib/libssl.so.48.1
cp /usr/lib/libz.so.5.0        /home/znc/usr/lib/libz.so.5.0
cp /usr/libexec/ld.so          /home/znc/usr/libexec/ld.so
cp /etc/resolv.conf            /home/znc/etc/resolv.conf
cp /etc/ssl/cert.pem           /home/znc/etc/ssl/cert.pem
cp /var/run/ld.so.hints        /home/znc/var/run/ld.so.hints
pkg_add -B /home/znc znc
chown -R root:wheel /home/znc/dev /home/znc/etc /home/znc/usr /home/znc/var
chown -R znc:znc /home/znc/home/znc/

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]
[ ** ] 
[ ** ] -- Admin user settings --
[ ** ] 
[ ?? ] Username (alphanumeric): 
# cp /etc/ssl/my.example.com.fullchain.pem /home/znc/home/znc/.znc/
# cp /etc/ssl/private/my.example.com.key /home/znc/home/znc/.znc/
# chown znc:znc /home/znc/home/znc/.znc/my.example.com.*

Inside ~:

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

Afterwards, to run znc:

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

Creating a start script:

doas touch /etc/rc.d/znc
doas chmod +x /etc/rc.d/znc

File contents:

#!/bin/ksh
#
# $OpenBSD: znc,v 1.2 2018/01/11 19:27:07 rpe Exp $

daemon_pidfile="/home/znc/home/znc/.znc/znc.pid"
daemon="env HOME=/home/znc /usr/sbin/chroot -u znc -g znc /home/znc znc"


service_stop() {
  if [ -f $daemon_pidfile ]; then
    pid=$(sed 's/[^0-9]*//g' $daemon_pidfile)
    kill $pid
  fi
}

case "$1" in
  stop)
    service_stop
    ;;
esac


. /etc/rc.d/rc.subr

rc_reload=NO

rc_cmd $1
doas rcctl start znc
doas rcctl stop znc

Now you must add this rule in /etc/pf.conf:

pass in log quick proto tcp to port {http https} keep state (max-src-conn 30, max-src-conn-rate 20/60)
pass in log quick proto tcp to port { 1337 31337 } keep state (max 3000, max-src-conn 200) #bnc
$ doas pfctl -f /etc/pf.conf

(Here the steps are a bit out of chronology)

To start the bouncer, I run this command:

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

If your bouncer is already online, make sure to save the config by logging into your irc client, connected to the bouncer:

/msg *status saveconfig

Then, go add this at the top of /home/znc/home/znc/.znc/configs/znc.conf (yes I deliberately ignore the warnings):

AnonIPLimit = 10000
AuthOnlyViaModule = false
ConfigWriteDelay = 0
ConnectDelay = 5
HideVersion = false
LoadModule = chansaver
LoadModule = lastseen
LoadModule = adminlog
LoadModule = identfile
LoadModule = webadmin
LoadModule = certauth
MaxBufferSize = 10000
ProtectWebSessions = true
SSLCertFile = /home/znc/.znc/my.example.com.fullchain.pem
SSLDHParamFile = /home/znc/.znc/dhparam.pem
SSLKeyFile = /home/znc/.znc/my.example.com.key
PidFile = /home/znc/.znc/znc.pid
ServerThrottle = 30
Version = 1.7.5

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

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

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

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

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

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

We will load the identfile module by default. This is necessary to provide proper ident using oidentd. Please follow the instructions in the link to configure ident.

I have znc bind to port 1338 without SSL for the web server. I will later use relayd to provide TLS acceleration on port 443.

Replace with your own IP addresses. Then, on your irc client logged into the bouncer:

/msg *status rehash
$ doas crontab -e

Add a few lines to have ZNC reconnect every 5 minutes. ZNC will only connect if no other ZNC instance is running:

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

To test the connection (and SSL certificate), run:

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

Make sure you have the proper SSL cert configured.

While you are at it, you will want to redirect any plaintext requests to the webpanel on port 80 to use SSL on port 443. Add this to /etc/httpd.conf:

server "bnc.example.com" {
        listen on * port 80
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
        location * {
                block return 302 "https://$HTTP_HOST$REQUEST_URI"
        }
}

Go ahead and reboot the web server:

$ doas rcctl restart httpd

Note: If you are using IPv6 and IPv4 for the same listener, perl IO::Socket::INET is unable to connect. Use two separate listeners.