unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

commit 3d97a9500c0af7215aa90e78da932d13ea16e522
parent e3f051076e70c917513ed3857773f523ad5dd8ab
Author: acidvegas <acid.vegas@acid.vegas>
Date: Sat, 19 Nov 2022 23:12:40 -0500

Updated to 6.0.4

Diffstat:
MConfig | 2+-
MMakefile.windows | 21++++++++++++++++++++-
MREADME.md | 15+++++++++++----
Mconfigure | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mconfigure.ac | 8+++++---
Mdoc/Config.header | 2+-
Mdoc/RELEASE-NOTES.md | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mdoc/conf/modules.conf | 12+++++++++---
Mdoc/conf/opers.conf | 1+
Mdoc/conf/unrealircd.remote.conf | 106++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mextras/doxygen/Doxyfile | 2+-
Mextras/unrealircd-upgrade-script.in | 2+-
Minclude/config.h | 2+-
Minclude/h.h | 59+++++++++++++++++++++++++++++++++++++++++------------------
Minclude/license.h | 17+++++++++--------
Minclude/modules.h | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Minclude/struct.h | 44++++++++++++++++++++++++++++++++++++--------
Minclude/windows/setup.h | 4++--
Msrc/Makefile.in | 2+-
Msrc/api-efunctions.c | 13++++++++-----
Msrc/api-extban.c | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/channel.c | 3---
Msrc/conf.c | 422++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/conf_preprocessor.c | 2+-
Msrc/crashreport.c | 4++--
Msrc/ircd.c | 4++--
Msrc/ircd_vars.c | 2+-
Msrc/log.c | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/misc.c | 298+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/modulemanager.c | 24+++++++++++++-----------
Msrc/modules/Makefile.in | 8++++----
Msrc/modules/antimixedutf8.c | 19+++++++++++++++++--
Msrc/modules/antirandom.c | 40++++++++++++++++++++++------------------
Msrc/modules/blacklist.c | 25++++++++++++++++++++-----
Msrc/modules/bot-tag.c | 15++++++++++++++-
Msrc/modules/certfp.c | 25++++++++++++++++++++++++-
Msrc/modules/chanmodes/floodprot.c | 2+-
Msrc/modules/chanmodes/history.c | 2+-
Msrc/modules/chanmodes/operonly.c | 9++++-----
Asrc/modules/channel-context.c | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/channeldb.c | 26+++++++++++++++++++++-----
Msrc/modules/chathistory.c | 2+-
Msrc/modules/chghost.c | 6+++---
Msrc/modules/connect.c | 4++--
Msrc/modules/connthrottle.c | 101+++++++++++++++++++++++++++----------------------------------------------------
Asrc/modules/creationtime.c | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/extbans/account.c | 27+++++++++++++++++++++------
Msrc/modules/extbans/certfp.c | 35++++++++++++++++++++++++++---------
Msrc/modules/extbans/country.c | 23++++++++++++++++++++---
Msrc/modules/extbans/realname.c | 25++++++++++++++++++++-----
Msrc/modules/extbans/securitygroup.c | 23++++++++++++++++++++---
Msrc/modules/extbans/timedban.c | 2+-
Msrc/modules/extended-monitor.c | 2+-
Asrc/modules/geoip-tag.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/geoip_base.c | 21+++++++++++++++++++--
Msrc/modules/geoip_classic.c | 2+-
Msrc/modules/geoip_maxmind.c | 2+-
Msrc/modules/hideserver.c | 2+-
Msrc/modules/history_backend_mem.c | 2+-
Msrc/modules/history_backend_null.c | 2+-
Msrc/modules/ident_lookup.c | 2+-
Msrc/modules/ison.c | 3+--
Msrc/modules/join.c | 41+++++++++++++++++++----------------------
Msrc/modules/kill.c | 2+-
Msrc/modules/labeled-response.c | 8++++++--
Msrc/modules/message.c | 103+++----------------------------------------------------------------------------
Msrc/modules/mode.c | 63++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/modules/names.c | 3+--
Msrc/modules/nick.c | 25++++++++++++++++---------
Msrc/modules/oper.c | 215+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/modules/operinfo.c | 10+++++-----
Msrc/modules/protoctl.c | 4++--
Msrc/modules/reputation.c | 22+++++++++++++++++++++-
Msrc/modules/restrict-commands.c | 57+++++++++++++++++++++++++++++++++------------------------
Msrc/modules/sapart.c | 2++
Msrc/modules/server.c | 102+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/modules/sethost.c | 1-
Msrc/modules/sinfo.c | 2+-
Msrc/modules/stats.c | 34+++++++++++++++++++++-------------
Msrc/modules/svsmode.c | 2+-
Msrc/modules/svsnoop.c | 2+-
Asrc/modules/svso.c | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/targetfloodprot.c | 2+-
Msrc/modules/tkl.c | 284+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/modules/tkldb.c | 1+
Msrc/modules/tls_antidos.c | 2+-
Msrc/modules/tls_cipher.c | 26+++++++++++++++++++++++++-
Msrc/modules/unreal_server_compat.c | 2+-
Msrc/modules/vhost.c | 3+--
Msrc/modules/watch-backend.c | 2+-
Msrc/modules/watch.c | 3+--
Msrc/modules/websocket.c | 2+-
Msrc/modules/whois.c | 49++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/modules/whox.c | 2+-
Msrc/operclass.c | 16++++++++++------
Asrc/securitygroup.c | 840+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/serv.c | 8++++++--
Msrc/socket.c | 15++++++++++++---
Msrc/support.c | 2+-
Msrc/tls.c | 2+-
Msrc/user.c | 208++++++++-----------------------------------------------------------------------
Msrc/version.c.SH | 2+-
Msrc/windows/UnrealIRCd.exe.manifest | 2+-
Msrc/windows/gui.c | 20+-------------------
Msrc/windows/unrealinst.iss | 2+-
Msrc/windows/wingui.rc | 7+------

106 files changed, 3369 insertions(+), 1290 deletions(-)

diff --git a/Config b/Config
@@ -375,7 +375,7 @@ echo "We will now ask you a number of questions. You can just press ENTER to acc
 echo ""
 
 # This needs to be updated each release so auto-upgrading works for settings, modules, etc!!:
-UNREALRELEASES="unrealircd-6.0.2 unrealircd-6.0.1.1 unrealircd-6.0.1 unrealircd-6.0.0 unrealircd-6.0.0-rc2 unrealircd-6.0.0-rc1 unrealircd-6.0.0-beta4 unrealircd-6.0.0-beta3 unrealircd-6.0.0-beta2 unrealircd-6.0.0-beta1 unrealircd-5.2.3 unrealircd-5.2.2 unrealircd-5.2.1.1 unrealircd-5.2.1 unrealircd-5.2.1-rc1 unrealircd-5.2.0.2 unrealircd-5.2.0.1 unrealircd-5.2.0 unrealircd-5.2.0-rc1 unrealircd-5.0.9.1 unrealircd-5.0.9 unrealircd-5.0.9-rc1 unrealircd-5.0.8 unrealircd-5.0.8-rc1 unrealircd-5.0.7 unrealircd-5.0.7-rc1 unrealircd-5.0.6"
+UNREALRELEASES="unrealircd-6.0.4.1 unrealircd-6.0.4 unrealircd-6.0.4-rc2 unrealircd-6.0.4-rc1 unrealircd-6.0.3 unrealircd-6.0.2 unrealircd-6.0.1.1 unrealircd-6.0.1 unrealircd-6.0.0 unrealircd-6.0.0-rc2 unrealircd-6.0.0-rc1 unrealircd-6.0.0-beta4 unrealircd-6.0.0-beta3 unrealircd-6.0.0-beta2 unrealircd-6.0.0-beta1 unrealircd-5.2.3 unrealircd-5.2.2 unrealircd-5.2.1.1 unrealircd-5.2.1 unrealircd-5.2.1-rc1 unrealircd-5.2.0.2 unrealircd-5.2.0.1 unrealircd-5.2.0 unrealircd-5.2.0-rc1 unrealircd-5.0.9.1 unrealircd-5.0.9 unrealircd-5.0.9-rc1 unrealircd-5.0.8 unrealircd-5.0.8-rc1 unrealircd-5.0.7 unrealircd-5.0.7-rc1 unrealircd-5.0.6"
 if [ -f "config.settings" ]; then
 	. ./config.settings
 else
diff --git a/Makefile.windows b/Makefile.windows
@@ -178,7 +178,7 @@ EXP_OBJ_FILES=src/ircd_vars.obj src/channel.obj src/send.obj src/socket.obj \
  src/fdlist.obj src/dbuf.obj  \
  src/hash.obj src/parse.obj \
  src/whowas.obj \
- src/misc.obj src/match.obj src/crule.obj \
+ src/securitygroup.obj src/misc.obj src/match.obj src/crule.obj \
  src/debug.obj  src/support.obj src/list.obj \
  src/serv.obj src/user.obj \
  src/version.obj src/ircsprintf.obj \
@@ -254,6 +254,7 @@ DLL_FILES=\
  src/modules/close.dll \
  src/modules/connect.dll \
  src/modules/connthrottle.dll \
+ src/modules/creationtime.dll \
  src/modules/cycle.dll \
  src/modules/dccallow.dll \
  src/modules/dccdeny.dll \
@@ -278,6 +279,7 @@ DLL_FILES=\
  src/modules/geoip_base.dll \
  src/modules/geoip_classic.dll \
  src/modules/geoip_csv.dll \
+ src/modules/geoip-tag.dll \
  src/modules/globops.dll \
  src/modules/help.dll \
  src/modules/hideserver.dll \
@@ -364,6 +366,7 @@ DLL_FILES=\
  src/modules/svsnline.dll \
  src/modules/svsnolag.dll \
  src/modules/svsnoop.dll \
+ src/modules/svso.dll \
  src/modules/svspart.dll \
  src/modules/svssilence.dll \
  src/modules/svssno.dll \
@@ -379,6 +382,7 @@ DLL_FILES=\
  src/modules/trace.dll \
  src/modules/tsctl.dll \
  src/modules/typing-indicator.dll \
+ src/modules/channel-context.dll \
  src/modules/umode2.dll \
  src/modules/unreal_server_compat.dll \
  src/modules/unsqline.dll \
@@ -502,6 +506,9 @@ src/conf_preprocessor.obj: src/conf_preprocessor.c $(INCLUDES)
 src/debug.obj: src/debug.c $(INCLUDES)
         $(CC) $(CFLAGS) src/debug.c
 
+src/securitygroup.obj: src/securitygroup.c $(INCLUDES)
+        $(CC) $(CFLAGS) src/securitygroup.c
+
 src/misc.obj: src/misc.c $(INCLUDES) ./include/dbuf.h
         $(CC) $(CFLAGS) src/misc.c
 
@@ -828,6 +835,9 @@ src/modules/connect.dll: src/modules/connect.c $(INCLUDES)
 src/modules/connthrottle.dll: src/modules/connthrottle.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/connthrottle.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/connthrottle.pdb $(MODLFLAGS)
 
+src/modules/creationtime.dll: src/modules/creationtime.c $(INCLUDES)
+	$(CC) $(MODCFLAGS) src/modules/creationtime.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/creationtime.pdb $(MODLFLAGS)
+
 src/modules/cycle.dll: src/modules/cycle.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/cycle.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/cycle.pdb $(MODLFLAGS)
 
@@ -900,6 +910,9 @@ src/modules/geoip_classic.dll: src/modules/geoip_classic.c $(INCLUDES)
 src/modules/geoip_csv.dll: src/modules/geoip_csv.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/geoip_csv.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip_csv.pdb $(MODLFLAGS)
 
+src/modules/geoip-tag.dll: src/modules/geoip-tag.c $(INCLUDES)
+	$(CC) $(MODCFLAGS) src/modules/geoip-tag.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip-tag.pdb $(MODLFLAGS)
+
 src/modules/geoip_maxmind.dll: src/modules/geoip_maxmind.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/geoip_maxmind.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip_maxmind.pdb $(MODLFLAGS)
 
@@ -1161,6 +1174,9 @@ src/modules/svsnolag.dll: src/modules/svsnolag.c $(INCLUDES)
 src/modules/svsnoop.dll: src/modules/svsnoop.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/svsnoop.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/svsnoop.pdb $(MODLFLAGS)
 
+src/modules/svso.dll: src/modules/svso.c $(INCLUDES)
+	$(CC) $(MODCFLAGS) src/modules/svso.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/svso.pdb $(MODLFLAGS)
+
 src/modules/svspart.dll: src/modules/svspart.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/svspart.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/svspart.pdb $(MODLFLAGS)
 
@@ -1206,6 +1222,9 @@ src/modules/tsctl.dll: src/modules/tsctl.c $(INCLUDES)
 src/modules/typing-indicator.dll: src/modules/typing-indicator.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/typing-indicator.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/typing-indicator.pdb $(MODLFLAGS)
 
+src/modules/channel-context.dll: src/modules/channel-context.c $(INCLUDES)
+	$(CC) $(MODCFLAGS) src/modules/channel-context.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/channel-context.pdb $(MODLFLAGS)
+
 src/modules/umode2.dll: src/modules/umode2.c $(INCLUDES)
 	$(CC) $(MODCFLAGS) src/modules/umode2.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/umode2.pdb $(MODLFLAGS)
 
diff --git a/README.md b/README.md
@@ -13,10 +13,17 @@ online documentation.
 * UnrealIRCd 6 is the *stable* series since December 2021. All new features go in there.
 * UnrealIRCd 5 is the *oldstable* series. It will receive bug fixes until
   July 1, 2022 plus another 12 months of security fixes.
+* For full details of release scheduling and EOL dates, see
+  [UnrealIRCd releases](https://www.unrealircd.org/docs/UnrealIRCd_releases) on the wiki
 
 ## How to get started
-Please consult our excellent online documentation at https://www.unrealircd.org/docs/
-when setting up the IRCd!
+### Use the wiki!
+**IMPORTANT:** We recommend you follow our installation guide on the wiki instead of the
+steps in this README. The wiki has more detailed information and is more easy to navigate.
+* [Installing from source for *NIX](https://www.unrealircd.org/docs/Installing_from_source)
+* [Installating instructions for Windows](https://www.unrealircd.org/docs/Installing_(Windows))
+
+Please consult the online documentation at https://www.unrealircd.org/docs/ when setting up the IRCd!
 
 ### Step 1: Installation
 #### Windows
@@ -36,11 +43,11 @@ Do the following steps under a separate account for running UnrealIRCd,
 * Now change to the directory where you installed UnrealIRCd, e.g. `cd /home/xxxx/unrealircd`
 
 ### Step 2: Configuration
-Configuration files are stored in the conf/ folder by default (eg: /home/xxxx/unrealircd/conf)
+Configuration files are stored in the `conf/` folder by default (eg: `/home/xxxx/unrealircd/conf`)
 
 #### Create a configuration file
 If you are new, then you need to create your own configuration file:
-Copy conf/examples/example.conf to conf/ and call it unrealircd.conf.
+Copy `conf/examples/example.conf` to `conf/` and call it `unrealircd.conf`.
 Then open it in an editor and carefully modify it using the documentation and FAQ as a guide (see below).
 
 ### Step 3: Booting
diff --git a/configure b/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.69 for unrealircd 6.0.3.
+# Generated by GNU Autoconf 2.69 for unrealircd 6.0.4.2.
 #
 # Report bugs to <https://bugs.unrealircd.org/>.
 #
@@ -580,8 +580,8 @@ MAKEFLAGS=
 # Identity of this package.
 PACKAGE_NAME='unrealircd'
 PACKAGE_TARNAME='unrealircd'
-PACKAGE_VERSION='6.0.3'
-PACKAGE_STRING='unrealircd 6.0.3'
+PACKAGE_VERSION='6.0.4.2'
+PACKAGE_STRING='unrealircd 6.0.4.2'
 PACKAGE_BUGREPORT='https://bugs.unrealircd.org/'
 PACKAGE_URL='https://unrealircd.org/'
 
@@ -1347,7 +1347,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures unrealircd 6.0.3 to adapt to many kinds of systems.
+\`configure' configures unrealircd 6.0.4.2 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1413,7 +1413,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of unrealircd 6.0.3:";;
+     short | recursive ) echo "Configuration of unrealircd 6.0.4.2:";;
    esac
   cat <<\_ACEOF
 
@@ -1589,7 +1589,7 @@ fi
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-unrealircd configure 6.0.3
+unrealircd configure 6.0.4.2
 generated by GNU Autoconf 2.69
 
 Copyright (C) 2012 Free Software Foundation, Inc.
@@ -1958,7 +1958,7 @@ cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by unrealircd $as_me 6.0.3, which was
+It was created by unrealircd $as_me 6.0.4.2, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   $ $0 $@
@@ -2366,7 +2366,7 @@ _ACEOF
 
 
 # Minor version number (e.g.: Z in X.Y.Z)
-UNREAL_VERSION_MINOR="3"
+UNREAL_VERSION_MINOR="4"
 
 cat >>confdefs.h <<_ACEOF
 #define UNREAL_VERSION_MINOR $UNREAL_VERSION_MINOR
@@ -2376,7 +2376,7 @@ _ACEOF
 # The version suffix such as a beta marker or release candidate
 # marker. (e.g.: -rcX for unrealircd-3.2.9-rcX). This macro is a
 # string instead of an integer because it contains arbitrary data.
-UNREAL_VERSION_SUFFIX=""
+UNREAL_VERSION_SUFFIX=".2"
 
 cat >>confdefs.h <<_ACEOF
 #define UNREAL_VERSION_SUFFIX "$UNREAL_VERSION_SUFFIX"
@@ -5644,6 +5644,54 @@ ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
 ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
 ac_compiler_gnu=$ac_cv_c_compiler_gnu
 
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether C compiler accepts -Wformat-overflow" >&5
+$as_echo_n "checking whether C compiler accepts -Wformat-overflow... " >&6; }
+if ${ax_cv_check_cflags__Werror___Wformat_overflow+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+
+  ax_check_save_flags=$CFLAGS
+  CFLAGS="$CFLAGS -Werror  -Wformat-overflow"
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main ()
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"; then :
+  ax_cv_check_cflags__Werror___Wformat_overflow=yes
+else
+  ax_cv_check_cflags__Werror___Wformat_overflow=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+  CFLAGS=$ax_check_save_flags
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_check_cflags__Werror___Wformat_overflow" >&5
+$as_echo "$ax_cv_check_cflags__Werror___Wformat_overflow" >&6; }
+if test x"$ax_cv_check_cflags__Werror___Wformat_overflow" = xyes; then :
+  CFLAGS="$CFLAGS -Wno-format-overflow"
+else
+  :
+fi
+
+  ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
   { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether C compiler accepts -Wunused" >&5
 $as_echo_n "checking whether C compiler accepts -Wunused... " >&6; }
 if ${ax_cv_check_cflags__Werror___Wunused+:} false; then :
@@ -9414,7 +9462,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by unrealircd $as_me 6.0.3, which was
+This file was extended by unrealircd $as_me 6.0.4.2, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -9477,7 +9525,7 @@ _ACEOF
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-unrealircd config.status 6.0.3
+unrealircd config.status 6.0.4.2
 configured by $0, generated by GNU Autoconf 2.69,
   with options \\"\$ac_cs_config\\"
 
diff --git a/configure.ac b/configure.ac
@@ -7,7 +7,7 @@ dnl src/windows/unrealinst.iss
 dnl doc/Config.header
 dnl src/version.c.SH
 
-AC_INIT([unrealircd], [6.0.3], [https://bugs.unrealircd.org/], [], [https://unrealircd.org/])
+AC_INIT([unrealircd], [6.0.4.2], [https://bugs.unrealircd.org/], [], [https://unrealircd.org/])
 AC_CONFIG_SRCDIR([src/ircd.c])
 AC_CONFIG_HEADER([include/setup.h])
 AC_CONFIG_AUX_DIR([autoconf])
@@ -34,13 +34,13 @@ UNREAL_VERSION_MAJOR=["0"]
 AC_DEFINE_UNQUOTED([UNREAL_VERSION_MAJOR], [$UNREAL_VERSION_MAJOR], [Major version number (e.g.: Y for X.Y.Z)])
 
 # Minor version number (e.g.: Z in X.Y.Z)
-UNREAL_VERSION_MINOR=["3"]
+UNREAL_VERSION_MINOR=["4"]
 AC_DEFINE_UNQUOTED([UNREAL_VERSION_MINOR], [$UNREAL_VERSION_MINOR], [Minor version number (e.g.: Z for X.Y.Z)])
 
 # The version suffix such as a beta marker or release candidate
 # marker. (e.g.: -rcX for unrealircd-3.2.9-rcX). This macro is a
 # string instead of an integer because it contains arbitrary data.
-UNREAL_VERSION_SUFFIX=[""]
+UNREAL_VERSION_SUFFIX=[".2"]
 AC_DEFINE_UNQUOTED([UNREAL_VERSION_SUFFIX], ["$UNREAL_VERSION_SUFFIX"], [Version suffix such as a beta marker or release candidate marker. (e.g.: -rcX for unrealircd-3.2.9-rcX)])
 
 AC_PATH_PROG(RM,rm)
@@ -237,6 +237,8 @@ check_cc_flag([-Wformat-zero-length], [CFLAGS="$CFLAGS -Wno-format-zero-length"]
 
 check_cc_flag([-Wformat-truncation], [CFLAGS="$CFLAGS -Wno-format-truncation"])
 
+check_cc_flag([-Wformat-overflow], [CFLAGS="$CFLAGS -Wno-format-overflow"])
+
 dnl While it can be useful to occasionally to compile with warnings about
 dnl unused variables and parameters, we often 'think ahead' when coding things
 dnl so they may be useless now but not later. Similarly, for variables, we
diff --git a/doc/Config.header b/doc/Config.header
@@ -7,7 +7,7 @@
  \___/|_| |_|_|  \___|\__,_|_|\___/\_| \_| \____/\__,_|
 
                                Configuration Program
-                                for UnrealIRCd 6.0.3
+                                for UnrealIRCd 6.0.4.2
                                     
 This program will help you to compile your IRC server, and ask you
 questions regarding the compile-time settings of it during the process. 
diff --git a/doc/RELEASE-NOTES.md b/doc/RELEASE-NOTES.md
@@ -1,15 +1,183 @@
-UnrealIRCd 6.0.3
-=================
+UnrealIRCd 6.0.4.2
+===================
+Another small update to 6.0.4.x:
 
-A number of serious issues were discovered in UnrealIRCd 6. Among these is
-an issue which will likely crash the IRCd sooner or later if you /REHASH
-with any active clients connected.
-We suggest everyone who is running UnrealIRCd 6 to upgrade to 6.0.3.
+* Fix crash when linking. This requires a certain sequence of events: first
+  a server is linked in successfully, then we need to REHASH, and then a new
+  link attempt has to come in with the same server name (for example because
+  there is a network issue and the old link has not timed out yet).
+  If all that happens, then an UnreaIRCd 6 server may crash, but not always.
+* Two IRCv3 specifications were ratified which we already supported as drafts:
+  * Change CAP `draft/extended-monitor` to `extended-monitor`
+  * Add message-tag `bot` next to existing (for now) `draft/bot`
+* Update Turkish translations
+
+UnrealIRCd 6.0.4.1
+===================
+This is a small update to 6.0.4. It fixes the following issues that were
+present in all 6.0.x versions:
+
+* Fix sporadic crash when linking a server (after successful authentication).
+  This feels like a compiler bug. It affected only some people with GCC and
+  only in some situations. When compiled with clang there was no problem.
+  Hopefully we can work around it this way.
+* Make /INVITE bypass (nearly) all channel mode restrictions, as it used to
+  be in UnrealIRCd 5.x. Both for invites by channel ops and for OperOverride.
+  This also fixes a bug where an IRCOp with OperOverride could not bypass +l
+  (limit) and other restrictions and would have to resort back to using
+  MODE or SAMODE. Only +b and +i could be bypassed via INVITE OperOverride.
+
+UnrealIRCd 6.0.4
+-----------------
+This release comes with lots of features and enhancements. In particular,
+security groups and mask items now allow you to write cleaner and more
+flexible configuration files. There are also JSON logging enhancements and
+several bug fixes. Thanks a lot to everyone who tested the release candidates!
 
 If you are already running UnrealIRCd 6 then read below. Otherwise, jump
 straight to the [summary about UnrealIRCd 6](#Summary) to learn more
 about UnrealIRCd 6.
 
+### Enhancements:
+* Show security groups in `WHOIS`
+* The [security-group block](https://www.unrealircd.org/docs/Security-group_block)
+  has been expanded and the same functionality is now available in
+  [mask items](https://www.unrealircd.org/docs/Mask_item) too:
+  * This means the existing options like `identified`, `webirc`, `tls` and
+    `reputation-score` can be used in `allow::mask` etc.
+  * New options (in both security-group and mask) are:
+    * `connect-time`: time a user is connected to IRC
+    * `security-group`: to check another security group
+    * `account`: services account name
+    * `country`: country code, as found by GeoIP
+    * `realname`: realname (gecos) of the user
+    * `certfp`: certificate fingerprint
+  * Every option also has an exclude- variant, eg. `exclude-country`.
+    If a user matches any `exclude-` option then it is considered not a match.
+  * The modules [connthrottle](https://www.unrealircd.org/docs/Connthrottle),
+    [restrict-commands](https://www.unrealircd.org/docs/Set_block#set::restrict-commands)
+    and [antirandom](https://www.unrealircd.org/docs/Set_block#set::antirandom)
+    now use the new `except` sub-block which is a mask item. The old syntax
+    (eg <code>set::antirandom::except-webirc</code>) is still accepted by UnrealIRCd
+    and converted to the appropriate new setting behind the scenes
+    (<code>set::antirandom::except::webirc</code>).
+  * The modules [blacklist](https://www.unrealircd.org/docs/Blacklist_block)
+    and [antimixedutf8](https://www.unrealircd.org/docs/Set_block#set::antimixedutf8)
+    now also support the `except` block (a mask item).
+  * Other than that the extended functionality is available in these blocks:
+    `allow`, `oper`, `tld`, `vhost`, `deny channel`, `allow channel`.
+  * Example of direct use in a ::mask item:
+    ```
+    /* Spanish MOTD for Spanish speaking countries */
+    tld {
+        mask { country { ES; AR; BO; CL; CO; CR; DO; EC; SV; GT; HN; MX; NI; PA; PY; PE; PR; UY; VE; } }
+        motd "motd.es.txt";
+        rules "rules.es.txt";
+    }
+    ```
+  * Example of defining a security group and using it in a mask item later:
+    ```
+    security-group irccloud {
+        mask { ip1; ip2; ip3; ip4; }
+    }
+    allow {
+        mask { security-group irccloud; }
+        class clients;
+        maxperip 128;
+    }
+    except ban {
+        mask { security-group irccloud; }
+        type { blacklist; connect-flood; handshake-data-flood; }
+    }
+    ```
+* Because the mask item is so powerful now, the `password` in the
+  [oper block](https://www.unrealircd.org/docs/Oper_block) is optional now.
+* We now support oper::auto-login, which means the user will become IRCOp
+  automatically if they match the conditions on-connect. This can be used
+  in combination with
+  [certificate fingerprint](https://www.unrealircd.org/docs/Certificate_fingerprint)
+  authentication for example:
+  ```
+  security-group Syzop { certfp "1234etc."; }
+  oper Syzop {
+      auto-login yes;
+      mask { security-group Syzop; }
+      operclass netadmin-with-override;
+      class opers;
+  }
+  except ban {
+      mask { security-group Syzop; }
+      type all;
+  }
+  ```
+* For [JSON logging](https://www.unrealircd.org/docs/JSON_logging) a number
+  of fields were added when a client is expanded:
+  * `geoip`: with subitem `country_code` (eg. `NL`)
+  * `tls`: with subitems `cipher` and `certfp`
+  * Under subitem `users`:
+    * `vhost`: if the visible host differs from the realhost then this is
+      set (thus for both vhost and cloaked host)
+    * `cloakedhost`: this is always set (except for eg. services users), even
+      if the user is not cloaked so you can easily search on a cloaked host.
+    * `idle_since`: last time the user has spoken (local clients only)
+    * `channels`: list of channels (array), with a maximum of 384 chars.
+* The JSON logging now also strips ASCII below 32, so color- and
+  control codes.
+* Support IRCv3 `+draft/channel-context`
+* Add `example.es.conf` (Spanish example configuration file)
+* The country of users is now communicated in the
+  [message-tag](https://www.unrealircd.org/docs/Message_tags)
+  `unrealircd.org/geoip` (only to IRCOps).
+* Add support for linking servers via UNIX domain sockets
+  (`link::outgoing::file`).
+
+### Fixes:
+* Crash in `except ban` with `~security-group:xyz`
+* Crash if hideserver module was loaded but `LINKS` was not blocked.
+* Crash on Windows when using the "Rehash" GUI option.
+* Infinite loop if one security-group referred to another.
+* Duplicate entries in the `+beI` lists of `+P` channels.
+* Regular users were able to -o a service bot (that has umode +S)
+* Module manager did not stop on compile error
+* [`set::modes-on-join`](https://www.unrealircd.org/docs/Set_block#set::modes-on-join)
+  did not work with `+f` + timed bans properly, eg `[3t#b1]:10`
+* Several log messages were missing some information.
+* Reputation syncing across servers had a small glitch. Fix is mostly
+  useful for servers that were not linked to the network for days or weeks.
+
+### Changes:
+* Clarified that UnrealIRCd is licensed as "GPLv2 or later"
+* Fix use of variables in
+  [`set::reject-message](https://www.unrealircd.org/docs/Set_block#set::reject-message)
+  and in [`blacklist::reason](https://www.unrealircd.org/docs/Blacklist_block):
+  previously short forms of variables were (unintentionally) expanded
+  as well, such as `$serv` for `$server`. This is no longer supported, you need
+  to use the correct full variable names.
+
+### Developers and protocol:
+* The `creationtime` is now communicated of users. Until now this
+  information was only known locally (the thing that was communicated
+  that came close was "last nick change" but that is not the same).
+  This is synced via (early) moddata across servers.
+  Module coders can use `get_connected_time()`.
+* The `RPL_HOSTHIDDEN` is now sent from `userhost_changed()` so you
+  don't explicitly send it yourself anymore.
+* The `SVSO` command is back, so services can make people IRCOp again.
+  See `HELPOP SVSO` or [the commit](https://github.com/unrealircd/unrealircd/commit/50e5d91c798e7d07ca0c68d9fca302a6b6610786)
+  for more information.
+* Due to last change the `HOOKTYPE_LOCAL_OPER` parameters were changed.
+* Module coders can enhance the
+  [JSON logging](https://www.unrealircd.org/docs/JSON_logging)
+  expansion items for clients and channels via new hooks like
+  `HOOKTYPE_JSON_EXPAND_CLIENT`. This is used by the geoip and tls modules.
+
+UnrealIRCd 6.0.3
+-----------------
+A number of serious issues were discovered in UnrealIRCd 6. Among these is
+an issue which will likely crash the IRCd sooner or later if you /REHASH
+with any active clients connected.
+We suggest everyone who is running UnrealIRCd 6 to upgrade to 6.0.3.
+
 Fixes:
 * Crash in `WATCH` if the IRCd has been rehashed at least once. After doing
   a `REHASH` with active clients it will likely corrupt memory. It may take
diff --git a/doc/conf/modules.conf b/doc/conf/modules.conf
@@ -74,11 +74,13 @@ loadmodule "setident";
 loadmodule "squit";
 loadmodule "stats";
 loadmodule "tkl";
+loadmodule "tline";
 loadmodule "trace";
 loadmodule "tsctl";
 loadmodule "unsqline";
 
 // Server-2-Server Commands
+loadmodule "creationtime";
 loadmodule "eos";
 loadmodule "md";
 loadmodule "netinfo";
@@ -97,6 +99,7 @@ loadmodule "sendsno";
 loadmodule "sendumode";
 loadmodule "svsjoin";
 loadmodule "svskill";
+loadmodule "svslogin";
 loadmodule "svslusers";
 loadmodule "svsmode";
 loadmodule "svsmotd";
@@ -172,12 +175,13 @@ loadmodule "extbans/quiet";         /* +b ~quiet          */
 loadmodule "extbans/textban";       /* +b ~text           */
 loadmodule "extbans/timedban";      /* +b ~time           */
 loadmodule "extbans/securitygroup"; /* +b ~security-group */
-
+e
 // IRCv3 Extensions
 loadmodule "account-notify";
 loadmodule "account-tag";
 loadmodule "batch";
 loadmodule "bot-tag";
+loadmodule "channel-context";
 loadmodule "chathistory";
 loadmodule "clienttagdeny";
 loadmodule "echo-message";
@@ -200,6 +204,7 @@ loadmodule "blacklist";
 loadmodule "certfp";
 loadmodule "channeldb";
 loadmodule "charsys";
+loadmodule "connect-flood";
 loadmodule "connthrottle";
 #loadmodule "geoip_base";
 #loadmodule "geoip_classic";
@@ -209,6 +214,7 @@ loadmodule "history_backend_mem";
 loadmodule "ident_lookup";
 loadmodule "jointhrottle";
 loadmodule "json-log-tag";
+loadmodule "max-unknown-connections-per-ip";
 loadmodule "targetfloodprot";
 loadmodule "tkldb";
 loadmodule "tls_antidos";
@@ -220,4 +226,5 @@ loadmodule "restrict-commands";
 loadmodule "rmtkl";
 loadmodule "watch-backend";
 #loadmodule "webirc";
-#loadmodule "websocket";
-\ No newline at end of file
+#loadmodule "webserver";
+#loadmodule "websocket";
diff --git a/doc/conf/opers.conf b/doc/conf/opers.conf
@@ -1,4 +1,5 @@
 oper acidvegas {
+	auto-login yes;
 	mask localhost;
 	password "REDACTED" { sslclientcertfp; }
 	class clients;
diff --git a/doc/conf/unrealircd.remote.conf b/doc/conf/unrealircd.remote.conf
@@ -1,4 +1,12 @@
-admin { ""; }
+@define $VOID "8,4   E N T E R   T H E   V O I D   ";
+
+admin {
+	"4Administrator: Brandon Brown      14(aka MRCHATS)  6branbran89@supernets.org";
+	"    4Moderator: Bristopher Manning 14(aka delorean) 6simpsonsfan95@supernets.org";
+	"        4Sales: Branthony Bronson  14(aka pyrex)    6showercaphandgun@supernets.org";
+	"";
+	"Feel free to chat with us in #5000 for network help & support!";
+}
 
 alias botserv { type services; }
 alias bs { target botserv; type services; }
@@ -60,7 +68,7 @@ blacklist dronebl {
 	}
 	action gzline;
 	ban-time 30d;
-	reason "8,4   E N T E R   T H E   V O I D   ";
+	reason "$VOID";
 }
 
 blacklist efnetrbl {
@@ -71,7 +79,7 @@ blacklist efnetrbl {
 	}
 	action gzline;
 	ban-time 30d;
-	reason "8,4   E N T E R   T H E   V O I D   ";
+	reason "$VOID";
 }
 
 blacklist torbl {
@@ -82,7 +90,7 @@ blacklist torbl {
 	}
 	action gzline;
 	ban-time 30d;
-	reason "8,4   E N T E R   T H E   V O I D   ";
+	reason "$VOID";
 }
 
 set {
@@ -96,13 +104,13 @@ set {
 	restrict-usermodes "ips";
 	restrict-channelmodes "nLpPs";
 	restrict-commands {
-		channel-message { connect-delay 60;  exempt-identified yes; exempt-reputation-score 100; }
-		channel-notice  { connect-delay 60;  exempt-identified yes; exempt-reputation-score 100; }
-		invite          { connect-delay 300; exempt-identified yes; exempt-reputation-score 100; }
-		join            { connect-delay 15;  exempt-identified yes; exempt-reputation-score 100; }
-		list            { connect-delay 30;  exempt-identified yes; exempt-reputation-score 100; }
-		private-message { connect-delay 300; exempt-identified yes; exempt-reputation-score 100; }
-		private-notice  { connect-delay 300; exempt-identified yes; exempt-reputation-score 100; }
+		channel-message { connect-time 60;  identified yes; reputation-score 100; }
+		channel-notice  { connect-time 60;  identified yes; reputation-score 100; }
+		invite          { connect-time 300; identified yes; reputation-score 100; }
+		join            { connect-time 15;  identified yes; reputation-score 100; }
+		list            { connect-time 30;  identified yes; reputation-score 100; }
+		private-message { connect-time 300; identified yes; reputation-score 100; }
+		private-notice  { connect-time 300; identified yes; reputation-score 100; }
 	}
 	auto-join "#superbowl";
 	static-quit "EMO-QUIT";
@@ -145,14 +153,14 @@ set {
 				ban-action gzline;
 				ban-time 1h;
 			}
-			#target-flood {
-			#	channel-notice  15:5;
-			#	channel-privmsg 45:5;
-			#	channel-tagmsg  15:5;
-			#	private-notice  10:5;
-			#	private-privmsg 30:5;
-			#	private-tagmsg  10:5;
-			#}
+			target-flood {
+				channel-notice  15:5;
+				channel-privmsg 45:5;
+				channel-tagmsg  15:5;
+				private-notice  10:5;
+				private-privmsg 30:5;
+				private-tagmsg  10:5;
+			}
 		}
 		known-users {
 			away-flood   3:300;
@@ -185,29 +193,29 @@ set {
 	modef-default-unsettime 5;
 	spamfilter {
 		ban-time 1d;
-		ban-reason "8,4   E N T E R   T H E   V O I D   ";
+		ban-reason "$VOID";
 		except "#anythinggoes";
 	}
 	max-targets-per-command { kick 1; part 1; privmsg 1; }
 	hide-ban-reason yes;
 	reject-message {
-		gline                "8,4   E N T E R   T H E   V O I D   ";
-		kline                "8,4   E N T E R   T H E   V O I D   ";
-		password-mismatch    "8,4   E N T E R   T H E   V O I D   ";
-		server-full          "8,4   E N T E R   T H E   V O I D   ";
-		too-many-connections "8,4   E N T E R   T H E   V O I D   ";
-		unauthorized         "8,4   E N T E R   T H E   V O I D   ";
+		gline                "$VOID";
+		kline                "$VOID";
+		password-mismatch    "$VOID";
+		server-full          "$VOID";
+		too-many-connections "$VOID";
+		unauthorized         "$VOID";
 	}
 	antimixedutf8 {
 		score 8;
 		ban-action block;
-		ban-reason "8,4   E N T E R   T H E   V O I D   ";
+		ban-reason "$VOID";
 	}
 	connthrottle {
-		known-users   { minimum-reputation-score 100; sasl-bypass yes;       }
-		new-users     { local-throttle 20:60;         global-throttle 30:60; }
-		disabled-when { reputation-gathering 1w;      start-delay 3m;        }
-		reason "8,4   E N T E R   T H E   V O I D   ";
+		except        { reputation-score 100;    identified yes; webirc yes; }
+		new-users     { local-throttle 20:60;    global-throttle 30:60;      }
+		disabled-when { reputation-gathering 1w; start-delay 3m;             }
+		reason "$VOID";
 	}
 	history {
 		channel {
@@ -220,32 +228,32 @@ set {
 	}
 	hide-idle-time { policy always; }
 	whois-details {
-		account         { everyone full; }
-		away            { everyone full; }
-		basic           { everyone full; }
-		bot             { everyone full; }
-		certfp          { everyone full; }
-		channels        { everyone none; self full; oper full; }
-		geo             { everyone none; }
-		idle            { everyone none; }
-		modes           { everyone none; self full; oper full; }
+		account         { everyone full;                          }
+		away            { everyone full;                          }
+		basic           { everyone full;                          }
+		bot             { everyone full;                          }
+		certfp          { everyone full;                          }
+		channels        { everyone none;    self full; oper full; }
+		geo             { everyone none;                          }
+		idle            { everyone none;                          }
+		modes           { everyone none;    self full; oper full; }
 		oper            { everyone limited; self full; oper full; }
-		realhost        { everyone none; self full; oper full; }
-		registered-nick { everyone full; }
-		reputation      { everyone full; }
+		realhost        { everyone none;    self full; oper full; }
+		registered-nick { everyone full;                          }
+		reputation      { everyone full;                          }
 		secure          { everyone limited; self full; oper full; }
-		server          { everyone full; }
-		services        { everyone full; }
-		shunned         { everyone none; self none; oper full; }
-		swhois          { everyone full; }
+		server          { everyone full;                          }
+		services        { everyone full;                          }
+		shunned         { everyone none;    self none; oper full; }
+		swhois          { everyone full;                          }
 	}
 }
 
 hideserver {
 	disable-map yes;
 	disable-links yes;
-	map-deny-message "8,4   E N T E R   T H E   V O I D   ";
-	links-deny-message "8,4   E N T E R   T H E   V O I D   ";
+	map-deny-message "$VOID";
+	links-deny-message "$VOID";
 }
 
 security-group known-users {
diff --git a/extras/doxygen/Doxyfile b/extras/doxygen/Doxyfile
@@ -38,7 +38,7 @@ PROJECT_NAME           = "UnrealIRCd"
 # could be handy for archiving the generated documentation or if some version
 # control system is used.
 
-PROJECT_NUMBER         = 6.0.3
+PROJECT_NUMBER         = 6.0.4.2
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer a
diff --git a/extras/unrealircd-upgrade-script.in b/extras/unrealircd-upgrade-script.in
@@ -119,5 +119,5 @@ fi
 chmod +x unrealircd-upgrade-script.stage2
 ./unrealircd-upgrade-script.stage2 $*
 SAVERET="$?"
-rm -f unrealircd-upgrade-script.stage2 unrealircd-upgrade-script.stage2
+rm -f unrealircd-upgrade-script.stage2 unrealircd-upgrade-script.stage2.asc
 exit $SAVERET
diff --git a/include/config.h b/include/config.h
@@ -123,7 +123,7 @@
  * Common usage for this are: a trusted bot ran by an IRCOp, that you only
  * want to give "flood access" and nothing else, and other such things.
  */
-#define FAKELAG_CONFIGURABLE
+//#undef FAKELAG_CONFIGURABLE
 
 /* The default value for class::sendq */
 #define DEFAULT_SENDQ	3000000
diff --git a/include/h.h b/include/h.h
@@ -111,7 +111,6 @@ extern MODVAR ConfigItem_deny_version	*conf_deny_version;
 extern MODVAR ConfigItem_alias		*conf_alias;
 extern MODVAR ConfigItem_help		*conf_help;
 extern MODVAR ConfigItem_offchans	*conf_offchans;
-extern MODVAR SecurityGroup		*securitygroups;
 extern void		completed_connection(int, int, void *);
 extern void clear_unknown();
 extern EVENT(e_unload_module_delayed);
@@ -677,6 +676,7 @@ extern void unreal_setfilemodtime(const char *filename, time_t mtime);
 extern void DeleteTempModules(void);
 extern MODVAR Extban *extbaninfo;
 extern Extban *findmod_by_bantype(const char *str, const char **remainder);
+extern Extban *findmod_by_bantype_raw(const char *str, int ban_name_length);
 extern Extban *ExtbanAdd(Module *reserved, ExtbanInfo req);
 extern void ExtbanDel(Extban *);
 extern void extban_init(void);
@@ -749,8 +749,9 @@ extern MODVAR const char *(*tkl_type_string)(TKL *tk);
 extern MODVAR const char *(*tkl_type_config_string)(TKL *tk);
 extern MODVAR TKL *(*tkl_add_serverban)(int type, const char *usermask, const char *hostmask, const char *reason, const char *setby,
                                             time_t expire_at, time_t set_at, int soft, int flags);
-extern MODVAR TKL *(*tkl_add_banexception)(int type, const char *usermask, const char *hostmask, const char *reason, const char *set_by,
-                                               time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags);
+extern MODVAR TKL *(*tkl_add_banexception)(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
+                                           const char *reason, const char *set_by,
+                                           time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags);
 extern MODVAR TKL *(*tkl_add_nameban)(int type, const char *name, int hold, const char *reason, const char *setby,
                                           time_t expire_at, time_t set_at, int flags);
 extern MODVAR TKL *(*tkl_add_spamfilter)(int type, unsigned short target, unsigned short action, Match *match, const char *setby,
@@ -779,10 +780,9 @@ extern MODVAR int (*match_spamfilter)(Client *client, const char *str_in, int ty
 extern MODVAR int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd);
 extern MODVAR int (*join_viruschan)(Client *client, TKL *tk, int type);
 extern MODVAR const char *(*StripColors)(const char *text);
-extern MODVAR const char *(*StripControlCodes)(const char *text);
 extern MODVAR void (*spamfilter_build_user_string)(char *buf, const char *nick, Client *acptr);
 extern MODVAR void (*send_protoctl_servers)(Client *client, int response);
-extern MODVAR int (*verify_link)(Client *client, ConfigItem_link **link_out);
+extern MODVAR ConfigItem_link *(*verify_link)(Client *client);
 extern MODVAR void (*send_server_message)(Client *client);
 extern MODVAR void (*broadcast_md_client)(ModDataInfo *mdi, Client *acptr, ModData *md);
 extern MODVAR void (*broadcast_md_channel)(ModDataInfo *mdi, Channel *channel, ModData *md);
@@ -834,6 +834,8 @@ extern MODVAR char *(*tkl_uhost)(TKL *tkl, char *buf, size_t buflen, int options
 extern MODVAR void (*do_unreal_log_remote_deliver)(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized);
 extern MODVAR char *(*get_chmodes_for_user)(Client *client, const char *flags);
 extern MODVAR WhoisConfigDetails (*whois_get_policy)(Client *client, Client *target, const char *name);
+extern MODVAR int (*make_oper)(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost);
+extern MODVAR int (*unreal_match_iplist)(Client *client, NameList *l);
 /* /Efuncs */
 
 /* TLS functions */
@@ -859,6 +861,7 @@ extern MODVAR EVP_MD *sha1_function;
 extern MODVAR EVP_MD *md5_function;
 /* End of TLS functions */
 
+/* Default handlers for efunctions */
 extern void parse_message_tags_default_handler(Client *client, char **str, MessageTag **mtag_list);
 extern const char *mtags_to_string_default_handler(MessageTag *m, Client *client);
 extern void *labeled_response_save_context_default_handler(void);
@@ -868,6 +871,8 @@ extern int add_silence_default_handler(Client *client, const char *mask, int sen
 extern int del_silence_default_handler(Client *client, const char *mask);
 extern int is_silenced_default_handler(Client *client, Client *acptr);
 extern void do_unreal_log_remote_deliver_default_handler(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized);
+extern int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost);
+/* End of default handlers for efunctions */
 
 extern MODVAR MOTDFile opermotd, svsmotd, motd, botmotd, smotd, rules;
 extern MODVAR int max_connection_count;
@@ -925,10 +930,6 @@ extern void unreal_delete_match(Match *m);
 extern int unreal_match(Match *m, const char *str);
 extern int unreal_match_method_strtoval(const char *str);
 extern char *unreal_match_method_valtostr(int val);
-extern void unreal_delete_masks(ConfigItem_mask *m);
-extern void unreal_add_masks(ConfigItem_mask **head, ConfigEntry *ce);
-extern int unreal_mask_match(Client *acptr, ConfigItem_mask *m);
-extern int unreal_mask_match_string(const char *name, ConfigItem_mask *m);
 #ifdef _WIN32
 extern MODVAR BOOL IsService;
 #endif
@@ -946,7 +947,7 @@ extern void modulemanager(int argc, char *argv[]);
 extern int inet_pton4(const char *src, unsigned char *dst);
 extern int inet_pton6(const char *src, unsigned char *dst);
 extern int unreal_bind(int fd, const char *ip, int port, SocketType socket_type);
-extern int unreal_connect(int fd, const char *ip, int port, int ipv6);
+extern int unreal_connect(int fd, const char *ip, int port, SocketType socket_type);
 extern int is_valid_ip(const char *str);
 extern int ipv6_capable(void);
 extern int unix_sockets_capable(void);
@@ -1092,14 +1093,6 @@ extern int hide_idle_time(Client *client, Client *target);
 extern void lost_server_link(Client *serv, const char *tls_error_string);
 extern const char *sendtype_to_cmd(SendType sendtype);
 extern MODVAR MessageTagHandler *mtaghandlers;
-extern int security_group_valid_name(const char *name);
-extern int security_group_exists(const char *name);
-extern SecurityGroup *add_security_group(const char *name, int order);
-extern SecurityGroup *find_security_group(const char *name);
-extern void free_security_group(SecurityGroup *s);
-extern void set_security_group_defaults(void);
-extern int user_allowed_by_security_group(Client *client, SecurityGroup *s);
-extern int user_allowed_by_security_group_name(Client *client, const char *secgroupname);
 #define nv_find_by_name(stru, name)       do_nv_find_by_name(stru, name, ARRAY_SIZEOF((stru)))
 extern long do_nv_find_by_name(NameValue *table, const char *cmd, int numelements);
 #define nv_find_by_value(stru, value)       do_nv_find_by_value(stru, value, ARRAY_SIZEOF((stru)))
@@ -1119,6 +1112,9 @@ extern void add_fmt_nvplist(NameValuePrioList **lst, int priority, const char *n
 extern void add_nvplist_numeric_fmt(NameValuePrioList **lst, int priority, const char *name, Client *to, int numeric, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf,6,7)));
 extern NameValuePrioList *find_nvplist(NameValuePrioList *list, const char *name);
 extern void free_nvplist(NameValuePrioList *lst);
+extern void unreal_add_name_values(NameValuePrioList **n, const char *name, ConfigEntry *ce);
+extern const char *namevalue(NameValuePrioList *n);
+extern const char *namevalue_nospaces(NameValuePrioList *n);
 extern const char *get_connect_extinfo(Client *client);
 extern char *unreal_strftime(const char *str);
 extern void strtolower(char *str);
@@ -1132,6 +1128,30 @@ extern void read_until(char **p, char *stopchars);
 extern int is_ip_valid(const char *ip);
 extern int is_file_readable(const char *file, const char *dir);
 json_t *json_string_unreal(const char *s);
+/* securitygroup.c start */
+extern MODVAR SecurityGroup *securitygroups;
+extern void unreal_delete_masks(ConfigItem_mask *m);
+extern void unreal_add_masks(ConfigItem_mask **head, ConfigEntry *ce);
+extern int unreal_mask_match(Client *acptr, ConfigItem_mask *m);
+extern int unreal_mask_match_string(const char *name, ConfigItem_mask *m);
+extern int test_match_item(ConfigFile *conf, ConfigEntry *cep, int *errors);
+extern int test_match_block(ConfigFile *conf, ConfigEntry *ce, int *errors_out);
+extern int test_match_block_too_broad(ConfigFile *conf, ConfigEntry *ce);
+extern int security_group_valid_name(const char *name);
+extern int security_group_exists(const char *name);
+extern SecurityGroup *add_security_group(const char *name, int order);
+extern SecurityGroup *find_security_group(const char *name);
+extern void free_security_group(SecurityGroup *s);
+extern void set_security_group_defaults(void);
+extern int user_allowed_by_security_group(Client *client, SecurityGroup *s);
+extern int user_allowed_by_security_group_name(Client *client, const char *secgroupname);
+extern const char *get_security_groups(Client *client);
+extern int test_match_item(ConfigFile *conf, ConfigEntry *cep, int *errors);
+extern int conf_match_item(ConfigFile *conf, ConfigEntry *cep, SecurityGroup **block);
+extern int test_match_block(ConfigFile *conf, ConfigEntry *ce, int *errors_out);
+extern int conf_match_block(ConfigFile *conf, ConfigEntry *ce, SecurityGroup **block);
+extern int test_extended_list(Extban *extban, ConfigEntry *cep, int *errors);
+/* securitygroup.c end */
 /* src/unrealdb.c start */
 extern UnrealDB *unrealdb_open(const char *filename, UnrealDBMode mode, char *secret_block);
 extern int unrealdb_close(UnrealDB *c);
@@ -1249,3 +1269,6 @@ extern void add_proc_io_server(void);
 extern void procio_post_rehash(int failure);
 /* end of proc i/o */
 extern int minimum_msec_since_last_run(struct timeval *tv_old, long minimum);
+extern long get_connected_time(Client *client);
+extern const char *StripControlCodes(const char *text);
+extern const char *StripControlCodesEx(const char *text, char *output, size_t outputlen, int strip_flags);
diff --git a/include/license.h b/include/license.h
@@ -27,10 +27,10 @@
 
 char *gnulicense[] = {
 	"   \2UnrealIRCd License\2",
-	"This program is free software; you can redistribute it and/or modify",
-	"it under the terms of the GNU General Public License as published by",
-	"the Free Software Foundation; either version 1, or (at your option)",
-	"any later version.",
+	"This program is free software; you can redistribute it and/or",
+	"modify it under the terms of the GNU General Public License",
+	"as published by the Free Software Foundation; either version 2",
+	"of the License, or (at your option) any later version.",
 	"",
 	"This program is distributed in the hope that it will be useful,",
 	"but WITHOUT ANY WARRANTY; without even the implied warranty of",
@@ -39,10 +39,11 @@ char *gnulicense[] = {
 	"",
 	"You should have received a copy of the GNU General Public License",
 	"along with this program; if not, write to the Free Software",
-	"Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.",
+	"Foundation, Inc., 51 Franklin Street, Fifth Floor,",
+	"Boston, MA  02110-1301, USA.",
 	"",
 	"To see the UnrealIRCd License, please point your browser",
-	"at http://www.gnu.org/copyleft/gpl.html or look in the",
-	"file LICENSE in the UnrealIRCd dist",
-	0
+	"to https://www.gnu.org/licenses/old-licenses/gpl-2.0.html",
+	"or look at the LICENSE file in the UnrealIRCd distribution.",
+	NULL
 };
diff --git a/include/modules.h b/include/modules.h
@@ -450,8 +450,11 @@ struct Extban {
 	/** extbans module */
 	Module *owner;
 
-	/* Set to 1 during rehash when module is unloading (which may be re-used, and then set to 0) */
+	/** Set to 1 during rehash when module is unloading (which may be re-used, and then set to 0) */
 	char unloaded;
+
+	/** Set to 1 when it is preregistered in MOD_TEST already */
+	char preregistered;
 };
 
 typedef struct {
@@ -1097,8 +1100,8 @@ extern void SavePersistentLongX(ModuleInfo *modinfo, const char *varshortname, l
 #define HOOKTYPE_SEE_CHANNEL_IN_WHOIS	77
 /** See hooktype_join_data() */
 #define HOOKTYPE_JOIN_DATA	78
-/** See hooktype_oper_invite_ban() */
-#define HOOKTYPE_OPER_INVITE_BAN	79
+/** See hooktype_invite_bypass() */
+#define HOOKTYPE_INVITE_BYPASS	79
 /** See hooktype_view_topic_outside_channel() */
 #define HOOKTYPE_VIEW_TOPIC_OUTSIDE_CHANNEL	80
 /** See hooktype_chan_permit_nick_change() */
@@ -1159,6 +1162,15 @@ extern void SavePersistentLongX(ModuleInfo *modinfo, const char *varshortname, l
 #define HOOKTYPE_CAN_SET_TOPIC	110
 /** See hooktype_ip_change() */
 #define HOOKTYPE_IP_CHANGE	111
+/** See hooktype_json_expand_client() */
+#define HOOKTYPE_JSON_EXPAND_CLIENT	112
+/** See hooktype_json_expand_client() */
+#define HOOKTYPE_JSON_EXPAND_CLIENT_USER	113
+/** See hooktype_json_expand_client() */
+#define HOOKTYPE_JSON_EXPAND_CLIENT_SERVER	114
+/** See hooktype_json_expand_channel() */
+#define HOOKTYPE_JSON_EXPAND_CHANNEL	115
+
 /* Adding a new hook here?
  * 1) Add the #define HOOKTYPE_.... with a new number
  * 2) Add a hook prototype (see below)
@@ -1683,9 +1695,10 @@ int hooktype_stats(Client *client, const char *str);
  * @param client		The client
  * @param add			1 if the user becomes IRCOp, 0 if the user is no longer IRCOp
  * @param oper_block		The name of the oper block used to oper up
+ * @param operclass		The name of the operclass
  * @return The return value is ignored (use return 0)
  */
-int hooktype_local_oper(Client *client, int add, ConfigItem_oper *oper_block);
+int hooktype_local_oper(Client *client, int add, const char *oper_block, const char *operclass);
 
 /** Called when a client sends a PASS command (function prototype for HOOKTYPE_LOCAL_PASS).
  * @param client		The client
@@ -1858,14 +1871,16 @@ int hooktype_see_channel_in_whois(Client *client, Client *target, Channel *chann
  */
 int hooktype_join_data(Client *who, Channel *channel);
 
-/** Should the user be able to bypass bans? (function prototype for HOOKTYPE_OPER_INVITE_BAN).
+/** Should the user be able to bypass channel restrictions because they are invited? (function prototype for HOOKTYPE_INVITE_BYPASS).
  * @param client		The client
  * @param channel		The channel
- * @note The actual meaning of this hook is more complex, you are unlikely to use it, anyway.
- * @retval HOOK_DENY		Deny the join if the user is also banned
+ * @retval HOOK_DENY		Don't allow the user to bypass channel restrictions when they are invited
  * @retval HOOK_CONTINUE	Obey the normal rules
+ * @note Usually you want a user to be able to bypass channel restrictions such as +l or +b when they are /INVITEd by another user
+ *       or have invited themselves (OperOverride). But, there may be special cases where you don't want this.
+ *       For example, this hook is used by +O to still not allow ircops to join +O channels even if they have OperOverride capability.
  */
-int hooktype_oper_invite_ban(Client *client, Channel *channel);
+int hooktype_invite_bypass(Client *client, Channel *channel);
 
 /** Should a user be able to view the topic when not in the channel? (function prototype for HOOKTYPE_VIEW_TOPIC_OUTSIDE_CHANNEL).
  * @param client		The client requesting the topic
@@ -2145,6 +2160,46 @@ int hooktype_realname_change(Client *client, const char *oldinfo);
  */
 int hooktype_ip_change(Client *client, const char *oldip);
 
+/** Called when json_expand_client() is called.
+ * Used for expanding information about 'client' in logging routines.
+ * @param client		The client that should be expanded
+ * @param detail		The amount of detail to provide (always 0 at the moment)
+ * @param j			The JSON object
+ * @return The return value is ignored (use return 0)
+ */
+int hooktype_json_expand_client(Client *client, int detail, json_t *j);
+
+/** Called when json_expand_client_user() is called.
+ * Used for expanding information about 'client' in logging routines
+ * when the client is a USER.
+ * @param client		The client that should be expanded
+ * @param detail		The amount of detail to provide (always 0 at the moment)
+ * @param j			The JSON object - root
+ * @param child			The JSON object - "user" child item
+ * @return The return value is ignored (use return 0)
+ */
+int hooktype_json_expand_client_user(Client *client, int detail, json_t *j, json_t *child);
+
+/** Called when json_expand_client_server() is called.
+ * Used for expanding information about 'client' in logging routines
+ * when the client is a SERVER.
+ * @param client		The client that should be expanded
+ * @param detail		The amount of detail to provide (always 0 at the moment)
+ * @param j			The JSON object - root
+ * @param child			The JSON object - "server" child item
+ * @return The return value is ignored (use return 0)
+ */
+int hooktype_json_expand_client_server(Client *client, int detail, json_t *j, json_t *child);
+
+/** Called when json_expand_channel() is called.
+ * Used for expanding information about 'channel' in logging routines.
+ * @param channel		The channel that should be expanded
+ * @param detail		The amount of detail to provide (always 0 at the moment)
+ * @param j			The JSON object
+ * @return The return value is ignored (use return 0)
+ */
+int hooktype_json_expand_channel(Channel *channel, int detail, json_t *j);
+
 /** @} */
 
 #ifdef GCC_TYPECHECKING
@@ -2223,7 +2278,7 @@ _UNREAL_ERROR(_hook_error_incompatible, "Incompatible hook function. Check argum
         ((hooktype == HOOKTYPE_JOIN_DATA) && !ValidateHook(hooktype_join_data, func)) || \
         ((hooktype == HOOKTYPE_PRE_KNOCK) && !ValidateHook(hooktype_pre_knock, func)) || \
         ((hooktype == HOOKTYPE_PRE_INVITE) && !ValidateHook(hooktype_pre_invite, func)) || \
-        ((hooktype == HOOKTYPE_OPER_INVITE_BAN) && !ValidateHook(hooktype_oper_invite_ban, func)) || \
+        ((hooktype == HOOKTYPE_INVITE_BYPASS) && !ValidateHook(hooktype_invite_bypass, func)) || \
         ((hooktype == HOOKTYPE_VIEW_TOPIC_OUTSIDE_CHANNEL) && !ValidateHook(hooktype_view_topic_outside_channel, func)) || \
         ((hooktype == HOOKTYPE_CHAN_PERMIT_NICK_CHANGE) && !ValidateHook(hooktype_chan_permit_nick_change, func)) || \
         ((hooktype == HOOKTYPE_IS_CHANNEL_SECURE) && !ValidateHook(hooktype_is_channel_secure, func)) || \
@@ -2259,7 +2314,11 @@ _UNREAL_ERROR(_hook_error_incompatible, "Incompatible hook function. Check argum
         ((hooktype == HOOKTYPE_POST_REMOTE_NICKCHANGE) && !ValidateHook(hooktype_post_remote_nickchange, func)) || \
         ((hooktype == HOOKTYPE_USERHOST_CHANGE) && !ValidateHook(hooktype_userhost_change, func)) || \
         ((hooktype == HOOKTYPE_REALNAME_CHANGE) && !ValidateHook(hooktype_realname_change, func)) || \
-        ((hooktype == HOOKTYPE_IP_CHANGE) && !ValidateHook(hooktype_ip_change, func)) ) \
+        ((hooktype == HOOKTYPE_IP_CHANGE) && !ValidateHook(hooktype_ip_change, func)) || \
+        ((hooktype == HOOKTYPE_JSON_EXPAND_CLIENT) && !ValidateHook(hooktype_json_expand_client, func)) || \
+        ((hooktype == HOOKTYPE_JSON_EXPAND_CLIENT_USER) && !ValidateHook(hooktype_json_expand_client_user, func)) || \
+        ((hooktype == HOOKTYPE_JSON_EXPAND_CLIENT_SERVER) && !ValidateHook(hooktype_json_expand_client_server, func)) || \
+        ((hooktype == HOOKTYPE_JSON_EXPAND_CHANNEL) && !ValidateHook(hooktype_json_expand_channel, func)) ) \
         _hook_error_incompatible();
 #endif /* GCC_TYPECHECKING */
 
@@ -2315,7 +2374,6 @@ enum EfunctionType {
 	EFUNC_FIND_TKLINE_MATCH_ZAP_EX,
 	EFUNC_SEND_LIST,
 	EFUNC_STRIPCOLORS,
-	EFUNC_STRIPCONTROLCODES,
 	EFUNC_SPAMFILTER_BUILD_USER_STRING,
 	EFUNC_SEND_PROTOCTL_SERVERS,
 	EFUNC_VERIFY_LINK,
@@ -2384,6 +2442,8 @@ enum EfunctionType {
 	EFUNC_DO_UNREAL_LOG_REMOTE_DELIVER,
 	EFUNC_GET_CHMODES_FOR_USER,
 	EFUNC_WHOIS_GET_POLICY,
+	EFUNC_MAKE_OPER,
+	EFUNC_UNREAL_MATCH_IPLIST,
 };
 
 /* Module flags */
diff --git a/include/struct.h b/include/struct.h
@@ -559,6 +559,7 @@ typedef enum ClientStatus {
 #define IsIPV6(x)			((x)->local->socket_type == SOCKET_TYPE_IPV6)
 #define IsUnixSocket(x)			((x)->local->socket_type == SOCKET_TYPE_UNIX)
 #define SetIPV6(x)			do { (x)->local->socket_type = SOCKET_TYPE_IPV6; } while(0)
+#define SetUnixSocket(x)			do { (x)->local->socket_type = SOCKET_TYPE_UNIX; } while(0)
 /** @} */
 
 
@@ -608,6 +609,7 @@ union ModData
 {
         int i;
         long l;
+        long long ll;
         char *str;
         void *ptr;
 };
@@ -762,6 +764,8 @@ struct NameList {
 /** Delete an entry from a NameList - AND free it */
 #define del_name_list(list, str)  _del_name_list(&list, str)
 
+extern void unreal_add_names(NameList **n, ConfigEntry *ce);
+
 /** @} */
 
 typedef struct MultiLine MultiLine;
@@ -1102,6 +1106,7 @@ struct Spamfilter {
 struct BanException {
 	char *usermask; /**< User mask */
 	char *hostmask; /**< Host mask */
+	SecurityGroup *match; /**< Security group (for config file items only) */
 	unsigned short subtype; /**< See TKL_SUBTYPE_* */
 	char *bantypes; /**< Exception types */
 	char *reason; /**< Reason */
@@ -1557,7 +1562,7 @@ struct ConfigFlag_allow {
 struct ConfigItem_allow {
 	ConfigItem_allow *prev, *next;
 	ConfigFlag flag;
-	ConfigItem_mask *mask;
+	SecurityGroup *match;
 	char *server;
 	AuthConfig *auth;
 	int maxperip; /**< Maximum connections permitted per IP address (locally) */
@@ -1624,12 +1629,13 @@ struct ConfigItem_oper {
 	AuthConfig *auth;
 	char *operclass;
 	ConfigItem_class *class;
-	ConfigItem_mask *mask;
+	SecurityGroup *match;
 	unsigned long modes, require_modes;
 	char *vhost;
 	int maxlogins;
 	int server_notice_colors;
 	int server_notice_show_event;
+	int auto_login;
 };
 
 /** The TLS options that are used in set::tls and otherblocks::tls-options.
@@ -1679,7 +1685,7 @@ struct ConfigItem_ulines {
 struct ConfigItem_tld {
 	ConfigItem_tld 	*prev, *next;
 	ConfigFlag_tld 	flag;
-	ConfigItem_mask *mask;
+	SecurityGroup	*match;
 	char 		*channel;
 	char 		*motd_file, *rules_file, *smotd_file;
 	char 		*botmotd_file, *opermotd_file;
@@ -1713,7 +1719,7 @@ struct ConfigItem_sni {
 struct ConfigItem_vhost {
 	ConfigItem_vhost 	*prev, *next;
 	ConfigFlag 	flag;
-	ConfigItem_mask *mask;
+	SecurityGroup	*match;
 	char		*login, *virthost, *virtuser;
 	SWhois *swhois;
 	AuthConfig	*auth;
@@ -1725,9 +1731,10 @@ struct ConfigItem_link {
 	/* config options: */
 	char *servername; /**< Name of the server ('link <servername> { }') */
 	struct {
-		ConfigItem_mask *mask; /**< incoming mask(s) to accept */
+		SecurityGroup *match; /**< incoming mask(s) to accept */
 	} incoming;
 	struct {
+		char *file; /**< UNIX domain socket to connect to */
 		char *bind_ip; /**< Our IP to bind to when doing the connect */
 		char *hostname; /**< Hostname or IP to connect to */
 		int port; /**< Port to connect to */
@@ -1779,14 +1786,14 @@ struct ConfigItem_deny_channel {
 	ConfigFlag		flag;
 	char			*channel, *reason, *redirect, *class;
 	unsigned char	warn;
-	ConfigItem_mask *mask;
+	SecurityGroup		*match;
 };
 
 struct ConfigItem_allow_channel {
 	ConfigItem_allow_channel		*prev, *next;
 	ConfigFlag		flag;
 	char			*channel, *class;
-	ConfigItem_mask *mask;
+	SecurityGroup		*match;
 };
 
 struct ConfigItem_allow_dcc {
@@ -1859,11 +1866,28 @@ struct SecurityGroup {
 	SecurityGroup *prev, *next;
 	int priority;
 	char name[SECURITYGROUPLEN+1];
+	NameValuePrioList *printable_list;
+	int printable_list_counter;
+	/* Include */
 	int identified;
 	int reputation_score;
+	long connect_time;
 	int webirc;
 	int tls;
-	ConfigItem_mask *include_mask;
+	NameList *ip;
+	ConfigItem_mask *mask;
+	NameList *security_group;
+	NameValuePrioList *extended;
+	/* Exclude */
+	int exclude_identified;
+	int exclude_reputation_score;
+	long exclude_connect_time;
+	int exclude_webirc;
+	int exclude_tls;
+	NameList *exclude_ip;
+	ConfigItem_mask *exclude_mask;
+	NameList *exclude_security_group;
+	NameValuePrioList *exclude_extended;
 };
 
 #define HM_HOST 1
@@ -2242,6 +2266,10 @@ typedef enum WhoisConfigDetails {
 	WHOIS_CONFIG_DETAILS_FULL	= 3,
 } WhoisConfigDetails;
 
+/* Options for StripControlCodesEx() */
+#define UNRL_STRIP_LOW_ASCII    0x1     /**< Strip all ASCII < 32 (control codes) */
+#define UNRL_STRIP_KEEP_LF      0x2     /**< Do not strip LF (line feed, \n) */
+
 #endif /* __struct_include__ */
 
 #include "dynconf.h"
diff --git a/include/windows/setup.h b/include/windows/setup.h
@@ -62,10 +62,10 @@
 #define UNREAL_VERSION_MAJOR 0
 
 /* Minor version number (e.g.: 1 for Unreal3.2.1) */
-#define UNREAL_VERSION_MINOR 3
+#define UNREAL_VERSION_MINOR 4
 
 /* Version suffix such as a beta marker or release candidate marker. (e.g.:
    -rcX for unrealircd-3.2.9-rcX) */
-#define UNREAL_VERSION_SUFFIX ""
+#define UNREAL_VERSION_SUFFIX ".2"
 
 #endif
diff --git a/src/Makefile.in b/src/Makefile.in
@@ -25,7 +25,7 @@ OBJS=ircd_vars.o dns.o auth.o channel.o crule.o dbuf.o \
 	fdlist.o hash.o ircsprintf.o list.o \
 	match.o modules.o parse.o mempool.o operclass.o \
 	conf_preprocessor.o conf.o proc_io_server.o debug.o dispatch.o \
-	misc.o serv.o aliases.o socket.o \
+	securitygroup.o misc.o serv.o aliases.o socket.o \
 	tls.o user.o scache.o send.o support.o \
 	version.o whowas.o random.o api-usermode.o api-channelmode.o \
 	api-moddata.o api-extban.o api-isupport.o api-command.o \
diff --git a/src/api-efunctions.c b/src/api-efunctions.c
@@ -57,8 +57,9 @@ TKL *(*tkl_add_spamfilter)(int type, unsigned short target, unsigned short actio
                                time_t expire_at, time_t set_at,
                                time_t spamf_tkl_duration, const char *spamf_tkl_reason,
                                int flags);
-TKL *(*tkl_add_banexception)(int type, const char *usermask, const char *hostmask, const char *reason, const char *set_by,
-                                time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags);
+TKL *(*tkl_add_banexception)(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
+                             const char *reason, const char *set_by,
+                             time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags);
 TKL *(*tkl_del_line)(TKL *tkl);
 void (*tkl_check_local_remove_shun)(TKL *tmp);
 int (*find_tkline_match)(Client *client, int skip_soft);
@@ -74,10 +75,9 @@ int (*match_spamfilter)(Client *client, const char *str_in, int type, const char
 int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd);
 int (*join_viruschan)(Client *client, TKL *tk, int type);
 const char *(*StripColors)(const char *text);
-const char *(*StripControlCodes)(const char *text);
 void (*spamfilter_build_user_string)(char *buf, const char *nick, Client *client);
 void (*send_protoctl_servers)(Client *client, int response);
-int (*verify_link)(Client *client, ConfigItem_link **link_out);
+ConfigItem_link *(*verify_link)(Client *client);
 void (*introduce_user)(Client *to, Client *client);
 void (*send_server_message)(Client *client);
 void (*broadcast_md_client)(ModDataInfo *mdi, Client *client, ModData *md);
@@ -135,6 +135,8 @@ int (*watch_check)(Client *client, int reply, int (*watch_notify)(Client *client
 void (*do_unreal_log_remote_deliver)(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized);
 char *(*get_chmodes_for_user)(Client *client, const char *flags);
 WhoisConfigDetails (*whois_get_policy)(Client *client, Client *target, const char *name);
+int (*make_oper)(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost);
+int (*unreal_match_iplist)(Client *client, NameList *l);
 
 Efunction *EfunctionAddMain(Module *module, EfunctionType eftype, int (*func)(), void (*vfunc)(), void *(*pvfunc)(), char *(*stringfunc)(), const char *(*conststringfunc)())
 {
@@ -339,7 +341,6 @@ void efunctions_init(void)
 	efunc_init_function(EFUNC_MATCH_SPAMFILTER_MTAGS, match_spamfilter_mtags, NULL);
 	efunc_init_function(EFUNC_JOIN_VIRUSCHAN, join_viruschan, NULL);
 	efunc_init_function(EFUNC_STRIPCOLORS, StripColors, NULL);
-	efunc_init_function(EFUNC_STRIPCONTROLCODES, StripControlCodes, NULL);
 	efunc_init_function(EFUNC_SPAMFILTER_BUILD_USER_STRING, spamfilter_build_user_string, NULL);
 	efunc_init_function(EFUNC_SEND_PROTOCTL_SERVERS, send_protoctl_servers, NULL);
 	efunc_init_function(EFUNC_VERIFY_LINK, verify_link, NULL);
@@ -406,4 +407,6 @@ void efunctions_init(void)
 	efunc_init_function(EFUNC_DO_UNREAL_LOG_REMOTE_DELIVER, do_unreal_log_remote_deliver, do_unreal_log_remote_deliver_default_handler);
 	efunc_init_function(EFUNC_GET_CHMODES_FOR_USER, get_chmodes_for_user, NULL);
 	efunc_init_function(EFUNC_WHOIS_GET_POLICY, whois_get_policy, NULL);
+	efunc_init_function(EFUNC_MAKE_OPER, make_oper, make_oper_default_handler);
+	efunc_init_function(EFUNC_UNREAL_MATCH_IPLIST, unreal_match_iplist, NULL);
 }
diff --git a/src/api-extban.c b/src/api-extban.c
@@ -38,9 +38,27 @@ void set_isupport_extban(void)
 	ISupportSetFmt(NULL, "EXTBAN", "~,%s", extbanstr);
 }
 
-Extban *findmod_by_bantype(const char *str, const char **remainder)
+Extban *findmod_by_bantype_raw(const char *str, int ban_name_length)
 {
 	Extban *e;
+
+	for (e=extbans; e; e = e->next)
+	{
+		if ((ban_name_length == 1) && (e->letter == str[0]))
+			return e;
+		if (e->name)
+		{
+			int namelen = strlen(e->name);
+			if ((namelen == ban_name_length) && !strncmp(e->name, str, namelen))
+				return e;
+		}
+	}
+
+	 return NULL;
+}
+
+Extban *findmod_by_bantype(const char *str, const char **remainder)
+{
 	int ban_name_length;
 	const char *p = strchr(str, ':');
 
@@ -54,20 +72,7 @@ Extban *findmod_by_bantype(const char *str, const char **remainder)
 		*remainder = p+1;
 
 	ban_name_length = p - str - 1;
-
-	for (e=extbans; e; e = e->next)
-	{
-		if ((ban_name_length == 1) && (e->letter == str[1]))
-			return e;
-		if (e->name)
-		{
-			int namelen = strlen(e->name);
-			if ((namelen == ban_name_length) && !strncmp(e->name, str+1, namelen))
-				return e;
-		}
-	}
-
-	 return NULL;
+	return findmod_by_bantype_raw(str+1, ban_name_length);
 }
 
 /* Check if this is a valid extended ban name */
@@ -121,6 +126,7 @@ static void extban_add_sorted(Extban *n)
 Extban *ExtbanAdd(Module *module, ExtbanInfo req)
 {
 	Extban *e;
+	ModuleObject *banobj;
 	int existing = 0;
 
 	if (!req.name)
@@ -167,14 +173,35 @@ Extban *ExtbanAdd(Module *module, ExtbanInfo req)
 	{
 		if (e->letter == req.letter)
 		{
+			/* Extban already exists in our list, let's see... */
 			if (e->unloaded)
 			{
 				e->unloaded = 0;
 				existing = 1;
 				break;
-			} else {
-				if (module)
-					module->errorcode = MODERR_EXISTS;
+			} else
+			if ((module->flags == MODFLAG_TESTING) && e->preregistered)
+			{
+				/* We are in MOD_INIT (yeah confusing, isn't it?)
+				 * and the extban already exists and it was preregistered.
+				 * Then go ahead with really registering it.
+				 */
+				e->preregistered = 0;
+				existing = 1;
+			} else
+			if (module->flags == MODFLAG_NONE)
+			{
+				/* Better don't touch it, as we may still fail at this stage
+				 * and if we would set .conv_param etc to this and the new module
+				 * gets unloaded because of a config typo then we would be screwed
+				 * (now we are not).
+				 * NOTE: this does mean that if you hot-load an extban module
+				 * then it may only be available for config stuff the 2nd rehash.
+				 */
+				return e;
+			} else
+			{
+				module->errorcode = MODERR_EXISTS;
 				return NULL;
 			}
 		}
@@ -195,14 +222,16 @@ Extban *ExtbanAdd(Module *module, ExtbanInfo req)
 	e->is_banned_events = req.is_banned_events;
 	e->owner = module;
 	e->options = req.options;
-	if (module)
-	{
-		ModuleObject *banobj = safe_alloc(sizeof(ModuleObject));
-		banobj->object.extban = e;
-		banobj->type = MOBJ_EXTBAN;
-		AddListItem(banobj, module->objects);
-		module->errorcode = MODERR_NOERROR;
-	}
+
+	if (module->flags == MODFLAG_NONE)
+		e->preregistered = 1;
+
+	banobj = safe_alloc(sizeof(ModuleObject));
+	banobj->object.extban = e;
+	banobj->type = MOBJ_EXTBAN;
+	AddListItem(banobj, module->objects);
+	module->errorcode = MODERR_NOERROR;
+
 	set_isupport_extban();
 	return e;
 }
@@ -217,10 +246,27 @@ static void unload_extban_commit(Extban *e)
 
 	/* Then unload the extban */
 	DelListItem(e, extbans);
+	safe_free(e->name);
 	safe_free(e);
 	set_isupport_extban();
 }
 
+/** Unload all unused extended bans after a REHASH */
+void unload_all_unused_extbans(void)
+{
+	Extban *e, *e_next;
+
+	for (e=extbans; e; e = e_next)
+	{
+		e_next = e->next;
+		if (e->letter && e->unloaded)
+		{
+			unload_extban_commit(e);
+		}
+	}
+
+}
+
 void ExtbanDel(Extban *e)
 {
 	/* Always free the module object */
diff --git a/src/channel.c b/src/channel.c
@@ -40,9 +40,6 @@ long sajoinmode = 0;
  */
 Channel *channels = NULL;
 
-/* A buffer for rebuilding channel/nick lists with comma's */
-static char buf[BUFSIZE];
-
 static mp_pool_t *channel_pool = NULL;
 
 /** This describes the letters, modes and options for core channel modes.
diff --git a/src/conf.c b/src/conf.c
@@ -62,7 +62,7 @@ static int	_conf_alias		(ConfigFile *conf, ConfigEntry *ce);
 static int	_conf_help		(ConfigFile *conf, ConfigEntry *ce);
 static int	_conf_offchans		(ConfigFile *conf, ConfigEntry *ce);
 static int	_conf_sni		(ConfigFile *conf, ConfigEntry *ce);
-static int	_conf_security_group	(ConfigFile *conf, ConfigEntry *ce);
+extern int	_conf_security_group	(ConfigFile *conf, ConfigEntry *ce);
 static int	_conf_secret		(ConfigFile *conf, ConfigEntry *ce);
 
 /*
@@ -95,7 +95,7 @@ static int	_test_alias		(ConfigFile *conf, ConfigEntry *ce);
 static int	_test_help		(ConfigFile *conf, ConfigEntry *ce);
 static int	_test_offchans		(ConfigFile *conf, ConfigEntry *ce);
 static int	_test_sni		(ConfigFile *conf, ConfigEntry *ce);
-static int	_test_security_group	(ConfigFile *conf, ConfigEntry *ce);
+extern int	_test_security_group	(ConfigFile *conf, ConfigEntry *ce);
 static int	_test_secret		(ConfigFile *conf, ConfigEntry *ce);
 
 /* This MUST be alphabetized */
@@ -185,6 +185,7 @@ ConfigEntry		*config_find_entry(ConfigEntry *ce, const char *name);
 extern void add_entropy_configfile(struct stat *st, const char *buf);
 extern void unload_all_unused_umodes(void);
 extern void unload_all_unused_extcmodes(void);
+extern void unload_all_unused_extbans(void);
 extern void unload_all_unused_caps(void);
 extern void unload_all_unused_history_backends(void);
 int reloadable_perm_module_unloaded(void);
@@ -232,7 +233,6 @@ ConfigResource	*config_resources = NULL;
 ConfigItem_blacklist_module	*conf_blacklist_module = NULL;
 ConfigItem_help		*conf_help = NULL;
 ConfigItem_offchans	*conf_offchans = NULL;
-SecurityGroup		*securitygroups = NULL;
 Secret			*secrets = NULL;
 
 MODVAR Configuration		iConf;
@@ -577,6 +577,11 @@ long config_checkval(const char *orig, unsigned short flags)
 /** Free configuration setting for set::modes-on-join */
 void free_conf_channelmodes(struct ChMode *store)
 {
+	int i;
+
+	for (i=0; i < 255; i++)
+		safe_free(store->extparams[i]);
+
 	memset(store, 0, sizeof(struct ChMode));
 }
 
@@ -2353,7 +2358,7 @@ void config_rehash()
 		safe_free(oper_ptr->operclass);
 		safe_free(oper_ptr->vhost);
 		Auth_FreeAuthConfig(oper_ptr->auth);
-		unreal_delete_masks(oper_ptr->mask);
+		free_security_group(oper_ptr->match);
 		DelListItem(oper_ptr, conf_oper);
 		for (s = oper_ptr->swhois; s; s = s_next)
 		{
@@ -2400,7 +2405,7 @@ void config_rehash()
 	for (allow_ptr = conf_allow; allow_ptr; allow_ptr = (ConfigItem_allow *) next)
 	{
 		next = (ListStruct *)allow_ptr->next;
-		unreal_delete_masks(allow_ptr->mask);
+		free_security_group(allow_ptr->match);
 		Auth_FreeAuthConfig(allow_ptr->auth);
 		DelListItem(allow_ptr, conf_allow);
 		safe_free(allow_ptr);
@@ -2437,6 +2442,8 @@ void config_rehash()
 		free_motd(&tld_ptr->opermotd);
 		free_motd(&tld_ptr->botmotd);
 
+		free_security_group(tld_ptr->match);
+
 		DelListItem(tld_ptr, conf_tld);
 		safe_free(tld_ptr);
 	}
@@ -2450,7 +2457,7 @@ void config_rehash()
 		Auth_FreeAuthConfig(vhost_ptr->auth);
 		safe_free(vhost_ptr->virthost);
 		safe_free(vhost_ptr->virtuser);
-		unreal_delete_masks(vhost_ptr->mask);
+		free_security_group(vhost_ptr->match);
 		for (s = vhost_ptr->swhois; s; s = s_next)
 		{
 			s_next = s->next;
@@ -2488,7 +2495,7 @@ void config_rehash()
 		safe_free(deny_channel_ptr->reason);
 		safe_free(deny_channel_ptr->class);
 		DelListItem(deny_channel_ptr, conf_deny_channel);
-		unreal_delete_masks(deny_channel_ptr->mask);
+		free_security_group(deny_channel_ptr->match);
 		safe_free(deny_channel_ptr);
 	}
 
@@ -2498,7 +2505,7 @@ void config_rehash()
 		safe_free(allow_channel_ptr->channel);
 		safe_free(allow_channel_ptr->class);
 		DelListItem(allow_channel_ptr, conf_allow_channel);
-		unreal_delete_masks(allow_channel_ptr->mask);
+		free_security_group(allow_channel_ptr->match);
 		safe_free(allow_channel_ptr);
 	}
 
@@ -3036,7 +3043,7 @@ ConfigItem_tld *find_tld(Client *client)
 
 	for (tld = conf_tld; tld; tld = tld->next)
 	{
-		if (unreal_mask_match(client, tld->mask))
+		if (user_allowed_by_security_group(client, tld->match))
 		{
 			if ((tld->options & TLD_TLS) && !IsSecureConnect(client))
 				continue;
@@ -3056,7 +3063,8 @@ ConfigItem_link *find_link(const char *servername, Client *client)
 
 	for (link = conf_link; link; link = link->next)
 	{
-		if (match_simple(link->servername, servername) && unreal_mask_match(client, link->incoming.mask))
+		if (match_simple(link->servername, servername) &&
+		    user_allowed_by_security_group(client, link->incoming.match))
 		{
 		    return link;
 		}
@@ -3137,7 +3145,7 @@ ConfigItem_deny_channel *find_channel_allowed(Client *client, const char *name)
 		{
 			if (dchannel->class && strcmp(client->local->class->name, dchannel->class))
 				continue;
-			if (dchannel->mask && !unreal_mask_match(client, dchannel->mask))
+			if (dchannel->match && !user_allowed_by_security_group(client, dchannel->match))
 				continue;
 			break; /* MATCH deny channel { } */
 		}
@@ -3152,7 +3160,7 @@ ConfigItem_deny_channel *find_channel_allowed(Client *client, const char *name)
 			{
 				if (achannel->class && strcmp(client->local->class->name, achannel->class))
 					continue;
-				if (achannel->mask && !unreal_mask_match(client, achannel->mask))
+				if (achannel->match && !user_allowed_by_security_group(client, achannel->match))
 					continue;
 				break; /* MATCH allow channel { } */
 			}
@@ -3935,6 +3943,7 @@ int	_conf_oper(ConfigFile *conf, ConfigEntry *ce)
 
 	oper =  safe_alloc(sizeof(ConfigItem_oper));
 	safe_strdup(oper->name, ce->value);
+	oper->match = safe_alloc(sizeof(SecurityGroup));
 
 	/* Inherit some defaults: */
 	oper->server_notice_colors = tempiConf.server_notice_colors;
@@ -3990,6 +3999,10 @@ int	_conf_oper(ConfigFile *conf, ConfigEntry *ce)
 		{
 			oper->server_notice_show_event = config_checkval(cep->value, CFG_YESNO);
 		}
+		else if (!strcmp(cep->name, "auto-login"))
+		{
+			oper->auto_login = config_checkval(cep->value, CFG_YESNO);
+		}
 		else if (!strcmp(cep->name, "modes"))
 		{
 			oper->modes = set_usermode(cep->value);
@@ -4002,9 +4015,9 @@ int	_conf_oper(ConfigFile *conf, ConfigEntry *ce)
 		{
 			oper->maxlogins = atoi(cep->value);
 		}
-		else if (!strcmp(cep->name, "mask"))
+		else if (!strcmp(cep->name, "mask") || !strcmp(cep->name, "match"))
 		{
-			unreal_add_masks(&oper->mask, cep);
+			conf_match_block(conf, cep, &oper->match);
 		}
 		else if (!strcmp(cep->name, "vhost"))
 		{
@@ -4018,8 +4031,8 @@ int	_conf_oper(ConfigFile *conf, ConfigEntry *ce)
 int	_test_oper(ConfigFile *conf, ConfigEntry *ce)
 {
 	char has_class = 0, has_password = 0, has_snomask = 0;
-	char has_modes = 0, has_require_modes = 0, has_mask = 0, has_maxlogins = 0;
-	char has_operclass = 0, has_vhost = 0;
+	char has_modes = 0, has_require_modes = 0, has_mask = 0, has_match = 0, has_broad_match = 0;
+	char has_maxlogins = 0, has_operclass = 0, has_vhost = 0, has_auto_login = 0;
 	ConfigEntry *cep;
 	int errors = 0;
 
@@ -4125,6 +4138,10 @@ int	_test_oper(ConfigFile *conf, ConfigEntry *ce)
 			else if (!strcmp(cep->name, "server-notice-show-event"))
 			{
 			}
+			else if (!strcmp(cep->name, "auto-login"))
+			{
+				has_auto_login = config_checkval(cep->value, CFG_YESNO);
+			}
 			/* oper::modes */
 			else if (!strcmp(cep->name, "modes"))
 			{
@@ -4187,7 +4204,22 @@ int	_test_oper(ConfigFile *conf, ConfigEntry *ce)
 			else if (!strcmp(cep->name, "mask"))
 			{
 				if (cep->value || cep->items)
+				{
 					has_mask = 1;
+					test_match_block(conf, cep, &errors);
+					if (test_match_block_too_broad(conf, cep))
+						has_broad_match = 1;
+				}
+			}
+			else if (!strcmp(cep->name, "match"))
+			{
+				if (cep->value || cep->items)
+				{
+					has_match = 1;
+					test_match_block(conf, cep, &errors);
+					if (test_match_block_too_broad(conf, cep))
+						has_broad_match = 1;
+				}
 			}
 			else
 			{
@@ -4207,7 +4239,22 @@ int	_test_oper(ConfigFile *conf, ConfigEntry *ce)
 			else if (!strcmp(cep->name, "mask"))
 			{
 				if (cep->value || cep->items)
+				{
 					has_mask = 1;
+					test_match_block(conf, cep, &errors);
+					if (test_match_block_too_broad(conf, cep))
+						has_broad_match = 1;
+				}
+			}
+			else if (!strcmp(cep->name, "match"))
+			{
+				if (cep->value || cep->items)
+				{
+					has_match = 1;
+					test_match_block(conf, cep, &errors);
+					if (test_match_block_too_broad(conf, cep))
+						has_broad_match = 1;
+				}
 			}
 			else if (!strcmp(cep->name, "password"))
 			{
@@ -4230,16 +4277,39 @@ int	_test_oper(ConfigFile *conf, ConfigEntry *ce)
 			}
 		}
 	}
-	if (!has_password)
+
+	if (has_auto_login && has_broad_match)
 	{
-		config_error_missing(ce->file->filename, ce->line_number,
-			"oper::password");
+		config_error("%s:%i: your oper block for '%s' has auto-login but is completely unrestricted (mask *@*)!",
+		             ce->file->filename, ce->line_number, ce->value);
+		errors++;
+	} else
+	if (!has_password && has_broad_match)
+	{
+		config_error("%s:%i: your oper block for '%s' has no password and is completely unrestricted (mask *@*)!",
+		             ce->file->filename, ce->line_number, ce->value);
 		errors++;
 	}
-	if (!has_mask)
+
+	/* The rest should NOT be in an 'else'... */
+	if (has_password && has_auto_login)
+	{
+		config_error("%s:%i: You have auto-login enabled for your oper block '%s' but you also have a password set. "
+		             "Remove the password if you want to use auto-login.",
+		             ce->file->filename, ce->line_number, ce->value);
+		errors++;
+	}
+	if (!has_mask && !has_match)
 	{
 		config_error_missing(ce->file->filename, ce->line_number,
-			"oper::mask");
+			"oper::match");
+		errors++;
+	}
+	if (has_mask && has_match)
+	{
+		config_error("%s:%d: You cannot have both ::mask and ::match. "
+		             "You should only use oper::match.",
+		             ce->file->filename, ce->line_number);
 		errors++;
 	}
 	if (!has_class)
@@ -4590,8 +4660,8 @@ int     _conf_tld(ConfigFile *conf, ConfigEntry *ce)
 
 	for (cep = ce->items; cep; cep = cep->next)
 	{
-		if (!strcmp(cep->name, "mask"))
-			unreal_add_masks(&ca->mask, cep);
+		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
+			conf_match_block(conf, cep, &ca->match);
 		else if (!strcmp(cep->name, "motd"))
 		{
 			safe_strdup(ca->motd_file, cep->value);
@@ -4640,12 +4710,12 @@ int     _test_tld(ConfigFile *conf, ConfigEntry *ce)
 	ConfigEntry *cep;
 	int	    errors = 0;
 	int	    fd = -1;
-	char has_mask = 0, has_motd = 0, has_rules = 0, has_shortmotd = 0, has_channel = 0;
-	char has_opermotd = 0, has_botmotd = 0, has_options = 0;
+	char has_mask = 0, has_match = 0, has_motd = 0, has_rules = 0, has_shortmotd = 0;
+	char has_channel = 0, has_opermotd = 0, has_botmotd = 0, has_options = 0;
 
 	for (cep = ce->items; cep; cep = cep->next)
 	{
-		if (!cep->value && strcmp(cep->name, "options"))
+		if (!cep->value && strcmp(cep->name, "options") && strcmp(cep->name, "mask") && strcmp(cep->name, "match"))
 		{
 			config_error_empty(cep->file->filename, cep->line_number,
 				"tld", cep->name);
@@ -4656,7 +4726,18 @@ int     _test_tld(ConfigFile *conf, ConfigEntry *ce)
 		if (!strcmp(cep->name, "mask"))
 		{
 			if (cep->value || cep->items)
+			{
 				has_mask = 1;
+				test_match_block(conf, cep, &errors);
+			}
+		}
+		else if (!strcmp(cep->name, "match"))
+		{
+			if (cep->value || cep->items)
+			{
+				has_match = 1;
+				test_match_block(conf, cep, &errors);
+			}
 		}
 		/* tld::motd */
 		else if (!strcmp(cep->name, "motd"))
@@ -4806,10 +4887,17 @@ int     _test_tld(ConfigFile *conf, ConfigEntry *ce)
 			continue;
 		}
 	}
-	if (!has_mask)
+	if (!has_mask && !has_match)
 	{
 		config_error_missing(ce->file->filename, ce->line_number,
-			"tld::mask");
+			"tld::match");
+		errors++;
+	}
+	if (has_mask && has_match)
+	{
+		config_error("%s:%d: You cannot have both ::mask and ::match. "
+		             "You should only use %s::match.",
+		             ce->file->filename, ce->line_number, ce->name);
 		errors++;
 	}
 	if (!has_motd)
@@ -5342,12 +5430,13 @@ int	_conf_allow(ConfigFile *conf, ConfigEntry *ce)
 	}
 	allow = safe_alloc(sizeof(ConfigItem_allow));
 	allow->ipv6_clone_mask = tempiConf.default_ipv6_clone_mask;
+	allow->match = safe_alloc(sizeof(SecurityGroup));
 
 	for (cep = ce->items; cep; cep = cep->next)
 	{
-		if (!strcmp(cep->name, "mask") || !strcmp(cep->name, "ip") || !strcmp(cep->name, "hostname"))
+		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask") || !strcmp(cep->name, "ip") || !strcmp(cep->name, "hostname"))
 		{
-			unreal_add_masks(&allow->mask, cep);
+			conf_match_block(conf, cep, &allow->match);
 		}
 		else if (!strcmp(cep->name, "password"))
 			allow->auth = AuthBlockToAuthConfig(cep);
@@ -5413,7 +5502,7 @@ int	_test_allow(ConfigFile *conf, ConfigEntry *ce)
 	ConfigEntry *cep, *cepp;
 	int		errors = 0;
 	Hook *h;
-	char has_ip = 0, has_hostname = 0, has_mask = 0;
+	char has_ip = 0, has_hostname = 0, has_mask = 0, has_match = 0;
 	char has_maxperip = 0, has_global_maxperip = 0, has_password = 0, has_class = 0;
 	char has_redirectserver = 0, has_redirectport = 0, has_options = 0;
 	int hostname_possible_silliness = 0;
@@ -5463,6 +5552,7 @@ int	_test_allow(ConfigFile *conf, ConfigEntry *ce)
 	for (cep = ce->items; cep; cep = cep->next)
 	{
 		if (strcmp(cep->name, "options") &&
+		    strcmp(cep->name, "match") &&
 		    strcmp(cep->name, "mask") &&
 		    config_is_blankorempty(cep, "allow"))
 		{
@@ -5494,6 +5584,12 @@ int	_test_allow(ConfigFile *conf, ConfigEntry *ce)
 		else if (!strcmp(cep->name, "mask"))
 		{
 			has_mask = 1;
+			test_match_block(conf, cep, &errors);
+		}
+		else if (!strcmp(cep->name, "match"))
+		{
+			has_match = 1;
+			test_match_block(conf, cep, &errors);
 		}
 		else if (!strcmp(cep->name, "maxperip"))
 		{
@@ -5641,27 +5737,33 @@ int	_test_allow(ConfigFile *conf, ConfigEntry *ce)
 		}
 	}
 
-	if (has_mask && (has_ip || has_hostname))
+	if ((has_mask || has_match) && (has_ip || has_hostname))
 	{
-		config_error("%s:%d: The allow block uses allow::mask, but you also have an allow::ip and allow::hostname.",
+		config_error("%s:%d: The allow block uses allow::match, but you also have an allow::ip and allow::hostname.",
 			ce->file->filename, ce->line_number);
-		config_error("Please delete your allow::ip and allow::hostname entries and/or integrate them into allow::mask");
+		config_error("Please delete your allow::ip and allow::hostname entries and/or integrate them into allow::match");
 	} else
 	if (has_ip)
 	{
-		config_warn("%s:%d: The allow block uses allow::mask nowadays. Rename your allow::ip item to allow::mask.",
+		config_warn("%s:%d: The allow block uses allow::match nowadays. Rename your allow::ip item to allow::match.",
 			ce->file->filename, ce->line_number);
 		config_warn("See https://www.unrealircd.org/docs/FAQ#allow-mask for more information");
 	} else
 	if (has_hostname)
 	{
-		config_warn("%s:%d: The allow block uses allow::mask nowadays. Rename your allow::hostname item to allow::mask.",
+		config_warn("%s:%d: The allow block uses allow::match nowadays. Rename your allow::hostname item to allow::match.",
 			ce->file->filename, ce->line_number);
 		config_warn("See https://www.unrealircd.org/docs/FAQ#allow-mask for more information");
 	} else
-	if (!has_mask)
+	if (has_mask && has_match)
+	{
+		config_error("%s:%d: You cannot have both ::mask and ::match. You should only use allow::match.",
+				 ce->file->filename, ce->line_number);
+		errors++;
+	} else
+	if (!has_match && !has_mask)
 	{
-		config_error("%s:%d: allow block needs an allow::mask",
+		config_error("%s:%d: allow block needs an allow::match",
 				 ce->file->filename, ce->line_number);
 		errors++;
 	}
@@ -5702,15 +5804,15 @@ int	_conf_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 	ConfigItem_allow_channel 	*allow = NULL;
 	ConfigEntry 	    	*cep;
 	char *class = NULL;
-	ConfigEntry *mask = NULL;
+	ConfigEntry *match = NULL;
 
 	/* First, search for ::class, if any */
 	for (cep = ce->items; cep; cep = cep->next)
 	{
 		if (!strcmp(cep->name, "class"))
 			class = cep->value;
-		else if (!strcmp(cep->name, "mask"))
-			mask = cep;
+		else if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
+			match = cep;
 	}
 
 	for (cep = ce->items; cep; cep = cep->next)
@@ -5722,8 +5824,8 @@ int	_conf_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 			safe_strdup(allow->channel, cep->value);
 			if (class)
 				safe_strdup(allow->class, class);
-			if (mask)
-				unreal_add_masks(&allow->mask, mask);
+			if (match)
+				conf_match_block(conf, match, &allow->match);
 			AddListItem(allow, conf_allow_channel);
 		}
 	}
@@ -5732,9 +5834,10 @@ int	_conf_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 
 int	_test_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 {
-	ConfigEntry		*cep;
-	int			errors = 0;
-	char			has_channel = 0, has_class = 0;
+	ConfigEntry	*cep;
+	int		errors = 0;
+	char		has_match = 0, has_mask = 0, has_channel = 0, has_class = 0;
+
 	for (cep = ce->items; cep; cep = cep->next)
 	{
 		if (config_is_blankorempty(cep, "allow channel"))
@@ -5758,8 +5861,15 @@ int	_test_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 			}
 			has_class = 1;
 		}
+		else if (!strcmp(cep->name, "match"))
+		{
+			has_match = 1;
+			test_match_block(conf, cep, &errors);
+		}
 		else if (!strcmp(cep->name, "mask"))
 		{
+			has_mask = 1;
+			test_match_block(conf, cep, &errors);
 		}
 		else
 		{
@@ -5768,6 +5878,13 @@ int	_test_allow_channel(ConfigFile *conf, ConfigEntry *ce)
 			errors++;
 		}
 	}
+	if (has_mask && has_match)
+	{
+		config_error("%s:%d: You cannot have both ::mask and ::match. "
+		             "You should only use %s::match.",
+		             ce->file->filename, ce->line_number, ce->name);
+		errors++;
+	}
 	if (!has_channel)
 	{
 		config_error_missing(ce->file->filename, ce->line_number,
@@ -5860,6 +5977,7 @@ int	_conf_vhost(ConfigFile *conf, ConfigEntry *ce)
 	ConfigItem_vhost *vhost;
 	ConfigEntry *cep, *cepp;
 	vhost = safe_alloc(sizeof(ConfigItem_vhost));
+	vhost->match = safe_alloc(sizeof(SecurityGroup));
 
 	for (cep = ce->items; cep; cep = cep->next)
 	{
@@ -5880,9 +5998,9 @@ int	_conf_vhost(ConfigFile *conf, ConfigEntry *ce)
 			safe_strdup(vhost->login, cep->value);
 		else if (!strcmp(cep->name, "password"))
 			vhost->auth = AuthBlockToAuthConfig(cep);
-		else if (!strcmp(cep->name, "mask"))
+		else if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
 		{
-			unreal_add_masks(&vhost->mask, cep);
+			conf_match_block(conf, cep, &vhost->match);
 		}
 		else if (!strcmp(cep->name, "swhois"))
 		{
@@ -5914,7 +6032,7 @@ int	_test_vhost(ConfigFile *conf, ConfigEntry *ce)
 {
 	int errors = 0;
 	ConfigEntry *cep;
-	char has_vhost = 0, has_login = 0, has_password = 0, has_mask = 0;
+	char has_vhost = 0, has_login = 0, has_password = 0, has_mask = 0, has_match = 0;
 
 	for (cep = ce->items; cep; cep = cep->next)
 	{
@@ -6007,6 +6125,12 @@ int	_test_vhost(ConfigFile *conf, ConfigEntry *ce)
 		else if (!strcmp(cep->name, "mask"))
 		{
 			has_mask = 1;
+			test_match_block(conf, cep, &errors);
+		}
+		else if (!strcmp(cep->name, "match"))
+		{
+			has_match = 1;
+			test_match_block(conf, cep, &errors);
 		}
 		else if (!strcmp(cep->name, "swhois"))
 		{
@@ -6038,10 +6162,17 @@ int	_test_vhost(ConfigFile *conf, ConfigEntry *ce)
 			"vhost::password");
 		errors++;
 	}
-	if (!has_mask)
+	if (!has_mask && !has_match)
 	{
 		config_error_missing(ce->file->filename, ce->line_number,
-			"vhost::mask");
+			"vhost::match");
+		errors++;
+	}
+	if (has_mask && has_match)
+	{
+		config_error("%s:%d: You cannot have both ::mask and ::match. "
+		             "You should only use %s::match.",
+		             ce->file->filename, ce->line_number, ce->name);
 		errors++;
 	}
 	return errors;
@@ -6172,9 +6303,9 @@ int	_conf_link(ConfigFile *conf, ConfigEntry *ce)
 		{
 			for (cepp = cep->items; cepp; cepp = cepp->next)
 			{
-				if (!strcmp(cepp->name, "mask"))
+				if (!strcmp(cepp->name, "match") || !strcmp(cepp->name, "mask"))
 				{
-					unreal_add_masks(&link->incoming.mask, cepp);
+					conf_match_block(conf, cepp, &link->incoming.match);
 				}
 			}
 		}
@@ -6184,6 +6315,8 @@ int	_conf_link(ConfigFile *conf, ConfigEntry *ce)
 			{
 				if (!strcmp(cepp->name, "bind-ip"))
 					safe_strdup(link->outgoing.bind_ip, cepp->value);
+				else if (!strcmp(cepp->name, "file"))
+					safe_strdup(link->outgoing.file, cepp->value);
 				else if (!strcmp(cepp->name, "hostname"))
 					safe_strdup(link->outgoing.hostname, cepp->value);
 				else if (!strcmp(cepp->name, "port"))
@@ -6273,7 +6406,7 @@ int	_test_link(ConfigFile *conf, ConfigEntry *ce)
 	ConfigEntry *cep, *cepp, *ceppp;
 	int errors = 0;
 
-	int has_incoming = 0, has_incoming_mask = 0, has_outgoing = 0;
+	int has_incoming = 0, has_incoming_mask = 0, has_incoming_match = 0, has_outgoing = 0, has_outgoing_file = 0;
 	int has_outgoing_bind_ip = 0, has_outgoing_hostname = 0, has_outgoing_port = 0;
 	int has_outgoing_options = 0, has_hub = 0, has_leaf = 0, has_leaf_depth = 0;
 	int has_password = 0, has_class = 0, has_options = 0;
@@ -6300,11 +6433,26 @@ int	_test_link(ConfigFile *conf, ConfigEntry *ce)
 			config_detect_duplicate(&has_incoming, cep, &errors);
 			for (cepp = cep->items; cepp; cepp = cepp->next)
 			{
+				if (!strcmp(cepp->name, "match"))
+				{
+					if (cepp->value || cepp->items)
+					{
+						has_incoming_match = 1;
+						test_match_block(conf, cepp, &errors);
+					} else
+					if (config_is_blankorempty(cepp, "link::incoming"))
+					{
+						errors++;
+						continue;
+					}
+				} else
 				if (!strcmp(cepp->name, "mask"))
 				{
 					if (cepp->value || cepp->items)
+					{
 						has_incoming_mask = 1;
-					else
+						test_match_block(conf, cepp, &errors);
+					} else
 					if (config_is_blankorempty(cepp, "link::incoming"))
 					{
 						errors++;
@@ -6328,6 +6476,15 @@ int	_test_link(ConfigFile *conf, ConfigEntry *ce)
 					config_detect_duplicate(&has_outgoing_bind_ip, cepp, &errors);
 					// todo: ipv4 vs ipv6
 				}
+				else if (!strcmp(cepp->name, "file"))
+				{
+					if (config_is_blankorempty(cepp, "link::outgoing"))
+					{
+						errors++;
+						continue;
+					}
+					config_detect_duplicate(&has_outgoing_file, cepp, &errors);
+				}
 				else if (!strcmp(cepp->name, "hostname"))
 				{
 					if (config_is_blankorempty(cepp, "link::outgoing"))
@@ -6491,24 +6648,41 @@ int	_test_link(ConfigFile *conf, ConfigEntry *ce)
 	if (has_incoming)
 	{
 		/* If we have an incoming sub-block then we need at least 'mask' and 'password' */
-		if (!has_incoming_mask)
+		if (!has_incoming_mask && !has_incoming_match)
+		{
+			config_error_missing(ce->file->filename, ce->line_number, "link::incoming::match");
+			errors++;
+		}
+		if (has_incoming_mask && has_incoming_match)
 		{
-			config_error_missing(ce->file->filename, ce->line_number, "link::incoming::mask");
+			config_error("%s:%d: You cannot have both link::incoming::mask and link::incoming::match. "
+				     "You should only use link::incoming::match.",
+				     ce->file->filename, ce->line_number);
 			errors++;
 		}
 	}
 
 	if (has_outgoing)
 	{
-		/* If we have an outgoing sub-block then we need at least a hostname and port */
-		if (!has_outgoing_hostname)
+		/* If we have an outgoing sub-block then we need at least a hostname and port or a file */
+		if (!has_outgoing_file)
 		{
-			config_error_missing(ce->file->filename, ce->line_number, "link::outgoing::hostname");
-			errors++;
+			if (!has_outgoing_hostname)
+			{
+				config_error_missing(ce->file->filename, ce->line_number, "link::outgoing::hostname");
+				errors++;
+			}
+			if (!has_outgoing_port)
+			{
+				config_error_missing(ce->file->filename, ce->line_number, "link::outgoing::port");
+				errors++;
+			}
 		}
-		if (!has_outgoing_port)
+		else if (has_outgoing_file && (has_outgoing_hostname || has_outgoing_port))
 		{
-			config_error_missing(ce->file->filename, ce->line_number, "link::outgoing::port");
+			config_error("%s:%d: link block should either have a 'file' (for *NIX domain socket), "
+			             "OR have a 'hostname' and 'port' (for IPv4/IPv6). You cannot combine both in one link block.",
+			             ce->file->filename, ce->line_number);
 			errors++;
 		}
 	}
@@ -8965,6 +9139,7 @@ int	_test_set(ConfigFile *conf, ConfigEntry *ce)
 		{
 			/* keep this in sync with _test_allow() */
 			int ipv6mask;
+			CheckNull(cep);
 			ipv6mask = atoi(cep->value);
 			if (ipv6mask == 0)
 			{
@@ -9818,9 +9993,9 @@ int	_conf_deny_channel(ConfigFile *conf, ConfigEntry *ce)
 		{
 			safe_strdup(deny->class, cep->value);
 		}
-		else if (!strcmp(cep->name, "mask"))
+		else if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
 		{
-			unreal_add_masks(&deny->mask, cep);
+			conf_match_block(conf, cep, &deny->match);
 		}
 	}
 	AddListItem(deny, conf_deny_channel);
@@ -9894,6 +10069,7 @@ int     _test_deny(ConfigFile *conf, ConfigEntry *ce)
 	if (!strcmp(ce->value, "channel"))
 	{
 		char has_channel = 0, has_warn = 0, has_reason = 0, has_redirect = 0, has_class = 0;
+		char has_mask = 0, has_match = 0;
 		for (cep = ce->items; cep; cep = cep->next)
 		{
 			if (config_is_blankorempty(cep, "deny channel"))
@@ -9951,8 +10127,15 @@ int     _test_deny(ConfigFile *conf, ConfigEntry *ce)
 				}
 				has_class = 1;
 			}
+			else if (!strcmp(cep->name, "match"))
+			{
+				has_match = 1;
+				test_match_block(conf, cep, &errors);
+			}
 			else if (!strcmp(cep->name, "mask"))
 			{
+				has_mask = 1;
+				test_match_block(conf, cep, &errors);
 			}
 			else
 			{
@@ -9973,6 +10156,13 @@ int     _test_deny(ConfigFile *conf, ConfigEntry *ce)
 				"deny channel::reason");
 			errors++;
 		}
+		if (has_mask && has_match)
+		{
+			config_error("%s:%d: You cannot have both ::mask and ::match. "
+				     "You should only use %s %s::match.",
+				     ce->file->filename, ce->line_number, ce->name, ce->value);
+			errors++;
+		}
 	}
 	else if (!strcmp(ce->value, "link"))
 	{
@@ -10177,104 +10367,6 @@ int     _test_deny(ConfigFile *conf, ConfigEntry *ce)
 	return errors;
 }
 
-int _test_security_group(ConfigFile *conf, ConfigEntry *ce)
-{
-	int errors = 0;
-	ConfigEntry *cep;
-
-	if (!ce->value)
-	{
-		config_error("%s:%i: security-group block needs a name, eg: security-group web-users {",
-			ce->file->filename, ce->line_number);
-		errors++;
-	} else {
-		if (!strcasecmp(ce->value, "unknown-users"))
-		{
-			config_error("%s:%i: The 'unknown-users' group is a special group that is the "
-			             "inverse of 'known-users', you cannot create or adjust it in the "
-			             "config file, as it is created automatically by UnrealIRCd.",
-			             ce->file->filename, ce->line_number);
-			errors++;
-			return errors;
-		}
-		if (!security_group_valid_name(ce->value))
-		{
-			config_error("%s:%i: security-group block name '%s' contains invalid characters or is too long. "
-			             "Only letters, numbers, underscore and hyphen are allowed.",
-			             ce->file->filename, ce->line_number, ce->value);
-			errors++;
-		}
-	}
-
-	for (cep = ce->items; cep; cep = cep->next)
-	{
-		if (!strcmp(cep->name, "webirc"))
-		{
-			CheckNull(cep);
-		} else
-		if (!strcmp(cep->name, "identified"))
-		{
-			CheckNull(cep);
-		} else
-		if (!strcmp(cep->name, "tls"))
-		{
-			CheckNull(cep);
-		} else
-		if (!strcmp(cep->name, "reputation-score"))
-		{
-			int v;
-			CheckNull(cep);
-			v = atoi(cep->value);
-			if ((v < 1) || (v > 10000))
-			{
-				config_error("%s:%i: security-group::reputation-score needs to be a value of 1-10000",
-					cep->file->filename, cep->line_number);
-				errors++;
-			}
-		} else
-		if (!strcmp(cep->name, "include-mask"))
-		{
-		} else
-		{
-			config_error_unknown(cep->file->filename, cep->line_number,
-				"security-group", cep->name);
-			errors++;
-			continue;
-		}
-	}
-
-	return errors;
-}
-
-int _conf_security_group(ConfigFile *conf, ConfigEntry *ce)
-{
-	ConfigEntry *cep;
-	SecurityGroup *s = add_security_group(ce->value, 1);
-
-	for (cep = ce->items; cep; cep = cep->next)
-	{
-		if (!strcmp(cep->name, "webirc"))
-			s->webirc = config_checkval(cep->value, CFG_YESNO);
-		else if (!strcmp(cep->name, "identified"))
-			s->identified = config_checkval(cep->value, CFG_YESNO);
-		else if (!strcmp(cep->name, "tls"))
-			s->tls = config_checkval(cep->value, CFG_YESNO);
-		else if (!strcmp(cep->name, "reputation-score"))
-			s->reputation_score = atoi(cep->value);
-		else if (!strcmp(cep->name, "priority"))
-		{
-			s->priority = atoi(cep->value);
-			DelListItem(s, securitygroups);
-			AddListItemPrio(s, securitygroups, s->priority);
-		}
-		else if (!strcmp(cep->name, "include-mask"))
-		{
-			unreal_add_masks(&s->include_mask, cep);
-		}
-	}
-	return 1;
-}
-
 Secret *find_secret(const char *secret_name)
 {
 	Secret *s;
@@ -10687,6 +10779,7 @@ int rehash_internal(Client *client)
 	reread_motdsandrules();
 	unload_all_unused_umodes();
 	unload_all_unused_extcmodes();
+	unload_all_unused_extbans();
 	unload_all_unused_caps();
 	unload_all_unused_history_backends();
 	// unload_all_unused_moddata(); -- this will crash
@@ -10703,8 +10796,9 @@ int rehash_internal(Client *client)
 void link_cleanup(ConfigItem_link *link_ptr)
 {
 	safe_free(link_ptr->servername);
-	unreal_delete_masks(link_ptr->incoming.mask);
+	free_security_group(link_ptr->incoming.match);
 	Auth_FreeAuthConfig(link_ptr->auth);
+	safe_free(link_ptr->outgoing.file);
 	safe_free(link_ptr->outgoing.bind_ip);
 	safe_free(link_ptr->outgoing.hostname);
 	safe_free(link_ptr->hub);
diff --git a/src/conf_preprocessor.c b/src/conf_preprocessor.c
@@ -1,6 +1,6 @@
 /* UnrealIRCd configuration preprocessor
  * (C) Copyright 2019 Bram Matthys ("Syzop") and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  *
  * Technically this isn't a 100% true preprocessor, but to the end user
  * it will certainly look like it, hence the name.
diff --git a/src/crashreport.c b/src/crashreport.c
@@ -1,6 +1,6 @@
 /* UnrealIRCd crash reporter code.
  * (C) Copyright 2015-2019 Bram Matthys ("Syzop") and the UnrealIRCd Team.
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -306,7 +306,7 @@ int crash_report_asan_log(FILE *reportfd, char *coredump)
 		stripcrlf(buf);
 		fprintf(reportfd, " %s\n", buf);
 	}
-	n = pclose(fd);
+	n = fclose(fd);
 	fprintf(reportfd, "END OF ASAN LOG\n");
 
 	if (WEXITSTATUS(n) == 127)
diff --git a/src/ircd.c b/src/ircd.c
@@ -57,7 +57,7 @@ EVENT(garbage_collect)
 		if (loop.do_garbage_collect == 1) {
 			loop.do_garbage_collect = 0;
 			unreal_log(ULOG_INFO, "main", "GARBAGE_COLLECT_STARTED", NULL, "Cleaned up $count garbage blocks",
-			           (ii - freelinks));
+			           log_data_integer("count", (ii - freelinks)));
 		}
 	}
 	if (loop.do_garbage_collect == 1)
@@ -83,7 +83,7 @@ int match_tkls(Client *client)
 		{
 			unreal_log(ULOG_INFO, "tkl", "BAN_REALNAME", client,
 			           "Banned client $client.details due to realname ban: $reason",
-			           bconf->reason ? bconf->reason : "no reason");
+			           log_data_string("reason", bconf->reason ? bconf->reason : "no reason"));
 
 			if (bconf->reason) {
 				if (IsUser(client))
diff --git a/src/ircd_vars.c b/src/ircd_vars.c
@@ -1,7 +1,7 @@
 /************************************************************************
  * UnrealIRCd - Unreal Internet Relay Chat Daemon - src/ircd_vars.c
  * (c) 2021- Bram Matthys and The UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 #include "unrealircd.h"
 
diff --git a/src/log.c b/src/log.c
@@ -3,7 +3,7 @@
  * (C) 2021 Bram Matthys (Syzop) and the UnrealIRCd Team
  *
  * See file AUTHORS in IRC package for additional names of
- * the programmers. 
+ * the programmers.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -43,6 +43,26 @@ int log_sources_match(LogSource *logsource, LogLevel loglevel, const char *subsy
 void do_unreal_log_internal(LogLevel loglevel, const char *subsystem, const char *event_id, Client *client, int expand_msg, const char *msg, va_list vl);
 void log_blocks_switchover(void);
 
+/** Calculate expansion of a JSON string thanks to double escaping.
+ * orig => JSON => IRC
+ *    " => \"   => \\"
+ *    \ => \\   => \\\\
+ */
+int json_dump_string_length(const char *s)
+{
+	int len = 0;
+	for (; *s; s++)
+	{
+		if (*s == '\\')
+			len += 4;
+		else if (*s == '"')
+			len += 3;
+		else
+			len++;
+	}
+	return len;
+}
+
 /** Convert a regular string value to a JSON string.
  * In UnrealIRCd, this must be used instead of json_string()
  * as we may use non-UTF8 sequences. Also, this takes care
@@ -53,13 +73,16 @@ void log_blocks_switchover(void);
  */
 json_t *json_string_unreal(const char *s)
 {
-	static char buf[8192];
+	char buf1[512], buf2[512];
 	char *verified_s;
+	const char *stripped;
 
 	if (s == NULL)
 		return json_null();
 
-	verified_s = unrl_utf8_make_valid(s, buf, sizeof(buf), 0);
+	stripped = StripControlCodesEx(s, buf1, sizeof(buf1), UNRL_STRIP_LOW_ASCII|UNRL_STRIP_KEEP_LF);
+	verified_s = unrl_utf8_make_valid(buf1, buf2, sizeof(buf2), 0);
+
 	return json_string(verified_s);
 }
 
@@ -561,6 +584,9 @@ void json_expand_client(json_t *j, const char *key, Client *client, int detail)
 	if (client->local && client->local->creationtime)
 		json_object_set_new(child, "connected_since", json_timestamp(client->local->creationtime));
 
+	if (client->local && client->local->idle_since)
+		json_object_set_new(child, "idle_since", json_timestamp(client->local->idle_since));
+
 	if (client->user)
 	{
 		char buf[512];
@@ -572,6 +598,10 @@ void json_expand_client(json_t *j, const char *key, Client *client, int detail)
 		json_object_set_new(user, "username", json_string_unreal(client->user->username));
 		if (!BadPtr(client->info))
 			json_object_set_new(user, "realname", json_string_unreal(client->info));
+		if (has_user_mode(client, 'x') && client->user->virthost && strcmp(client->user->virthost, client->user->realhost))
+			json_object_set_new(user, "vhost", json_string_unreal(client->user->virthost));
+		if (*client->user->cloakedhost)
+			json_object_set_new(user, "cloakedhost", json_string_unreal(client->user->cloakedhost));
 		if (client->uplink)
 			json_object_set_new(user, "servername", json_string_unreal(client->uplink->name));
 		if (IsLoggedIn(client))
@@ -592,6 +622,26 @@ void json_expand_client(json_t *j, const char *key, Client *client, int detail)
 		str = get_operclass(client);
 		if (str)
 			json_object_set_new(user, "operclass", json_string_unreal(str));
+		if (client->user->channel)
+		{
+			Membership *m;
+			int cnt = 0;
+			int len = 0;
+			json_t *channels = json_array();
+			json_object_set_new(user, "channels", channels);
+			for (m = client->user->channel; m; m = m->next)
+			{
+				len += json_dump_string_length(m->channel->name);
+				if (len > 384)
+				{
+					/* Truncated */
+					json_array_append_new(channels, json_string_unreal("..."));
+					break;
+				}
+				json_array_append_new(channels, json_string_unreal(m->channel->name));
+			}
+		}
+		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_USER, client, detail, child, user);
 	} else
 	if (IsMe(client))
 	{
@@ -645,7 +695,9 @@ void json_expand_client(json_t *j, const char *key, Client *client, int detail)
 		}
 		if (!BadPtr(client->server->features.nickchars))
 			json_object_set_new(features, "nick_character_sets", json_string_unreal(client->server->features.nickchars));
+		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
 	}
+	RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
 }
 
 void json_expand_channel(json_t *j, const char *key, Channel *channel, int detail)
@@ -675,6 +727,7 @@ void json_expand_channel(json_t *j, const char *key, Channel *channel, int detai
 	}
 
 	// Possibly later: If detail is set to 1 then expand more...
+	RunHook(HOOKTYPE_JSON_EXPAND_CHANNEL, channel, detail, child);
 }
 
 const char *timestamp_iso8601_now(void)
@@ -883,9 +936,11 @@ LogData *log_data_link_block(ConfigItem_link *link)
 	safe_strdup(d->key, "link_block");
 	d->value.object = j = json_object();
 	json_object_set_new(j, "name", json_string_unreal(link->servername));
+	json_object_set_new(j, "file", json_string_unreal(link->outgoing.file));
 	json_object_set_new(j, "hostname", json_string_unreal(link->outgoing.hostname));
 	json_object_set_new(j, "ip", json_string_unreal(link->connect_ip));
 	json_object_set_new(j, "port", json_integer(link->outgoing.port));
+	json_object_set_new(j, "class", json_string_unreal(link->class->name));
 
 	if (!link->outgoing.bind_ip && iConf.link_bindip)
 		bind_ip = iConf.link_bindip;
diff --git a/src/misc.c b/src/misc.c
@@ -916,145 +916,74 @@ int is_autojoin_chan(const char *chname)
 	return 0;
 }
 
-/** Free all masks in the mask list */
-void unreal_delete_masks(ConfigItem_mask *m)
+/** Add name entries from config */
+void unreal_add_names(NameList **n, ConfigEntry *ce)
 {
-	ConfigItem_mask *m_next;
-
-	for (; m; m = m_next)
+	if (ce->items)
 	{
-		m_next = m->next;
-
-		safe_free(m->mask);
-
-		safe_free(m);
-	}
-}
-
-/** Internal function to add one individual mask to the list */
-static void unreal_add_mask(ConfigItem_mask **head, ConfigEntry *ce)
-{
-	ConfigItem_mask *m = safe_alloc(sizeof(ConfigItem_mask));
-
-	/* Since we allow both mask "xyz"; and mask { abc; def; };... */
+		ConfigEntry *cep;
+		for (cep = ce->items; cep; cep = cep->next)
+			_add_name_list(n, cep->value ? cep->value : cep->name);
+	} else
 	if (ce->value)
-		safe_strdup(m->mask, ce->value);
-	else
-		safe_strdup(m->mask, ce->name);
-
-	add_ListItem((ListStruct *)m, (ListStruct **)head);
+	{
+		_add_name_list(n, ce->value);
+	}
 }
 
-/** Add mask entries from config */
-void unreal_add_masks(ConfigItem_mask **head, ConfigEntry *ce)
+/** Add name/value entries from config */
+void unreal_add_name_values(NameValuePrioList **n, const char *name, ConfigEntry *ce)
 {
 	if (ce->items)
 	{
 		ConfigEntry *cep;
 		for (cep = ce->items; cep; cep = cep->next)
-			unreal_add_mask(head, cep);
+			add_nvplist(n, 0, name, cep->value ? cep->value : cep->name);
 	} else
+	if (ce->value)
 	{
-		unreal_add_mask(head, ce);
+		add_nvplist(n, 0, name, ce->value);
 	}
 }
 
-/** Check if a client matches any of the masks in the mask list.
- * The following rules apply:
- * - If you have only negating entries, like '!abc' and '!def', then
- *   we assume an implicit * rule first, since that is clearly what
- *   the user wants.
- * - If you have a mix, like '*.com', '!irc1*', '!irc2*' then the
- *   implicit * is dropped and we assume you only want to match *.com,
- *   with the exception of irc1*.com and irc2*.com.
- * - If you only have normal entries without ! then things are
- *   as they always are.
- * @param client	The client to run the mask match against
- * @param mask		The mask entry from the config file
- * @returns 1 on match, 0 on non-match.
- */
-int unreal_mask_match(Client *client, ConfigItem_mask *mask)
+/** Prints the name:value pair of a NameValuePrioList */
+const char *namevalue(NameValuePrioList *n)
 {
-	int retval = 1;
-	ConfigItem_mask *m;
+	static char buf[512];
 
-	if (!mask)
-		return 0; /* Empty mask block is no match */
+	if (!n->name)
+		return "";
 
-	/* First check normal matches (without ! prefix) */
-	for (m = mask; m; m = m->next)
-	{
-		if (m->mask[0] != '!')
-		{
-			retval = 0; /* no implicit * */
-			if (match_user(m->mask, client, MATCH_CHECK_REAL|MATCH_CHECK_EXTENDED))
-			{
-				retval = 1;
-				break;
-			}
-		}
-	}
+	if (!n->value)
+		return n->name;
 
-	if (retval)
-	{
-		/* We matched. Check for exceptions (with ! prefix) */
-		for (m = mask; m; m = m->next)
-		{
-			if ((m->mask[0] == '!') && match_user(m->mask+1, client, MATCH_CHECK_REAL|MATCH_CHECK_EXTENDED))
-				return 0;
-		}
-	}
+	snprintf(buf, sizeof(buf), "%s:%s", n->name, n->value);
+	return buf;
+}
 
-	return retval;
-}
-
-/** Check if a string matches any of the masks in the mask list.
- * The following rules apply:
- * - If you have only negating entries, like '!abc' and '!def', then
- *   we assume an implicit * rule first, since that is clearly what
- *   the user wants.
- * - If you have a mix, like '*.com', '!irc1*', '!irc2*' then the
- *   implicit * is dropped and we assume you only want to match *.com,
- *   with the exception of irc1*.com and irc2*.com.
- * - If you only have normal entries without ! then things are
- *   as they always are.
- * @param name	The name to run the mask matching on
- * @param mask	The mask entry from the config file
- * @returns 1 on match, 0 on non-match.
+/** Version of namevalue() but replaces spaces with underscores.
+ * Used in for example numeric sending routines where a field
+ * may not contain any spaces.
  */
-int unreal_mask_match_string(const char *name, ConfigItem_mask *mask)
+const char *namevalue_nospaces(NameValuePrioList *n)
 {
-	int retval = 1;
-	ConfigItem_mask *m;
+	static char buf[512];
+	char *p;
 
-	if (!mask)
-		return 0; /* Empty mask block is no match */
+	if (!n->name)
+		return "";
 
-	/* First check normal matches (without ! prefix) */
-	for (m = mask; m; m = m->next)
-	{
-		if (m->mask[0] != '!')
-		{
-			retval = 0; /* no implicit * */
-			if (match_simple(m->mask, name))
-			{
-				retval = 1;
-				break;
-			}
-		}
-	}
+	if (!n->value)
+		strlcpy(buf, n->name, sizeof(buf));
 
-	if (retval)
-	{
-		/* We matched. Check for exceptions (with ! prefix) */
-		for (m = mask; m; m = m->next)
-		{
-			if ((m->mask[0] == '!') && match_simple(m->mask+1, name))
-				return 0;
-		}
-	}
+	snprintf(buf, sizeof(buf), "%s:%s", n->name, n->value);
+
+	/* Replace spaces with underscores */
+	for (p=buf; *p; p++)
+		if (*p == ' ')
+			*p = '_';
 
-	return retval;
+	return buf;
 }
 
 /** Our own strcasestr implementation because strcasestr is
@@ -1461,6 +1390,11 @@ void do_unreal_log_remote_deliver_default_handler(LogLevel loglevel, const char 
 {
 }
 
+int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost)
+{
+	return 0;
+}
+
 /** my_timegm: mktime()-like function which will use GMT/UTC.
  * Strangely enough there is no standard function for this.
  * On some *NIX OS's timegm() may be available, sometimes only
@@ -2539,3 +2473,141 @@ int minimum_msec_since_last_run(struct timeval *tv_old, long minimum)
 	}
 	return 0;
 }
+
+/** Strip color, bold, underline, and reverse codes from a string.
+ * @param text			The input text
+ * @param output		The buffer for the output text
+ * @param outputlen		The length of the output buffer
+ * @param strip_flags		Zero or (a combination of) UNRL_STRIP_LOW_ASCII / UNRL_STRIP_KEEP_LF.
+ * @returns The new string, which will be 'output', or in unusual cases (outputlen==0) will be NULL.
+ */
+const char *StripControlCodesEx(const char *text, char *output, size_t outputlen, int strip_flags)
+{
+	int i = 0, len = strlen(text), save_len=0;
+	char nc = 0, col = 0, rgb = 0;
+	char *o = output;
+	const char *save_text=NULL;
+
+	/* Handle special cases first.. */
+
+	if (outputlen == 0)
+		return NULL;
+
+	if (outputlen == 1)
+	{
+		*output = '\0';
+		return output;
+	}
+
+	/* Reserve room for the NUL byte */
+	outputlen--;
+
+	while (len > 0) 
+	{
+		if ( col && ((isdigit(*text) && nc < 2) || (*text == ',' && nc < 3)))
+		{
+			nc++;
+			if (*text == ',')
+				nc = 0;
+		}
+		/* Syntax for RGB is ^DHHHHHH where H is a hex digit.
+		 * If < 6 hex digits are specified, the code is displayed
+		 * as text
+		 */
+		else if ((rgb && isxdigit(*text) && nc < 6) || (rgb && *text == ',' && nc < 7))
+		{
+			nc++;
+			if (*text == ',')
+				nc = 0;
+		}
+		else 
+		{
+			if (col)
+				col = 0;
+			if (rgb)
+			{
+				if (nc != 6)
+				{
+					text = save_text+1;
+					len = save_len-1;
+					rgb = 0;
+					continue;
+				}
+				rgb = 0;
+			}
+			switch (*text)
+			{
+			case 3:
+				/* color */
+				col = 1;
+				nc = 0;
+				break;
+			case 4:
+				/* RGB */
+				save_text = text;
+				save_len = len;
+				rgb = 1;
+				nc = 0;
+				break;
+			case 2:
+				/* bold */
+				break;
+			case 31:
+				/* underline */
+				break;
+			case 22:
+				/* reverse */
+				break;
+			case 15:
+				/* plain */
+				break;
+			case 29:
+				/* italic */
+				break;
+			case 30:
+				/* strikethrough */
+				break;
+			case 17:
+				/* monospace */
+				break;
+			case 0xe2:
+				if (!strncmp(text+1, "\x80\x8b", 2))
+				{
+					/* +2 means we skip 3 */
+					text += 2;
+					len  -= 2;
+					break;
+				}
+				/*fallthrough*/
+			default:
+				if ((*text >= ' ') ||
+				    !(strip_flags & UNRL_STRIP_LOW_ASCII) ||
+				    ((strip_flags & UNRL_STRIP_KEEP_LF) && (*text == '\n'))
+				    )
+				{
+					*o++ = *text;
+					outputlen--;
+					if (outputlen == 0)
+					{
+						*o = '\0';
+						return output;
+					}
+				}
+				break;
+			}
+		}
+		text++;
+		len--;
+	}
+
+	*o = '\0';
+	return output;
+}
+
+/* strip color, bold, underline, and reverse codes from a string */
+const char *StripControlCodes(const char *text)
+{
+	static unsigned char new_str[4096];
+
+	return StripControlCodesEx(text, new_str, sizeof(new_str), 0);
+}
diff --git a/src/modulemanager.c b/src/modulemanager.c
@@ -1,6 +1,6 @@
 /* UnrealIRCd module manager.
  * (C) Copyright 2019 Bram Matthys ("Syzop") and the UnrealIRCd Team.
- * License: GPLv2
+ * License: GPLv2 or later
  * See https://www.unrealircd.org/docs/Module_manager for user documentation.
  */
 
@@ -1206,7 +1206,7 @@ int mm_compile(ManagedModule *m, char *tmpfile, int test)
 	fprintf(stderr, "ERROR: Compile errors encountered while compiling module '%s'\n"
 	                "You are suggested to contact the author (%s) of this module:\n%s\n",
 	                m->name, m->author, m->troubleshooting);
-	return 1;
+	return 0;
 }
 
 /** Actually download and install the module.
@@ -1222,7 +1222,7 @@ void mm_install_module(ManagedModule *m)
 		basename = "mod.c";
 	tmpfile = unreal_mktemp(TMPDIR, basename);
 
-	printf("ConfigResourceing %s from %s...\n", m->name, m->source);
+	printf("Downloading %s from %s...\n", m->name, m->source);
 	if (!mm_http_request(m->source, tmpfile, 1))
 	{
 		fprintf(stderr, "Repository %s seems to list a module file that cannot be retrieved (%s).\n", m->repo_url, m->source);
@@ -1655,21 +1655,23 @@ void mm_parse_c_file(int argc, char *args[])
 void mm_self_test(void)
 {
 	char name[512];
-	snprintf(name, sizeof(name), "%s/src/modules/third", BUILDDIR);
-	if (file_exists(name))
-		return;
+
 	if (!file_exists(BUILDDIR))
 	{
 		fprintf(stderr, "ERROR: Directory %s does not exist.\n"
 				"The UnrealIRCd source is required for the module manager to work!\n",
 				BUILDDIR);
+		exit(-1);
 	} else {
-		fprintf(stderr, "ERROR: Directory %s exists, but %s does not.\n"
-		                "The UnrealIRCd source is required for the module manager to work.\n"
-		                "It seems you only have a partial build directory??\n",
-		                BUILDDIR, name);
+		snprintf(name, sizeof(name), "%s/src/modules/third/Makefile", BUILDDIR);
+		if (!file_exists(name))
+		{
+			fprintf(stderr, "ERROR: Directory %s exists, but your UnrealIRCd is not compiled yet.\n"
+					"You must compile your UnrealIRCd first (run './Config', then 'make install')\n",
+					BUILDDIR);
+			exit(-1);
+		}
 	}
-	exit(-1);
 }
 
 void modulemanager(int argc, char *args[])
diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in
@@ -39,7 +39,7 @@ MODULES= \
 	sethost.so chghost.so chgident.so setname.so \
 	setident.so sdesc.so svsmode.so swhois.so\
 	svsmotd.so svsnline.so who_old.so whox.so mkpasswd.so \
-	away.so svsnoop.so svsnick.so \
+	away.so svsnoop.so svsnick.so svso.so \
 	chgname.so kill.so \
 	lag.so message.so oper.so pingpong.so \
 	quit.so sendumode.so sqline.so \
@@ -71,12 +71,12 @@ MODULES= \
 	message-tags.so batch.so \
 	account-tag.so labeled-response.so link-security.so \
 	message-ids.so plaintext-policy.so server-time.so sts.so \
-	echo-message.so userip-tag.so userhost-tag.so \
+	echo-message.so userip-tag.so userhost-tag.so geoip-tag.so \
 	bot-tag.so reply-tag.so json-log-tag.so \
-	typing-indicator.so \
+	typing-indicator.so channel-context.so \
 	ident_lookup.so history.so chathistory.so \
 	targetfloodprot.so clienttagdeny.so watch-backend.so \
-	monitor.so slog.so tls_cipher.so operinfo.so \
+	monitor.so slog.so tls_cipher.so operinfo.so creationtime.so \
 	unreal_server_compat.so \
 	extended-monitor.so geoip_csv.so \
 	geoip_base.so extjwt.so \
diff --git a/src/modules/antimixedutf8.c b/src/modules/antimixedutf8.c
@@ -18,6 +18,8 @@
  *                 ban-action block;
  *                 ban-reason "Possible mixed character spam";
  *                 ban-time 4h; // For other types
+ *                 except {
+ *                 }
  *         };
  * };
  *
@@ -56,6 +58,7 @@ struct {
 	BanAction ban_action;
 	char *ban_reason;
 	long ban_time;
+	SecurityGroup *except;
 } cfg;
 
 static void free_config(void);
@@ -196,9 +199,12 @@ CMD_OVERRIDE_FUNC(override_msg)
 {
 	int score, ret;
 	
-	if (!MyUser(client) || (parc < 3) || BadPtr(parv[2]))
+	if (!MyUser(client) || (parc < 3) || BadPtr(parv[2]) ||
+	    user_allowed_by_security_group(client, cfg.except))
 	{
-		/* Short circuit for: remote clients or insufficient parameters */
+		/* Short circuit for: remote clients, insufficient parameters,
+		 * antimixedutf8::except.
+		 */
 		CallCommandOverride(ovr, client, recv_mtags, parc, parv);
 		return;
 	}
@@ -271,6 +277,7 @@ static void init_config(void)
 static void free_config(void)
 {
 	safe_free(cfg.ban_reason);
+	free_security_group(cfg.except);
 	memset(&cfg, 0, sizeof(cfg)); /* needed! */
 }
 
@@ -319,6 +326,10 @@ int antimixedutf8_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *er
 		if (!strcmp(cep->name, "ban-time"))
 		{
 		} else
+		if (!strcmp(cep->name, "except"))
+		{
+			test_match_block(cf, cep, &errors);
+		} else
 		{
 			config_error("%s:%i: unknown directive set::antimixedutf8::%s",
 				cep->file->filename, cep->line_number, cep->name);
@@ -357,6 +368,10 @@ int antimixedutf8_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
 		if (!strcmp(cep->name, "ban-time"))
 		{
 			cfg.ban_time = config_checkval(cep->value, CFG_TIME);
+		} else
+		if (!strcmp(cep->name, "except"))
+		{
+			conf_match_block(cf, cep, &cfg.except);
 		}
 	}
 	return 1;
diff --git a/src/modules/antirandom.c b/src/modules/antirandom.c
@@ -25,7 +25,7 @@
 ModuleHeader MOD_HEADER
   = {
 	"antirandom",
-	"1.4",
+	"1.5",
 	"Detect and ban users with random names",
 	"UnrealIRCd Team",
 	"unrealircd-6",
@@ -504,8 +504,7 @@ struct {
 	long ban_time;
 	int convert_to_lowercase;
 	int show_failedconnects;
-	ConfigItem_mask *except_hosts;
-	int except_webirc;
+	SecurityGroup *except;
 } cfg;
 
 /* Forward declarations */
@@ -542,7 +541,8 @@ MOD_INIT()
 
 	/* Some default values: */
 	cfg.convert_to_lowercase = 1;
-	cfg.except_webirc = 1;
+	cfg.except = safe_alloc(sizeof(SecurityGroup));
+	cfg.except->webirc = 1;
 
 	return MOD_SUCCESS;
 }
@@ -562,7 +562,7 @@ MOD_UNLOAD()
 static void free_config(void)
 {
 	safe_free(cfg.ban_reason);
-	unreal_delete_masks(cfg.except_hosts);
+	free_security_group(cfg.except);
 	memset(&cfg, 0, sizeof(cfg)); /* needed! */
 }
 
@@ -580,6 +580,10 @@ int antirandom_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
 	
 	for (cep = ce->items; cep; cep = cep->next)
 	{
+		if (!strcmp(cep->name, "except"))
+		{
+			test_match_block(cf, cep, &errors);
+		} else
 		if (!strcmp(cep->name, "except-hosts"))
 		{
 		} else
@@ -653,14 +657,20 @@ int antirandom_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
 	
 	for (cep = ce->items; cep; cep = cep->next)
 	{
+		if (!strcmp(cep->name, "except"))
+		{
+			conf_match_block(cf, cep, &cfg.except);
+		} else
 		if (!strcmp(cep->name, "except-hosts"))
 		{
+			/* backwards compatible with set::antirandom::except */
 			for (cep2 = cep->items; cep2; cep2 = cep2->next)
-				unreal_add_masks(&cfg.except_hosts, cep2);
+				unreal_add_masks(&cfg.except->mask, cep2);
 		} else
 		if (!strcmp(cep->name, "except-webirc"))
 		{
-			cfg.except_webirc = config_checkval(cep->value, CFG_YESNO);
+			/* backwards compatible with set::antirandom::except */
+			cfg.except->webirc = config_checkval(cep->value, CFG_YESNO);
 		} else
 		if (!strcmp(cep->name, "threshold"))
 		{
@@ -863,14 +873,14 @@ int antirandom_preconnect(Client *client)
 		if (cfg.ban_action == BAN_ACT_WARN)
 		{
 			unreal_log(ULOG_INFO, "antirandom", "ANTIRANDOM_DENIED_USER", client,
-			           "[antirandom] would have denied access to user with score $score: $client:$client.info",
+			           "[antirandom] would have denied access to user with score $score: $client.details:$client.user.realname",
 			           log_data_integer("score", score));
 			return HOOK_CONTINUE;
 		}
 		if (cfg.show_failedconnects)
 		{
 			unreal_log(ULOG_INFO, "antirandom", "ANTIRANDOM_DENIED_USER", client,
-			           "[antirandom] denied access to user with score $score: $client:$client.info",
+			           "[antirandom] denied access to user with score $score: $client.details:$client.user.realname",
 			           log_data_integer("score", score));
 		}
 		place_host_ban(client, cfg.ban_action, cfg.ban_reason, cfg.ban_time);
@@ -894,13 +904,8 @@ static void free_stuff(void)
 /** Is this user exempt from antirandom interventions? */
 static int is_exempt(Client *client)
 {
-	/* WEBIRC gateway and exempt? */
-	if (cfg.except_webirc)
-	{
-		const char *val = moddata_client_get(client, "webirc");
-		if (val && (atoi(val)>0))
-			return 1;
-	}
+	if (user_allowed_by_security_group(client, cfg.except))
+		return 1;
 
 	if (find_tkl_exception(TKL_ANTIRANDOM, client))
 		return 1;
@@ -909,6 +914,5 @@ static int is_exempt(Client *client)
 	if (IsSoftBanAction(cfg.ban_action) && IsLoggedIn(client))
 		return 1;
 
-	/* On except host? */
-	return unreal_mask_match(client, cfg.except_hosts);
+	return 0;
 }
diff --git a/src/modules/blacklist.c b/src/modules/blacklist.c
@@ -67,6 +67,7 @@ struct Blacklist {
 	int action;
 	long ban_time;
 	char *reason;
+	SecurityGroup *except;
 };
 
 /* Blacklist user struct. In the c-ares DNS reply callback we need to pass
@@ -236,6 +237,8 @@ void delete_blacklist_block(Blacklist *e)
 
 	safe_free(e->name);
 	safe_free(e->reason);
+	free_security_group(e->except);
+
 	safe_free(e);
 }
 
@@ -356,6 +359,10 @@ int blacklist_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
 				}
 			}
 		} else
+		if (!strcmp(cep->name, "except"))
+		{
+			test_match_block(cf, cep, &errors);
+		} else
 		if (!cep->value)
 		{
 			config_error_empty(cep->file->filename, cep->line_number,
@@ -388,8 +395,8 @@ int blacklist_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
 				continue;
 			}
 			has_ban_time = 1;
-		} else
-		if (!strcmp(cep->name, "reason"))
+		}
+		else if (!strcmp(cep->name, "reason"))
 		{
 			if (has_reason)
 			{
@@ -524,13 +531,17 @@ int blacklist_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
 		{
 			d->action = banact_stringtoval(cep->value);
 		}
+		else if (!strcmp(cep->name, "ban-time"))
+		{
+			d->ban_time = config_checkval(cep->value, CFG_TIME);
+		}
 		else if (!strcmp(cep->name, "reason"))
 		{
 			safe_strdup(d->reason, cep->value);
 		}
-		else if (!strcmp(cep->name, "ban-time"))
+		else if (!strcmp(cep->name, "except"))
 		{
-			d->ban_time = config_checkval(cep->value, CFG_TIME);
+			conf_match_block(cf, cep, &d->except);
 		}
 	}
 
@@ -574,7 +585,7 @@ int blacklist_start_check(Client *client)
 		SetNoHandshakeDelay(client);
 		return 0;
 	}
-	
+
 	if (!BLUSER(client))
 	{
 		SetBLUser(client, safe_alloc(sizeof(BLUser)));
@@ -587,6 +598,10 @@ int blacklist_start_check(Client *client)
 		if (!BLUSER(client))
 			break;
 
+		/* Check if user is exempt (then don't bother checking) */
+		if (user_allowed_by_security_group(client, bl->except))
+			continue;
+
 		/* Initiate blacklist requests */
 		if (bl->backend_type == BLACKLIST_BACKEND_DNS)
 			blacklist_dns_request(client, bl);
diff --git a/src/modules/bot-tag.c b/src/modules/bot-tag.c
@@ -46,6 +46,12 @@ MOD_INIT()
 	MARK_AS_OFFICIAL_MODULE(modinfo);
 
 	memset(&mtag, 0, sizeof(mtag));
+	mtag.name = "bot";
+	mtag.is_ok = bottag_mtag_is_ok;
+	mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+	MessageTagHandlerAdd(modinfo->handle, &mtag);
+
+	memset(&mtag, 0, sizeof(mtag));
 	mtag.name = "draft/bot";
 	mtag.is_ok = bottag_mtag_is_ok;
 	mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
@@ -82,7 +88,14 @@ void mtag_add_bottag(Client *client, MessageTag *recv_mtags, MessageTag **mtag_l
 
 	if (IsUser(client) && has_user_mode(client, 'B'))
 	{
-		MessageTag *m = safe_alloc(sizeof(MessageTag));
+		MessageTag *m;
+
+		m = safe_alloc(sizeof(MessageTag));
+		safe_strdup(m->name, "bot");
+		m->value = NULL;
+		AddListItem(m, *mtag_list);
+
+		m = safe_alloc(sizeof(MessageTag));
 		safe_strdup(m->name, "draft/bot");
 		m->value = NULL;
 		AddListItem(m, *mtag_list);
diff --git a/src/modules/certfp.c b/src/modules/certfp.c
@@ -6,7 +6,7 @@
  *
  * (C) Copyright 2014-2015 The UnrealIRCd team (Syzop, DBoyz, Nath and others)
  * 
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -27,6 +27,7 @@ void certfp_unserialize(const char *str, ModData *m);
 int certfp_handshake(Client *client);
 int certfp_connect(Client *client);
 int certfp_whois(Client *client, Client *target, NameValuePrioList **list);
+int certfp_log(Client *client, int detail, json_t *j);
 
 ModDataInfo *certfp_md; /* Module Data structure which we acquire */
 
@@ -51,6 +52,7 @@ MOD_INIT()
 	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, certfp_handshake);
 	HookAdd(modinfo->handle, HOOKTYPE_SERVER_HANDSHAKE_OUT, 0, certfp_handshake);
 	HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, certfp_whois);
+	HookAdd(modinfo->handle, HOOKTYPE_JSON_EXPAND_CLIENT, 0, certfp_log);
 
 	return MOD_SUCCESS;
 }
@@ -158,3 +160,24 @@ void certfp_unserialize(const char *str, ModData *m)
 {
 	safe_strdup(m->str, str);
 }
+
+int certfp_log(Client *client, int detail, json_t *j)
+{
+	json_t *tls;
+	const char *str;
+
+	str = moddata_client_get(client, "certfp");
+	if (!str)
+		return 0;
+
+	tls = json_object_get(j, "tls");
+	if (!tls)
+	{
+		tls = json_object();
+		json_object_set_new(j, "tls", tls);
+	}
+
+	json_object_set_new(tls, "certfp", json_string_unreal(str));
+
+	return 0;
+}
diff --git a/src/modules/chanmodes/floodprot.c b/src/modules/chanmodes/floodprot.c
@@ -110,7 +110,7 @@ struct ChannelFloodProtection {
 /* Global variables */
 ModDataInfo *mdflood = NULL;
 Cmode_t EXTMODE_FLOODLIMIT = 0L;
-static int timedban_available = 0; /**< Set to 1 if extbans/timedban module is loaded. */
+static int timedban_available = 1; /**< Set to 1 if extbans/timedban module is loaded. Assumed 1 during config load due to set::modes-on-join race. */
 RemoveChannelModeTimer *removechannelmodetimer_list = NULL;
 char *floodprot_msghash_key = NULL;
 
diff --git a/src/modules/chanmodes/history.c b/src/modules/chanmodes/history.c
@@ -1,7 +1,7 @@
 /*
  * modules/chanmodes/history - Channel History
  * (C) Copyright 2009-2019 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 #include "unrealircd.h"
 
diff --git a/src/modules/chanmodes/operonly.c b/src/modules/chanmodes/operonly.c
@@ -35,7 +35,7 @@ Cmode_t EXTCMODE_OPERONLY;
 int operonly_require_oper(Client *client, Channel *channel, char mode, const char *para, int checkt, int what);
 int operonly_can_join(Client *client, Channel *channel, const char *key, char **errmsg);
 int operonly_view_topic_outside_channel(Client *client, Channel *channel);
-int operonly_oper_invite_ban(Client *client, Channel *channel);
+int operonly_invite_bypass(Client *client, Channel *channel);
 
 MOD_TEST()
 {
@@ -53,7 +53,7 @@ CmodeInfo req;
 	CmodeAdd(modinfo->handle, req, &EXTCMODE_OPERONLY);
 	
 	HookAdd(modinfo->handle, HOOKTYPE_CAN_JOIN, 0, operonly_can_join);
-	HookAdd(modinfo->handle, HOOKTYPE_OPER_INVITE_BAN, 0, operonly_oper_invite_ban);
+	HookAdd(modinfo->handle, HOOKTYPE_INVITE_BYPASS, 0, operonly_invite_bypass);
 	HookAdd(modinfo->handle, HOOKTYPE_VIEW_TOPIC_OUTSIDE_CHANNEL, 0, operonly_view_topic_outside_channel);
 
 	
@@ -81,10 +81,9 @@ int operonly_can_join(Client *client, Channel *channel, const char *key, char **
 	return 0;
 }
 
-int operonly_oper_invite_ban(Client *client, Channel *channel)
+int operonly_invite_bypass(Client *client, Channel *channel)
 {
-	 if ((channel->mode.mode & EXTCMODE_OPERONLY) &&
-		    !ValidatePermissionsForPath("channel:operonly:ban",client,NULL,NULL,NULL))
+	 if ((channel->mode.mode & EXTCMODE_OPERONLY) && !ValidatePermissionsForPath("channel:operonly:ban",client,NULL,NULL,NULL))
 		 return HOOK_DENY;
 
 	 return HOOK_CONTINUE;
diff --git a/src/modules/channel-context.c b/src/modules/channel-context.c
@@ -0,0 +1,95 @@
+/*
+ *   IRC - Internet Relay Chat, src/modules/channel-context.c
+ *   (C) 2022 Valware & The UnrealIRCd Team
+ *
+ *   See file AUTHORS in IRC package for additional names of
+ *   the programmers.
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 1, or (at your option)
+ *   any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+  = {
+	"channel-context",
+	"1.0",
+	"Channel Context (IRCv3)",
+	"UnrealIRCd team",
+	"unrealircd-6",
+	};
+
+int chancontext_mtag_is_ok(Client *client, const char *name, const char *value);
+void mtag_add_chancontext(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature);
+
+MOD_INIT()
+{
+	MessageTagHandlerInfo mtag;
+
+	MARK_AS_OFFICIAL_MODULE(modinfo);
+
+	memset(&mtag, 0, sizeof(mtag));
+	mtag.is_ok = chancontext_mtag_is_ok;
+	mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+	mtag.name = "+draft/channel-context";
+	MessageTagHandlerAdd(modinfo->handle, &mtag);
+
+	HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_chancontext);
+
+	return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+	return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+	return MOD_SUCCESS;
+}
+
+int chancontext_mtag_is_ok(Client *client, const char *name, const char *value)
+{
+	if (BadPtr(value))
+		return 0;
+
+	/* Validate a bit further, but only for local users.. */
+	if (MyUser(client))
+	{
+		Channel *channel = find_channel(value);
+		if (!channel)
+			return 0;
+		if (!IsMember(client, channel))
+			return 0;
+	}
+
+	return 1;
+}
+
+void mtag_add_chancontext(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature)
+{
+	MessageTag *m;
+
+	if (IsUser(client))
+	{
+		m = find_mtag(recv_mtags, "+draft/channel-context");
+		if (m)
+		{
+			m = duplicate_mtag(m);
+			AddListItem(m, *mtag_list);
+		}
+	}
+}
diff --git a/src/modules/channeldb.c b/src/modules/channeldb.c
@@ -1,7 +1,7 @@
 /*
  * Stores channel settings for +P channels in a .db file
  * (C) Copyright 2019 Syzop, Gottem and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -359,6 +359,14 @@ int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel)
 	return 1;
 }
 
+int ban_exists(Ban *lst, Ban *e)
+{
+	for (; lst; lst = lst->next)
+		if (!mycmp(lst->banstr, e->banstr))
+			return 1;
+	return 0;
+}
+
 #define R_SAFE(x) \
 	do { \
 		if (!(x)) { \
@@ -401,10 +409,18 @@ int read_listmode(UnrealDB *db, Ban **lst)
 		}
 		safe_strdup(e->banstr, str);
 
-		/* Add to list */
-		e->when = when;
-		e->next = *lst;
-		*lst = e;
+		if (ban_exists(*lst, e))
+		{
+			/* Free again - duplicate item */
+			safe_free(e->banstr);
+			safe_free(e->who);
+			safe_free(e);
+		} else {
+			/* Add to list */
+			e->when = when;
+			e->next = *lst;
+			*lst = e;
+		}
 	}
 
 	return 1;
diff --git a/src/modules/chathistory.c b/src/modules/chathistory.c
@@ -1,6 +1,6 @@
 /* src/modules/chathistory.c - IRCv3 CHATHISTORY command.
  * (C) Copyright 2021 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  *
  * This implements the "CHATHISTORY" command, the CAP and 005 token.
  * https://ircv3.net/specs/extensions/chathistory
diff --git a/src/modules/chghost.c b/src/modules/chghost.c
@@ -231,6 +231,9 @@ void _userhost_changed(Client *client)
 		if (HasCapabilityFast(client, CAP_CHGHOST))
 			sendto_one(client, NULL, "%s", buf);
 
+		if (MyUser(client))
+			sendnumeric(client, RPL_HOSTHIDDEN, GetHost(client));
+
 		/* A userhost change always generates the following network traffic:
 		 * server to server traffic, CAP "chghost" notifications, and
 		 * possibly PART+JOIN+MODE if force-rejoin had work to do.
@@ -338,7 +341,4 @@ CMD_FUNC(cmd_chghost)
 	safe_strdup(target->user->virthost, parv[2]);
 	
 	userhost_changed(target);
-
-	if (MyUser(target))
-		sendnumeric(target, RPL_HOSTHIDDEN, parv[2]);
 }
diff --git a/src/modules/connect.c b/src/modules/connect.c
@@ -106,10 +106,10 @@ CMD_FUNC(cmd_connect)
 		return;
 	}
 
-	if (!aconf->outgoing.hostname)
+	if (!aconf->outgoing.hostname && !aconf->outgoing.file)
 	{
 		sendnotice(client,
-		    "*** Connect: Server %s is not configured to be an outgoing link (has a link block, but no link::outgoing::hostname)",
+		    "*** Connect: Server %s is not configured to be an outgoing link (has a link block, but no link::outgoing::hostname or link::outgoing::file)",
 		    parv[1]);
 		return;
 	}
diff --git a/src/modules/connthrottle.c b/src/modules/connthrottle.c
@@ -1,7 +1,7 @@
 /*
  * connthrottle - Connection throttler
  * (C) Copyright 2004-2020 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  * See https://www.unrealircd.org/docs/Connthrottle
  */
 
@@ -31,10 +31,8 @@ struct cfgstruct {
 	/* set::connthrottle::known-users: */
 	ThrottleSetting local;
 	ThrottleSetting global;
-	/* set::connthrottle::new-users: */
-	int minimum_reputation_score;
-	int sasl_bypass;
-	int webirc_bypass;
+	/* set::connthrottle::except: */
+	SecurityGroup *except;
 	/* set::connthrottle::disabled-when: */
 	long reputation_gathering;
 	int start_delay;
@@ -53,10 +51,8 @@ struct UCounter {
 	ThrottleCounter local;		/**< Local counter */
 	ThrottleCounter global;		/**< Global counter */
 	int rejected_clients;		/**< Number of rejected clients this minute */
-	int allowed_score;		/**< Number of allowed clients of type known-user */
-	int allowed_sasl;		/**< Number of allowed clients of type SASL */
-	int allowed_webirc;		/**< Number of allowed clients of type WEBIRC */
-	int allowed_other;		/**< Number of allowed clients of type other (new) */
+	int allowed_except;		/**< Number of allowed clients - on except list */
+	int allowed_unknown_users;	/**< Number of allowed clients - not on except list */
 	char disabled;			/**< Module disabled by oper? */
 	int throttling_this_minute;	/**< Did we do any throttling this minute? */
 	int throttling_previous_minute;	/**< Did we do any throttling previous minute? */
@@ -87,9 +83,10 @@ MOD_TEST()
 	cfg.global.count = 30; cfg.global.period = 60;
 	cfg.start_delay = 180;		/* 3 minutes */
 	safe_strdup(cfg.reason, "Throttled: Too many users trying to connect, please wait a while and try again");
-	cfg.minimum_reputation_score = 24;
-	cfg.sasl_bypass = 1;
-	cfg.webirc_bypass = 0;
+	cfg.except = safe_alloc(sizeof(SecurityGroup));
+	cfg.except->reputation_score = 24;
+	cfg.except->identified = 1;
+	cfg.except->webirc = 0;
 
 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, ct_config_test);
 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, ct_config_posttest);
@@ -120,6 +117,7 @@ MOD_UNLOAD()
 {
 	SavePersistentPointer(modinfo, ucounter);
 	safe_free(cfg.reason);
+	free_security_group(cfg.except);
 	return MOD_SUCCESS;
 }
 
@@ -161,6 +159,10 @@ int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
 	
 	for (cep = ce->items; cep; cep = cep->next)
 	{
+		if (!strcmp(cep->name, "except"))
+		{
+			test_match_block(cf, cep, &errors);
+		} else
 		if (!strcmp(cep->name, "known-users"))
 		{
 			for (cepp = cep->items; cepp; cepp = cepp->next)
@@ -286,16 +288,20 @@ int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
 	
 	for (cep = ce->items; cep; cep = cep->next)
 	{
+		if (!strcmp(cep->name, "except"))
+		{
+			conf_match_block(cf, cep, &cfg.except);
+		} else
 		if (!strcmp(cep->name, "known-users"))
 		{
 			for (cepp = cep->items; cepp; cepp = cepp->next)
 			{
 				if (!strcmp(cepp->name, "minimum-reputation-score"))
-					cfg.minimum_reputation_score = atoi(cepp->value);
+					cfg.except->reputation_score = atoi(cepp->value);
 				else if (!strcmp(cepp->name, "sasl-bypass"))
-					cfg.sasl_bypass = config_checkval(cepp->value, CFG_YESNO);
+					cfg.except->identified = config_checkval(cepp->value, CFG_YESNO);
 				else if (!strcmp(cepp->name, "webirc-bypass"))
-					cfg.webirc_bypass = config_checkval(cepp->value, CFG_YESNO);
+					cfg.except->webirc = config_checkval(cepp->value, CFG_YESNO);
 			}
 		} else
 		if (!strcmp(cep->name, "new-users"))
@@ -360,23 +366,17 @@ EVENT(connthrottle_evt)
 		unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTLE_REPORT", NULL,
 		           "ConnThrottle] Stats for this server past 60 secs: "
 		           "Connections rejected: $num_rejected. "
-		           "Accepted: $num_accepted_known_users known user(s), "
-		           "$num_accepted_sasl SASL, "
-		           "$num_accepted_webirc WEBIRC and "
+		           "Accepted: $num_accepted_except except user(s) and "
 		           "$num_accepted_unknown_users new user(s).",
 		           log_data_integer("num_rejected", ucounter->rejected_clients),
-		           log_data_integer("num_accepted_known_users", ucounter->allowed_score),
-		           log_data_integer("num_accepted_sasl", ucounter->allowed_sasl),
-		           log_data_integer("num_accepted_webirc", ucounter->allowed_webirc),
-		           log_data_integer("num_accepted_unknown_users", ucounter->allowed_other));
+		           log_data_integer("num_accepted_except", ucounter->allowed_except),
+		           log_data_integer("num_accepted_unknown_users", ucounter->allowed_unknown_users));
 	}
 
 	/* Reset stats for next message */
 	ucounter->rejected_clients = 0;
-	ucounter->allowed_score = 0;
-	ucounter->allowed_sasl = 0;
-	ucounter->allowed_webirc = 0;
-	ucounter->allowed_other = 0;
+	ucounter->allowed_except = 0;
+	ucounter->allowed_unknown_users = 0;
 
 	ucounter->throttling_previous_minute = ucounter->throttling_this_minute;
 	ucounter->throttling_this_minute = 0; /* reset */
@@ -399,24 +399,8 @@ int ct_pre_lconnect(Client *client)
 	if (still_reputation_gathering())
 		return HOOK_CONTINUE; /* still gathering reputation data */
 
-	if (cfg.sasl_bypass && IsLoggedIn(client))
-	{
-		/* Allowed in: user authenticated using SASL */
-		return HOOK_CONTINUE;
-	}
-	
-	if (cfg.webirc_bypass && moddata_client_get(client, "webirc"))
-	{
-		/* Allowed in: user using WEBIRC */
-		return HOOK_CONTINUE;
-	}
-
-	score = GetReputation(client);
-	if (score >= cfg.minimum_reputation_score)
-	{
-		/* Allowed in: IP has enough reputation ("known user") */
-		return HOOK_CONTINUE;
-	}
+	if (user_allowed_by_security_group(client, cfg.except))
+		return HOOK_CONTINUE; /* allowed: user is exempt (known user or otherwise) */
 
 	/* If we reach this then the user is NEW */
 
@@ -486,30 +470,14 @@ int ct_lconnect(Client *client)
 	if (still_reputation_gathering())
 		return 0; /* still gathering reputation data */
 
-	if (cfg.sasl_bypass && IsLoggedIn(client))
-	{
-		/* Allowed in: user authenticated using SASL */
-		ucounter->allowed_sasl++;
-		return 0;
-	}
-	
-	if (cfg.webirc_bypass && moddata_client_get(client, "webirc"))
-	{
-		/* Allowed in: user using WEBIRC */
-		ucounter->allowed_webirc++;
-		return 0;
-	}
-
-	score = GetReputation(client);
-	if (score >= cfg.minimum_reputation_score)
+	if (user_allowed_by_security_group(client, cfg.except))
 	{
-		/* Allowed in: IP has enough reputation ("known user") */
-		ucounter->allowed_score++;
-		return 0;
+		ucounter->allowed_except++;
+		return HOOK_CONTINUE; /* allowed: user is exempt (known user or otherwise) */
 	}
 
 	/* Allowed NEW user */
-	ucounter->allowed_other++;
+	ucounter->allowed_unknown_users++;
 
 	bump_connect_counter(1);
 
@@ -539,9 +507,8 @@ int ct_rconnect(Client *client)
 	}
 #endif
 
-	score = GetReputation(client);
-	if (score >= cfg.minimum_reputation_score)
-		return 0; /* sufficient reputation: "known-user" */
+	if (user_allowed_by_security_group(client, cfg.except))
+		return 0; /* user is on except list (known user or otherwise) */
 
 	bump_connect_counter(0);
 
diff --git a/src/modules/creationtime.c b/src/modules/creationtime.c
@@ -0,0 +1,99 @@
+/*
+ * Store creationtime in ModData
+ * (C) Copyright 2022-.. Syzop and The UnrealIRCd Team
+ * License: GPLv2 or later
+ */
+
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+  = {
+	"creationtime",
+	"6.0",
+	"Store and retrieve creation time of clients",
+	"UnrealIRCd Team",
+	"unrealircd-6",
+    };
+
+/* Forward declarations */
+void creationtime_free(ModData *m);
+const char *creationtime_serialize(ModData *m);
+void creationtime_unserialize(const char *str, ModData *m);
+int creationtime_handshake(Client *client);
+int creationtime_welcome_user(Client *client, int numeric);
+int creationtime_whois(Client *client, Client *target);
+
+ModDataInfo *creationtime_md; /* Module Data structure which we acquire */
+
+#define SetCreationTime(x,y)	do { moddata_client(x, creationtime_md).ll = y; } while(0)
+
+MOD_INIT()
+{
+	ModDataInfo mreq;
+
+	MARK_AS_OFFICIAL_MODULE(modinfo);
+
+	memset(&mreq, 0, sizeof(mreq));
+	mreq.name = "creationtime";
+	mreq.free = creationtime_free;
+	mreq.serialize = creationtime_serialize;
+	mreq.unserialize = creationtime_unserialize;
+	mreq.sync = MODDATA_SYNC_EARLY;
+	mreq.type = MODDATATYPE_CLIENT;
+	creationtime_md = ModDataAdd(modinfo->handle, mreq);
+	if (!creationtime_md)
+		abort();
+
+	/* This event sets creationtime very early: on handshake in and out */
+	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, creationtime_handshake);
+	HookAdd(modinfo->handle, HOOKTYPE_SERVER_HANDSHAKE_OUT, 0, creationtime_handshake);
+
+	/* And this event (re)sets it because that also happens in
+	 * welcome_user() in nick.c regarding #2174
+	 */
+	HookAdd(modinfo->handle, HOOKTYPE_WELCOME, 0, creationtime_welcome_user);
+
+	return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+	return MOD_SUCCESS;
+}
+
+
+MOD_UNLOAD()
+{
+	return MOD_SUCCESS;
+}
+
+int creationtime_handshake(Client *client)
+{
+	SetCreationTime(client, client->local->creationtime);
+	return 0;
+}
+
+int creationtime_welcome_user(Client *client, int numeric)
+{
+	if (numeric == 0)
+		SetCreationTime(client, client->local->creationtime);
+	return 0;
+}
+
+void creationtime_free(ModData *m)
+{
+	m->ll = 0;
+}
+
+const char *creationtime_serialize(ModData *m)
+{
+	static char buf[64];
+
+	snprintf(buf, sizeof(buf), "%lld", (long long)m->ll);
+	return buf;
+}
+
+void creationtime_unserialize(const char *str, ModData *m)
+{
+	m->ll = atoll(str);
+}
diff --git a/src/modules/extbans/account.c b/src/modules/extbans/account.c
@@ -31,11 +31,10 @@ ModuleHeader MOD_HEADER
 const char *extban_account_conv_param(BanContext *b, Extban *extban);
 int extban_account_is_banned(BanContext *b);
 
-/** Called upon module init */
-MOD_INIT()
+Extban *register_account_extban(ModuleInfo *modinfo)
 {
 	ExtbanInfo req;
-	
+
 	memset(&req, 0, sizeof(req));
 	req.letter = 'a';
 	req.name = "account";
@@ -44,14 +43,30 @@ MOD_INIT()
 	req.is_banned = extban_account_is_banned;
 	req.is_banned_events = BANCHK_ALL|BANCHK_TKL;
 	req.options = EXTBOPT_INVEX|EXTBOPT_TKL;
-	if (!ExtbanAdd(modinfo->handle, req))
+	return ExtbanAdd(modinfo->handle, req);
+}
+
+/** Called upon module test */
+MOD_TEST()
+{
+	if (!register_account_extban(modinfo))
+	{
+		config_error("could not register extended ban type");
+		return MOD_FAILED;
+	}
+	return MOD_SUCCESS;
+}
+
+/** Called upon module init */
+MOD_INIT()
+{
+	if (!register_account_extban(modinfo))
 	{
 		config_error("could not register extended ban type");
 		return MOD_FAILED;
 	}
 
 	MARK_AS_OFFICIAL_MODULE(modinfo);
-	
 	return MOD_SUCCESS;
 }
 
@@ -95,7 +110,7 @@ int extban_account_is_banned(BanContext *b)
 	if (!strcmp(b->banstr, "*") && IsLoggedIn(b->client))
 		return 1;
 
-	if (match_simple(b->banstr, b->client->user->account))
+	if (b->client->user && match_simple(b->banstr, b->client->user->account))
 		return 1;
 
 	return 0;
diff --git a/src/modules/extbans/certfp.c b/src/modules/extbans/certfp.c
@@ -32,11 +32,10 @@ int extban_certfp_is_ok(BanContext *b);
 const char *extban_certfp_conv_param(BanContext *b, Extban *extban);
 int extban_certfp_is_banned(BanContext *b);
 
-/* Called upon module init */
-MOD_INIT()
+Extban *register_certfp_extban(ModuleInfo *modinfo)
 {
 	ExtbanInfo req;
-	
+
 	memset(&req, 0, sizeof(req));
 	req.letter = 'S';
 	req.name = "certfp";
@@ -45,14 +44,32 @@ MOD_INIT()
 	req.is_banned = extban_certfp_is_banned;
 	req.is_banned_events = BANCHK_ALL|BANCHK_TKL;
 	req.options = EXTBOPT_INVEX|EXTBOPT_TKL;
-	if (!ExtbanAdd(modinfo->handle, req))
+	return ExtbanAdd(modinfo->handle, req);
+}
+
+/* Called upon module test */
+MOD_TEST()
+{
+	if (!register_certfp_extban(modinfo))
+	{
+		config_error("could not register extended ban type");
+		return MOD_FAILED;
+	}
+
+	return MOD_SUCCESS;
+}
+
+/* Called upon module init */
+MOD_INIT()
+{
+	if (!register_certfp_extban(modinfo))
 	{
 		config_error("could not register extended ban type");
 		return MOD_FAILED;
 	}
 
 	MARK_AS_OFFICIAL_MODULE(modinfo);
-	
+
 	return MOD_SUCCESS;
 }
 
@@ -82,10 +99,10 @@ int extban_certfp_is_ok(BanContext *b)
 	if (b->is_ok_check == EXCHK_PARAM)
 	{
 		const char *p;
-		
+
 		if (strlen(b->banstr) != CERT_FP_LEN)
 			return extban_certfp_usage(b->client);
-		
+
 		for (p = b->banstr; *p; p++)
 			if (!isxdigit(*p))
 				return extban_certfp_usage(b->client);
@@ -100,9 +117,9 @@ const char *extban_certfp_conv_param(BanContext *b, Extban *extban)
 {
 	static char retbuf[EVP_MAX_MD_SIZE * 2 + 1];
 	char *p;
-	
+
 	strlcpy(retbuf, b->banstr, sizeof(retbuf));
-	
+
 	for (p = retbuf; *p; p++)
 	{
 		*p = tolower(*p);
diff --git a/src/modules/extbans/country.c b/src/modules/extbans/country.c
@@ -32,8 +32,7 @@ int extban_country_is_ok(BanContext *b);
 const char *extban_country_conv_param(BanContext *b, Extban *extban);
 int extban_country_is_banned(BanContext *b);
 
-/* Called upon module init */
-MOD_INIT()
+Extban *register_country_extban(ModuleInfo *modinfo)
 {
 	ExtbanInfo req;
 
@@ -45,7 +44,25 @@ MOD_INIT()
 	req.is_banned = extban_country_is_banned;
 	req.is_banned_events = BANCHK_ALL|BANCHK_TKL;
 	req.options = EXTBOPT_INVEX|EXTBOPT_TKL;
-	if (!ExtbanAdd(modinfo->handle, req))
+	return ExtbanAdd(modinfo->handle, req);
+}
+
+/* Called upon module test */
+MOD_TEST()
+{
+	if (!register_country_extban(modinfo))
+	{
+		config_error("could not register extended ban type");
+		return MOD_FAILED;
+	}
+
+	return MOD_SUCCESS;
+}
+
+/* Called upon module init */
+MOD_INIT()
+{
+	if (!register_country_extban(modinfo))
 	{
 		config_error("could not register extended ban type");
 		return MOD_FAILED;
diff --git a/src/modules/extbans/realname.c b/src/modules/extbans/realname.c
@@ -31,11 +31,10 @@ ModuleHeader MOD_HEADER
 const char *extban_realname_conv_param(BanContext *b, Extban *extban);
 int extban_realname_is_banned(BanContext *b);
 
-/** Called upon module init */
-MOD_INIT()
+Extban *register_realname_extban(ModuleInfo *modinfo)
 {
 	ExtbanInfo req;
-	
+
 	memset(&req, 0, sizeof(req));
 	req.letter = 'r';
 	req.name = "realname";
@@ -44,14 +43,30 @@ MOD_INIT()
 	req.is_banned = extban_realname_is_banned;
 	req.is_banned_events = BANCHK_ALL|BANCHK_TKL;
 	req.options = EXTBOPT_INVEX|EXTBOPT_TKL;
-	if (!ExtbanAdd(modinfo->handle, req))
+	return ExtbanAdd(modinfo->handle, req);
+}
+
+/** Called upon module test */
+MOD_TEST()
+{
+	if (!register_realname_extban(modinfo))
+	{
+		config_error("could not register extended ban type");
+		return MOD_FAILED;
+	}
+	return MOD_SUCCESS;
+}
+
+/** Called upon module init */
+MOD_INIT()
+{
+	if (!register_realname_extban(modinfo))
 	{
 		config_error("could not register extended ban type");
 		return MOD_FAILED;
 	}
 
 	MARK_AS_OFFICIAL_MODULE(modinfo);
-	
 	return MOD_SUCCESS;
 }
 
diff --git a/src/modules/extbans/securitygroup.c b/src/modules/extbans/securitygroup.c
@@ -32,8 +32,7 @@ const char *extban_securitygroup_conv_param(BanContext *b, Extban *extban);
 int extban_securitygroup_is_ok(BanContext *b);
 int extban_securitygroup_is_banned(BanContext *b);
 
-/** Called upon module init */
-MOD_INIT()
+Extban *register_securitygroup_extban(ModuleInfo *modinfo)
 {
 	ExtbanInfo req;
 
@@ -45,7 +44,25 @@ MOD_INIT()
 	req.is_banned = extban_securitygroup_is_banned;
 	req.is_banned_events = BANCHK_ALL|BANCHK_TKL;
 	req.options = EXTBOPT_INVEX|EXTBOPT_TKL;
-	if (!ExtbanAdd(modinfo->handle, req))
+	return ExtbanAdd(modinfo->handle, req);
+}
+
+/** Called upon module test */
+MOD_TEST()
+{
+	if (!register_securitygroup_extban(modinfo))
+	{
+		config_error("could not register extended ban type ~G");
+		return MOD_FAILED;
+	}
+
+	return MOD_SUCCESS;
+}
+
+/** Called upon module init */
+MOD_INIT()
+{
+	if (!register_securitygroup_extban(modinfo))
 	{
 		config_error("could not register extended ban type ~G");
 		return MOD_FAILED;
diff --git a/src/modules/extbans/timedban.c b/src/modules/extbans/timedban.c
@@ -1,7 +1,7 @@
 /*
  * timedban - Timed bans that are automatically unset.
  * (C) Copyright 2009-2017 Bram Matthys (Syzop) and the UnrealIRCd team.
- * License: GPLv2
+ * License: GPLv2 or later
  *
  * This module adds an extended ban ~t:time:mask
  * Where 'time' is the time in minutes after which the ban will be removed.
diff --git a/src/modules/extended-monitor.c b/src/modules/extended-monitor.c
@@ -49,7 +49,7 @@ MOD_INIT()
 	ModDataInfo mreq;
 
 	memset(&cap, 0, sizeof(cap));
-	cap.name = "draft/extended-monitor";
+	cap.name = "extended-monitor";
 	c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_EXTENDED_MONITOR);
 	if (!c)
 	{
diff --git a/src/modules/geoip-tag.c b/src/modules/geoip-tag.c
@@ -0,0 +1,108 @@
+/*
+ *   IRC - Internet Relay Chat, src/modules/geoip-tag.c
+ *   (C) 2022 westor, Syzop and The UnrealIRCd Team
+ *
+ *   See file AUTHORS in IRC package for additional names of
+ *   the programmers.
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 1, or (at your option)
+ *   any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+  = {
+	"geoip-tag",
+	"6.0",
+	"geoip message tag",
+	"UnrealIRCd Team",
+	"unrealircd-6",
+	};
+
+/* Forward declarations */
+int geoip_mtag_is_ok(Client *client, const char *name, const char *value);
+int geoip_mtag_should_send_to_client(Client *target);
+void mtag_add_geoip(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature);
+
+MOD_INIT()
+{
+	MessageTagHandlerInfo mtag;
+
+	MARK_AS_OFFICIAL_MODULE(modinfo);
+
+	memset(&mtag, 0, sizeof(mtag));
+	mtag.name = "unrealircd.org/geoip";
+	mtag.is_ok = geoip_mtag_is_ok;
+	mtag.should_send_to_client = geoip_mtag_should_send_to_client;
+	mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+	MessageTagHandlerAdd(modinfo->handle, &mtag);
+
+	HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_geoip);
+
+	return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+	return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+	return MOD_SUCCESS;
+}
+
+/** This function verifies if the client sending
+ * 'geoip-tag' is permitted to do so and uses a permitted
+ * syntax.
+ * We simply allow geoip-tag ONLY from servers and with any syntax.
+ */
+int geoip_mtag_is_ok(Client *client, const char *name, const char *value)
+{
+	if (IsServer(client))
+		return 1;
+
+	return 0;
+}
+
+void mtag_add_geoip(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, const char *signature)
+{
+	MessageTag *m;
+	
+	GeoIPResult *geoip;
+
+	if (IsUser(client) && ((geoip = geoip_client(client))))
+	{
+		MessageTag *m = find_mtag(recv_mtags, "unrealircd.org/geoip");
+		if (m)
+		{
+			m = duplicate_mtag(m);
+		} else {
+			m = safe_alloc(sizeof(MessageTag));
+			safe_strdup(m->name, "unrealircd.org/geoip");
+			safe_strdup(m->value, geoip->country_code);
+		}
+		AddListItem(m, *mtag_list);
+	}
+}
+
+/** Outgoing filter for this message tag */
+int geoip_mtag_should_send_to_client(Client *target)
+{
+	if (IsServer(target) || IsOper(target))
+		return 1;
+
+	return 0;
+}
diff --git a/src/modules/geoip_base.c b/src/modules/geoip_base.c
@@ -2,7 +2,7 @@
  * GEOIP Base module, needed for all geoip functions
  * as this stores the geo information in ModData.
  * (C) Copyright 2021-.. Syzop and The UnrealIRCd Team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -28,6 +28,7 @@ int geoip_base_handshake(Client *client);
 int geoip_base_ip_change(Client *client, const char *oldip);
 int geoip_base_whois(Client *client, Client *target, NameValuePrioList **list);
 int geoip_connect_extinfo(Client *client, NameValuePrioList **list);
+int geoip_log(Client *client, int detail, json_t *j);
 int geoip_base_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
 int geoip_base_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
 EVENT(geoip_base_set_existing_users_evt);
@@ -125,6 +126,7 @@ MOD_INIT()
 	HookAdd(modinfo->handle, HOOKTYPE_CONNECT_EXTINFO, 1, geoip_connect_extinfo); /* (prio: near-first) */
 	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0,geoip_base_handshake); /* in case the IP changed in registration phase (WEBIRC, HTTP Forwarded) */
 	HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, geoip_base_whois);
+	HookAdd(modinfo->handle, HOOKTYPE_JSON_EXPAND_CLIENT, 0, geoip_log);
 
 	CommandAdd(modinfo->handle, "GEOIP", cmd_geoip, MAXPARA, CMD_USER);
 
@@ -206,7 +208,7 @@ void geoip_base_unserialize(const char *str, ModData *m)
 	char *country_code = NULL;
 	GeoIPResult *res;
 
-	if (m->ptr == NULL)
+	if (m->ptr)
 	{
 		free_geoip_result((GeoIPResult *)m->ptr);
 		m->ptr = NULL;
@@ -254,6 +256,21 @@ int geoip_connect_extinfo(Client *client, NameValuePrioList **list)
 	return 0;
 }
 
+int geoip_log(Client *client, int detail, json_t *j)
+{
+	GeoIPResult *geo = GEOIPDATA(client);
+	json_t *geoip;
+
+	if (!geo)
+		return 0;
+
+	geoip = json_object();
+	json_object_set_new(j, "geoip", geoip);
+	json_object_set_new(geoip, "country_code", json_string_unreal(geo->country_code));
+
+	return 0;
+}
+
 int geoip_base_whois(Client *client, Client *target, NameValuePrioList **list)
 {
 	GeoIPResult *geo;
diff --git a/src/modules/geoip_classic.c b/src/modules/geoip_classic.c
@@ -1,6 +1,6 @@
 /* GEOIP Classic module
  * (C) Copyright 2021 Bram Matthys and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
diff --git a/src/modules/geoip_maxmind.c b/src/modules/geoip_maxmind.c
@@ -1,6 +1,6 @@
 /* GEOIP maxmind module
  * (C) Copyright 2021 Bram Matthys and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
diff --git a/src/modules/hideserver.c b/src/modules/hideserver.c
@@ -459,7 +459,7 @@ CMD_OVERRIDE_FUNC(override_links)
 			sendnumeric(client, RPL_LINKS, acptr->name, me.name,
 			    1, (acptr->info[0] ? acptr->info : "(Unknown Location)"));
 		else
-			sendnumeric(client, RPL_LINKS, acptr->name, acptr->uplink->name,
+			sendnumeric(client, RPL_LINKS, acptr->name, acptr->uplink ? acptr->uplink->name : me.name,
 			    acptr->hopcount, (acptr->info[0] ? acptr->info : "(Unknown Location)"));
 	}
 
diff --git a/src/modules/history_backend_mem.c b/src/modules/history_backend_mem.c
@@ -1,6 +1,6 @@
 /* src/modules/history_backend_mem.c - History Backend: memory
  * (C) Copyright 2019-2021 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 #include "unrealircd.h"
 
diff --git a/src/modules/history_backend_null.c b/src/modules/history_backend_null.c
@@ -1,6 +1,6 @@
 /* src/modules/history_backend_null.c - History Backend: null / none
  * (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 #include "unrealircd.h"
 
diff --git a/src/modules/ident_lookup.c b/src/modules/ident_lookup.c
@@ -1,6 +1,6 @@
 /* src/modules/ident_lookup.c - Ident lookups (RFC1413)
  * (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 #include "unrealircd.h"
 
diff --git a/src/modules/ison.c b/src/modules/ison.c
@@ -62,10 +62,9 @@ MOD_UNLOAD()
  * ISON :nicklist
  */
 
-static char buf[BUFSIZE];
-
 CMD_FUNC(cmd_ison)
 {
+	char buf[BUFSIZE];
 	char request[BUFSIZE];
 	char namebuf[USERLEN + HOSTLEN + 4];
 	Client *acptr;
diff --git a/src/modules/join.c b/src/modules/join.c
@@ -94,37 +94,32 @@ MOD_UNLOAD()
  */
 int _can_join(Client *client, Channel *channel, const char *key, char **errmsg)
 {
-	Link *lp;
-	Ban *banned;
 	Hook *h;
-	int i=0, j=0;
 
-	for (h = Hooks[HOOKTYPE_CAN_JOIN]; h; h = h->next)
+	/* An /INVITE lets you bypass all restrictions */
+	if (is_invited(client, channel))
 	{
-		i = (*(h->func.intfunc))(client,channel,key, errmsg);
-		if (i != 0)
-			return i;
+		int j = 0;
+		for (h = Hooks[HOOKTYPE_INVITE_BYPASS]; h; h = h->next)
+		{
+			j = (*(h->func.intfunc))(client,channel);
+			if (j != 0)
+				break;
+		}
+		/* Bypass is OK, unless a HOOKTYPE_INVITE_BYPASS hook returns HOOK_DENY */
+		if (j != HOOK_DENY)
+			return 0;
 	}
 
-	for (h = Hooks[HOOKTYPE_OPER_INVITE_BAN]; h; h = h->next)
+	for (h = Hooks[HOOKTYPE_CAN_JOIN]; h; h = h->next)
 	{
-		j = (*(h->func.intfunc))(client,channel);
-		if (j != 0)
-			break;
+		int i = (*(h->func.intfunc))(client,channel,key, errmsg);
+		if (i != 0)
+			return i;
 	}
 
 	/* See if we can evade this ban */
-	banned = is_banned(client, channel, BANCHK_JOIN, NULL, NULL);
-	if (banned && j == HOOK_DENY)
-	{
-		*errmsg = STR_ERR_BANNEDFROMCHAN;
-		return ERR_BANNEDFROMCHAN;
-	}
-
-	if (is_invited(client, channel))
-		return 0; /* allowed to walk through all the other modes */
-
-	if (banned)
+	if (is_banned(client, channel, BANCHK_JOIN, NULL, NULL))
 	{
 		*errmsg = STR_ERR_BANNEDFROMCHAN;
 		return ERR_BANNEDFROMCHAN;
@@ -262,6 +257,7 @@ void _join_channel(Channel *channel, Client *client, MessageTag *recv_mtags, con
 			MessageTag *mtags_mode = NULL;
 			Cmode *cm;
 			char modebuf[BUFSIZE], parabuf[BUFSIZE];
+			int should_destroy = 0;
 
 			channel->mode.mode = MODES_ON_JOIN;
 
@@ -281,6 +277,7 @@ void _join_channel(Channel *channel, Client *client, MessageTag *recv_mtags, con
 			sendto_server(NULL, 0, 0, mtags_mode, ":%s MODE %s %s %s %lld",
 			    me.id, channel->name, modebuf, parabuf, (long long)channel->creationtime);
 			sendto_one(client, mtags_mode, ":%s MODE %s %s %s", me.name, channel->name, modebuf, parabuf);
+			RunHook(HOOKTYPE_LOCAL_CHANMODE, &me, channel, mtags_mode, modebuf, parabuf, 0, 0, &should_destroy);
 			free_message_tags(mtags_mode);
 		}
 
diff --git a/src/modules/kill.c b/src/modules/kill.c
@@ -21,7 +21,6 @@
 #include "unrealircd.h"
 
 CMD_FUNC(cmd_kill);
-static char buf[BUFSIZE], buf2[BUFSIZE];
 
 ModuleHeader MOD_HEADER
   = {
@@ -58,6 +57,7 @@ CMD_FUNC(cmd_kill)
 {
 	char targetlist[BUFSIZE];
 	char reason[BUFSIZE];
+	char buf2[BUFSIZE];
 	char *str;
 	char *nick, *save = NULL;
 	Client *target;
diff --git a/src/modules/labeled-response.c b/src/modules/labeled-response.c
@@ -197,11 +197,15 @@ int lr_post_command(Client *from, MessageTag *mtags, const char *buf)
 			int more_tags = currentcmd.firstbuf[0] == '@';
 			currentcmd.client = NULL; /* prevent lr_packet from interfering */
 			snprintf(packet, sizeof(packet)-3,
-				 "@label=%s%s%s\r\n",
+				 "@label=%s%s%s",
 				 currentcmd.label,
 				 more_tags ? ";" : " ",
 				 more_tags ? currentcmd.firstbuf+1 : currentcmd.firstbuf);
-			sendbufto_one(from, packet, 0);
+			/* Format the IRC message correctly here, so we can take the
+			 * quick path through sendbufto_one().
+			 */
+			strlcat(packet, "\r\n", sizeof(packet));
+			sendbufto_one(from, packet, strlen(packet));
 			goto done;
 		}
 
diff --git a/src/modules/message.c b/src/modules/message.c
@@ -22,7 +22,6 @@
 
 /* Forward declarations */
 const char *_StripColors(const char *text);
-const char *_StripControlCodes(const char *text);
 int ban_version(Client *client, const char *text);
 CMD_FUNC(cmd_private);
 CMD_FUNC(cmd_notice);
@@ -47,7 +46,6 @@ MOD_TEST()
 {
 	MARK_AS_OFFICIAL_MODULE(modinfo);
 	EfunctionAddConstString(modinfo->handle, EFUNC_STRIPCOLORS, _StripColors);
-	EfunctionAddConstString(modinfo->handle, EFUNC_STRIPCONTROLCODES, _StripControlCodes);
 	EfunctionAdd(modinfo->handle, EFUNC_CAN_SEND_TO_CHANNEL, _can_send_to_channel);
 	return MOD_SUCCESS;
 }
@@ -526,8 +524,10 @@ CMD_FUNC(cmd_tagmsg)
 /* Taken from xchat by Peter Zelezny
  * changed very slightly by codemastr
  * RGB color stripping support added -- codemastr
+ *
+ * NOTE: if you change/update/enhance StripColors() then consider changing
+ *       the StripControlCodes() function as well (in misc.c) !!
  */
-
 const char *_StripColors(const char *text)
 {
 	int i = 0, len = strlen(text), save_len=0;
@@ -595,103 +595,6 @@ const char *_StripColors(const char *text)
 	return new_str;
 }
 
-/* strip color, bold, underline, and reverse codes from a string */
-const char *_StripControlCodes(const char *text) 
-{
-	int i = 0, len = strlen(text), save_len=0;
-	char nc = 0, col = 0, rgb = 0;
-	const char *save_text=NULL;
-	static unsigned char new_str[4096];
-	while (len > 0) 
-	{
-		if ( col && ((isdigit(*text) && nc < 2) || (*text == ',' && nc < 3)))
-		{
-			nc++;
-			if (*text == ',')
-				nc = 0;
-		}
-		/* Syntax for RGB is ^DHHHHHH where H is a hex digit.
-		 * If < 6 hex digits are specified, the code is displayed
-		 * as text
-		 */
-		else if ((rgb && isxdigit(*text) && nc < 6) || (rgb && *text == ',' && nc < 7))
-		{
-			nc++;
-			if (*text == ',')
-				nc = 0;
-		}
-		else 
-		{
-			if (col)
-				col = 0;
-			if (rgb)
-			{
-				if (nc != 6)
-				{
-					text = save_text+1;
-					len = save_len-1;
-					rgb = 0;
-					continue;
-				}
-				rgb = 0;
-			}
-			switch (*text)
-			{
-			case 3:
-				/* color */
-				col = 1;
-				nc = 0;
-				break;
-			case 4:
-				/* RGB */
-				save_text = text;
-				save_len = len;
-				rgb = 1;
-				nc = 0;
-				break;
-			case 2:
-				/* bold */
-				break;
-			case 31:
-				/* underline */
-				break;
-			case 22:
-				/* reverse */
-				break;
-			case 15:
-				/* plain */
-				break;
-			case 29:
-				/* italic */
-				break;
-			case 30:
-				/* strikethrough */
-				break;
-			case 17:
-				/* monospace */
-				break;
-			case 0xe2:
-				if (!strncmp(text+1, "\x80\x8b", 2))
-				{
-					/* +2 means we skip 3 */
-					text += 2;
-					len  -= 2;
-					break;
-				}
-				/*fallthrough*/
-			default:
-				new_str[i] = *text;
-				i++;
-				break;
-			}
-		}
-		text++;
-		len--;
-	}
-	new_str[i] = 0;
-	return new_str;
-}
-
 /** Check ban version { } blocks, returns 1  if banned and  0 if not. */
 int ban_version(Client *client, const char *text)
 {
diff --git a/src/modules/mode.c b/src/modules/mode.c
@@ -472,8 +472,8 @@ MultiLineMode *make_mode_str(Client *client, Channel *channel, Cmode_t oldem, in
 			if (curr == MAXMULTILINEMODES)
 			{
 				/* Should be impossible.. */
-				unreal_log(ULOG_ERROR, "mode", "MODE_MULTINE_EXCEEDED", client,
-				           "A mode string caused an avalanche effect of more than $max_multiline modes "
+				unreal_log(ULOG_ERROR, "mode", "MODE_MULTILINE_EXCEEDED", client,
+				           "A mode string caused an avalanche effect of more than $max_multiline_modes modes "
 				           "in channel $channel. Caused by client $client. Expect a desync.",
 				           log_data_integer("max_multiline_modes", MAXMULTILINEMODES),
 				           log_data_channel("channel", channel));
@@ -807,6 +807,7 @@ void do_mode_char_member_mode_new(Channel *channel, Cmode *handler, const char *
 			   "[BUG] Client $target.details on channel $channel: "
 			   "found via find_membership_link() but NOT found via find_member_link(). "
 			   "This should never happen! Please report on https://bugs.unrealircd.org/",
+			   log_data_client("target", target),
 			   log_data_channel("channel", channel));
 		return;
 	}
@@ -816,6 +817,58 @@ void do_mode_char_member_mode_new(Channel *channel, Cmode *handler, const char *
 	if ((what == MODE_DEL) && !strchr(member->member_modes, modechar))
 		return; /* already unset */
 
+	/* HOOKTYPE_MODE_DEOP code */
+	if (what == MODE_DEL)
+	{
+		int ret = EX_ALLOW;
+		const char *badmode = NULL;
+		Hook *h;
+		const char *my_access;
+		Membership *my_membership;
+
+		/* Set "my_access" to access flags of the requestor */
+		if (IsUser(client) && (my_membership = find_membership_link(client->user->channel, channel)))
+			my_access = my_membership->member_modes; /* client */
+		else
+			my_access = ""; /* server */
+
+		for (h = Hooks[HOOKTYPE_MODE_DEOP]; h; h = h->next)
+		{
+			int n = (*(h->func.intfunc))(client, target, channel, what, modechar, my_access, member->member_modes, &badmode);
+			if (n == EX_DENY)
+			{
+				ret = n;
+			} else
+			if (n == EX_ALWAYS_DENY)
+			{
+				ret = n;
+				break;
+			}
+		}
+
+		if (ret == EX_ALWAYS_DENY)
+		{
+			if (MyUser(client) && badmode)
+				sendto_one(client, NULL, "%s", badmode); /* send error message, if any */
+
+			if (MyUser(client))
+				return; /* stop processing this mode */
+		}
+
+		/* This probably should work but is completely untested (the operoverride stuff, I mean): */
+		if (ret == EX_DENY)
+		{
+			if (!op_can_override("channel:override:mode:del",client,channel,handler))
+			{
+				if (badmode)
+					sendto_one(client, NULL, "%s", badmode); /* send error message, if any */
+				return; /* stop processing this mode */
+			} else {
+				opermode = 1;
+			}
+		}
+	}
+
 	if (what == MODE_ADD)
 	{
 		if (strchr(member->member_modes, modechar))
@@ -1306,8 +1359,6 @@ CMD_FUNC(_cmd_umode)
 
 		/* Notify */
 		userhost_changed(client);
-		if (MyUser(client))
-			sendnumeric(client, RPL_HOSTHIDDEN, client->user->virthost);
 	}
 
 	/* -x */
@@ -1321,8 +1372,6 @@ CMD_FUNC(_cmd_umode)
 
 		/* Notify */
 		userhost_changed(client);
-		if (MyUser(client))
-			sendnumeric(client, RPL_HOSTHIDDEN, client->user->realhost);
 	}
 	/*
 	 * If I understand what this code is doing correctly...
@@ -1337,7 +1386,7 @@ CMD_FUNC(_cmd_umode)
 	{
 		list_del(&client->special_node);
 		if (MyUser(client))
-			RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL);
+			RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL, NULL);
 		remove_oper_privileges(client, 0);
 	}
 
diff --git a/src/modules/names.c b/src/modules/names.c
@@ -68,8 +68,6 @@ MOD_UNLOAD()
  * 12 Feb 2000 - geesh, time for a rewrite -lucas
  ************************************************************************/
 
-static char buf[BUFSIZE];
-
 /*
 ** cmd_names
 **	parv[1] = channel
@@ -88,6 +86,7 @@ CMD_FUNC(cmd_names)
 	int idx, flag = 1, spos;
 	const char *para = parv[1], *s;
 	char nuhBuffer[NICKLEN+USERLEN+HOSTLEN+3];
+	char buf[BUFSIZE];
 
 	if (parc < 2 || !MyConnect(client))
 	{
diff --git a/src/modules/nick.c b/src/modules/nick.c
@@ -46,7 +46,6 @@ ModuleHeader MOD_HEADER
 #define ASSUME_NICK_IN_FLIGHT
 
 /* Variables */
-static char buf[BUFSIZE];
 static char spamfilter_user[NICKLEN + USERLEN + HOSTLEN + REALLEN + 64];
 
 /* Forward declarations */
@@ -489,6 +488,7 @@ CMD_FUNC(cmd_uid)
 	Client *acptr, *serv = NULL;
 	Client *acptrs;
 	char nick[NICKLEN + 1];
+	char buf[BUFSIZE];
 	long lastnick = 0;
 	int differ = 1;
 	const char *hostname, *username, *sstamp, *umodes, *virthost, *ip_raw, *realname;
@@ -758,10 +758,21 @@ CMD_FUNC(cmd_nick)
 	}
 }
 
+/** Welcome the user on IRC.
+ * Send the RPL_WELCOME, LUSERS, MOTD, auto join channels, everything...
+ */
 void welcome_user(Client *client, TKL *viruschan_tkl)
 {
 	int i;
 	ConfigItem_tld *tlds;
+	char buf[BUFSIZE];
+
+	/* Make creation time the real 'online since' time, excluding registration time.
+	 * Otherwise things like set::anti-spam-quit-messagetime 10s could mean
+	 * 1 second in practice (#2174).
+	 */
+	client->local->creationtime = TStime();
+	client->local->idle_since = TStime();
 
 	RunHook(HOOKTYPE_WELCOME, client, 0);
 	sendnumeric(client, RPL_WELCOME, NETWORK_NAME, client->name, client->user->username, client->user->realhost);
@@ -826,10 +837,11 @@ void welcome_user(Client *client, TKL *viruschan_tkl)
 	sendto_serv_butone_nickcmd(client->direction, NULL, client, (*buf == '\0' ? "+" : buf));
 
 	broadcast_moddata_client(client);
-	RunHook(HOOKTYPE_LOCAL_CONNECT, client);
+
 	if (buf[0] != '\0' && buf[1] != '\0')
 		sendto_one(client, NULL, ":%s MODE %s :%s", client->name,
 		    client->name, buf);
+
 	if (client->user->snomask)
 		sendnumeric(client, RPL_SNOMASK, client->user->snomask);
 
@@ -839,12 +851,7 @@ void welcome_user(Client *client, TKL *viruschan_tkl)
 	if (IsSecure(client) && (iConf.outdated_tls_policy_user == POLICY_WARN) && outdated_tls_client(client))
 		sendnotice(client, "%s", outdated_tls_client_build_string(iConf.outdated_tls_policy_user_message, client));
 
-	/* Make creation time the real 'online since' time, excluding registration time.
-	 * Otherwise things like set::anti-spam-quit-messagetime 10s could mean
-	 * 1 second in practice (#2174).
-	 */
-	client->local->creationtime = TStime();
-	client->local->idle_since = TStime();
+	RunHook(HOOKTYPE_LOCAL_CONNECT, client);
 
 	/* Give the user a fresh start as far as fake-lag is concerned.
 	 * Otherwise the user could be lagged up already due to all the CAP stuff.
@@ -1261,7 +1268,7 @@ int AllowClient(Client *client)
 		if (aconf->flags.tls && !IsSecure(client))
 			continue;
 
-		if (!unreal_mask_match(client, aconf->mask))
+		if (!user_allowed_by_security_group(client, aconf->match))
 			continue;
 
 		/* Check authentication */
diff --git a/src/modules/oper.c b/src/modules/oper.c
@@ -20,10 +20,6 @@
 
 #include "unrealircd.h"
 
-CMD_FUNC(cmd_oper);
-
-
-/* Place includes here */
 #define MSG_OPER        "OPER"  /* OPER */
 
 ModuleHeader MOD_HEADER
@@ -35,27 +31,37 @@ ModuleHeader MOD_HEADER
 	"unrealircd-6",
     };
 
-/* This is called on module init, before Server Ready */
+/* Forward declarations */
+CMD_FUNC(cmd_oper);
+int _make_oper(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost);
+int oper_connect(Client *client);
+
+MOD_TEST()
+{
+	MARK_AS_OFFICIAL_MODULE(modinfo);
+	EfunctionAdd(modinfo->handle, EFUNC_MAKE_OPER, _make_oper);
+	return MOD_SUCCESS;
+}
+
 MOD_INIT()
 {
-	CommandAdd(modinfo->handle, MSG_OPER, cmd_oper, MAXPARA, CMD_USER);
 	MARK_AS_OFFICIAL_MODULE(modinfo);
+	CommandAdd(modinfo->handle, MSG_OPER, cmd_oper, MAXPARA, CMD_USER);
+	HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, oper_connect);
 	return MOD_SUCCESS;
 }
 
-/* Is first run when server is 100% ready */
 MOD_LOAD()
 {
 	return MOD_SUCCESS;
 }
 
-/* Called when module is unloaded */
 MOD_UNLOAD()
 {
 	return MOD_SUCCESS;
 }
 
-void set_oper_host(Client *client, char *host)
+void set_oper_host(Client *client, const char *host)
 {
         char uhost[HOSTLEN + USERLEN + 1];
         char *p;
@@ -70,8 +76,96 @@ void set_oper_host(Client *client, char *host)
 		    client->id, client->user->username);
 	        host = p;
 	}
-	iNAH_host(client, host);
-	SetHidden(client);
+	safe_strdup(client->user->virthost, host);
+	if (MyConnect(client))
+		sendto_server(NULL, 0, 0, NULL, ":%s SETHOST :%s", client->id, client->user->virthost);
+	client->umodes |= UMODE_SETHOST|UMODE_HIDE;
+}
+
+int _make_oper(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost)
+{
+	long old_umodes = client->umodes & ALL_UMODES;
+
+	userhost_save_current(client);
+
+	/* Put in the right class (if any) */
+	if (clientclass)
+	{
+		if (client->local->class)
+			client->local->class->clients--;
+		client->local->class = clientclass;
+		client->local->class->clients++;
+	}
+
+	/* set oper user modes */
+	client->umodes |= UMODE_OPER;
+	if (modes)
+		client->umodes |= modes; /* oper::modes */
+	else
+		client->umodes |= OPER_MODES; /* set::modes-on-oper */
+
+	/* oper::vhost */
+	if (vhost)
+	{
+		set_oper_host(client, vhost);
+	} else
+	if (IsHidden(client) && !client->user->virthost)
+	{
+		/* +x has just been set by modes-on-oper and no vhost. cloak the oper! */
+		safe_strdup(client->user->virthost, client->user->cloakedhost);
+	}
+
+	userhost_changed(client);
+
+	unreal_log(ULOG_INFO, "oper", "OPER_SUCCESS", client,
+		   "$client.details is now an IRC Operator [oper-block: $oper_block] [operclass: $operclass]",
+		   log_data_string("oper_block", operblock_name),
+		   log_data_string("operclass", operclass));
+
+	/* set oper snomasks */
+	if (snomask)
+		set_snomask(client, snomask); /* oper::snomask */
+	else
+		set_snomask(client, OPER_SNOMASK); /* set::snomask-on-oper */
+
+	send_umode_out(client, 1, old_umodes);
+	if (client->user->snomask)
+		sendnumeric(client, RPL_SNOMASK, client->user->snomask);
+
+	list_add(&client->special_node, &oper_list);
+
+	RunHook(HOOKTYPE_LOCAL_OPER, client, 1, operblock_name, operclass);
+
+	sendnumeric(client, RPL_YOUREOPER);
+
+	/* Update statistics */
+	if (IsInvisible(client) && !(old_umodes & UMODE_INVISIBLE))
+		irccounts.invisible++;
+	if (IsOper(client) && !IsHideOper(client))
+		irccounts.operators++;
+
+	if (SHOWOPERMOTD == 1)
+	{
+		const char *args[1] = { NULL };
+		do_cmd(client, NULL, "OPERMOTD", 1, args);
+	}
+
+	if (!BadPtr(OPER_AUTO_JOIN_CHANS) && strcmp(OPER_AUTO_JOIN_CHANS, "0"))
+	{
+		char *chans = strdup(OPER_AUTO_JOIN_CHANS);
+		const char *args[3] = {
+			client->name,
+			chans,
+			NULL
+		};
+		do_cmd(client, NULL, "JOIN", 3, args);
+		safe_free(chans);
+		/* Theoretically the oper may be killed on join. Would be fun, though */
+		if (IsDead(client))
+			return 0;
+	}
+
+	return 1;
 }
 
 /*
@@ -83,7 +177,6 @@ CMD_FUNC(cmd_oper)
 {
 	ConfigItem_oper *operblock;
 	const char *operblock_name, *password;
-	long old_umodes = client->umodes & ALL_UMODES;
 
 	if (!MyUser(client))
 		return;
@@ -152,7 +245,7 @@ CMD_FUNC(cmd_oper)
 	 * more seriously, they are logged as errors instead of warnings.
 	 */
 
-	if (!unreal_mask_match(client, operblock->mask))
+	if (!user_allowed_by_security_group(client, operblock->match))
 	{
 		sendnumeric(client, ERR_NOOPERHOST);
 		unreal_log(ULOG_ERROR, "oper", "OPER_FAILED", client,
@@ -164,7 +257,7 @@ CMD_FUNC(cmd_oper)
 		return;
 	}
 
-	if (!Auth_Check(client, operblock->auth, password))
+	if (operblock->auth && !Auth_Check(client, operblock->auth, password))
 	{
 		sendnumeric(client, ERR_PASSWDMISMATCH);
 		if (FAILOPER_WARN)
@@ -227,12 +320,6 @@ CMD_FUNC(cmd_oper)
 	/* Store which oper block was used to become IRCOp (for maxlogins and whois) */
 	safe_strdup(client->user->operlogin, operblock->name);
 
-	/* Put in the right class */
-	if (client->local->class)
-		client->local->class->clients--;
-	client->local->class = operblock->class;
-	client->local->class->clients++;
-
 	/* oper::swhois */
 	if (operblock->swhois)
 	{
@@ -241,67 +328,7 @@ CMD_FUNC(cmd_oper)
 			swhois_add(client, "oper", -100, s->line, &me, NULL);
 	}
 
-	/* set oper user modes */
-	client->umodes |= UMODE_OPER;
-	if (operblock->modes)
-		client->umodes |= operblock->modes; /* oper::modes */
-	else
-		client->umodes |= OPER_MODES; /* set::modes-on-oper */
-
-	/* oper::vhost */
-	if (operblock->vhost)
-	{
-		set_oper_host(client, operblock->vhost);
-	} else
-	if (IsHidden(client) && !client->user->virthost)
-	{
-		/* +x has just been set by modes-on-oper and no vhost. cloak the oper! */
-		safe_strdup(client->user->virthost, client->user->cloakedhost);
-	}
-
-	unreal_log(ULOG_INFO, "oper", "OPER_SUCCESS", client,
-		   "$client.details is now an IRC Operator [oper-block: $oper_block]",
-		   log_data_string("oper_block", parv[1]));
-
-	/* set oper snomasks */
-	if (operblock->snomask)
-		set_snomask(client, operblock->snomask); /* oper::snomask */
-	else
-		set_snomask(client, OPER_SNOMASK); /* set::snomask-on-oper */
-
-	send_umode_out(client, 1, old_umodes);
-	if (client->user->snomask)
-		sendnumeric(client, RPL_SNOMASK, client->user->snomask);
-
-	list_add(&client->special_node, &oper_list);
-
-	RunHook(HOOKTYPE_LOCAL_OPER, client, 1, operblock);
-
-	sendnumeric(client, RPL_YOUREOPER);
-
-	/* Update statistics */
-	if (IsInvisible(client) && !(old_umodes & UMODE_INVISIBLE))
-		irccounts.invisible++;
-	if (IsOper(client) && !IsHideOper(client))
-		irccounts.operators++;
-
-	if (SHOWOPERMOTD == 1)
-		do_cmd(client, NULL, "OPERMOTD", parc, parv);
-
-	if (!BadPtr(OPER_AUTO_JOIN_CHANS) && strcmp(OPER_AUTO_JOIN_CHANS, "0"))
-	{
-		char *chans = strdup(OPER_AUTO_JOIN_CHANS);
-		const char *args[3] = {
-			client->name,
-			chans,
-			NULL
-		};
-		do_cmd(client, NULL, "JOIN", 3, args);
-		safe_free(chans);
-		/* Theoretically the oper may be killed on join. Would be fun, though */
-		if (IsDead(client))
-			return;
-	}
+	make_oper(client, operblock->name, operblock->operclass, operblock->class, operblock->modes, operblock->snomask, operblock->vhost);
 
 	/* set::plaintext-policy::oper 'warn' */
 	if (!IsSecure(client) && !IsLocalhost(client) && (iConf.plaintext_policy_oper == POLICY_WARN))
@@ -323,3 +350,29 @@ CMD_FUNC(cmd_oper)
 		           log_data_string("warn_type", "OUTDATED_TLS_PROTOCOL_OR_CIPHER"));
 	}
 }
+
+int oper_connect(Client *client)
+{
+	ConfigItem_oper *e;
+
+	if (IsOper(client))
+		return 0;
+
+	for (e = conf_oper; e; e = e->next)
+	{
+		if (e->auto_login && user_allowed_by_security_group(client, e->match))
+		{
+			/* Ideally we would check all the criteria that cmd_oper does.
+			 * I'm taking a shortcut for now that is not ideal...
+			 */
+			const char *parx[3];
+			parx[0] = NULL;
+			parx[1] = e->name;
+			parx[2] = NULL;
+			do_cmd(client, NULL, "OPER", 3, parx);
+			return 0;
+		}
+	}
+
+	return 0;
+}
diff --git a/src/modules/operinfo.c b/src/modules/operinfo.c
@@ -1,7 +1,7 @@
 /*
  * Store oper login in ModData, used by WHOIS and for auditting purposes.
  * (C) Copyright 2021-.. Syzop and The UnrealIRCd Team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -16,7 +16,7 @@ ModuleHeader MOD_HEADER
     };
 
 /* Forward declarations */
-int operinfo_local_oper(Client *client, int up, ConfigItem_oper *oper_block);
+int operinfo_local_oper(Client *client, int up, const char *oper_block, const char *operclass);
 void operinfo_free(ModData *m);
 const char *operinfo_serialize(ModData *m);
 void operinfo_unserialize(const char *str, ModData *m);
@@ -68,12 +68,12 @@ MOD_UNLOAD()
 	return MOD_SUCCESS;
 }
 
-int operinfo_local_oper(Client *client, int up, ConfigItem_oper *oper_block)
+int operinfo_local_oper(Client *client, int up, const char *oper_block, const char *operclass)
 {
 	if (up)
 	{
-		moddata_client_set(client, "operlogin", oper_block->name);
-		moddata_client_set(client, "operclass", oper_block->operclass);
+		moddata_client_set(client, "operlogin", oper_block);
+		moddata_client_set(client, "operclass", operclass);
 	} else {
 		moddata_client_set(client, "operlogin", NULL);
 		moddata_client_set(client, "operclass", NULL);
diff --git a/src/modules/protoctl.c b/src/modules/protoctl.c
@@ -206,7 +206,7 @@ CMD_FUNC(cmd_protoctl)
 			if ((aclient = hash_find_id(sid, NULL)) != NULL)
 			{
 				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_SID_COLLISION", client,
-					   "Server link $client rejected. Server with SID $sid already exist via uplink $exiting_client.server.uplink.",
+					   "Server link $client rejected. Server with SID $sid already exist via uplink $existing_client.server.uplink.",
 					   log_data_string("sid", sid),
 					   log_data_client("existing_client", aclient));
 				exit_client(client, NULL, "SID collision");
@@ -266,7 +266,7 @@ CMD_FUNC(cmd_protoctl)
 			 */
 			strlcpy(client->name, servername, sizeof(client->name));
 
-			if (!verify_link(client, &aconf))
+			if (!(aconf = verify_link(client)))
 				return;
 
 			/* note: software, protocol and flags may be NULL */
diff --git a/src/modules/reputation.c b/src/modules/reputation.c
@@ -1,7 +1,7 @@
 /*
  * reputation - Provides a scoring system for "known users".
  * (C) Copyright 2015-2019 Bram Matthys (Syzop) and the UnrealIRCd team.
- * License: GPLv2
+ * License: GPLv2 or later
  *
  * How this works is simple:
  * Every 5 minutes the IP address of all the connected users receive
@@ -918,6 +918,24 @@ static inline int is_reputation_expired(ReputationEntry *e)
 	return 0;
 }
 
+/** If the reputation changed (due to server syncing) then update the
+ * individual users reputation score as well.
+ */
+void reputation_changed_update_users(ReputationEntry *e)
+{
+	Client *client;
+
+	list_for_each_entry(client, &client_list, client_node)
+	{
+		if (client->ip && !strcmp(e->ip, client->ip))
+		{
+			/* With some (possibly unneeded) care to only go forward */
+			if (Reputation(client) < e->score)
+				Reputation(client) = e->score;
+		}
+	}
+}
+
 EVENT(delete_old_records)
 {
 	int i;
@@ -1279,6 +1297,7 @@ CMD_FUNC(reputation_server_cmd)
 			   log_data_integer("score", e->score));
 #endif
 		e->score = score;
+		reputation_changed_update_users(e);
 	}
 
 	/* If we don't have any entry for this IP, add it now. */
@@ -1296,6 +1315,7 @@ CMD_FUNC(reputation_server_cmd)
 		e->score = score;
 		e->last_seen = TStime();
 		add_reputation_entry(e);
+		reputation_changed_update_users(e);
 	}
 
 	/* Propagate to the non-client direction (score may be updated) */
diff --git a/src/modules/restrict-commands.c b/src/modules/restrict-commands.c
@@ -32,11 +32,7 @@ struct RestrictedCommand {
 	RestrictedCommand *prev, *next;
 	char *cmd;
 	char *conftag;
-	long connect_delay;
-	int exempt_identified;
-	int exempt_reputation_score;
-	int exempt_webirc;
-	int exempt_tls;
+	SecurityGroup *except;
 };
 
 typedef struct {
@@ -103,6 +99,7 @@ MOD_UNLOAD()
 		next = rcmd->next;
 		safe_free(rcmd->conftag);
 		safe_free(rcmd->cmd);
+		free_security_group(rcmd->except);
 		DelListItem(rcmd, RestrictedCommandList);
 		safe_free(rcmd);
 	}
@@ -170,6 +167,12 @@ int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
 				continue;
 			}
 
+			if (!strcmp(cep2->name, "except"))
+			{
+				test_match_block(cf, cep2, &errors);
+				continue;
+			}
+
 			if (!cep2->value)
 			{
 				config_error("%s:%i: blank set::restrict-commands::%s:%s without value", cep2->file->filename, cep2->line_number, cep->name, cep2->name);
@@ -249,7 +252,13 @@ int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
 				config_warn("[restrict-commands] Command '%s' does not exist. Did you mistype? Or is the module providing it not loaded?", cmd);
 				continue;
 			}
-
+			if (find_restrictions_bycmd(cmd))
+			{
+				config_warn("[restrict-commands] Multiple set::restrict-commands items for command '%s'. "
+				            "Only one config block will be effective.",
+				            cmd);
+				continue;
+			}
 			if (!CommandOverrideAdd(ModInf.handle, cmd, 0, rcmd_override))
 			{
 				config_warn("[restrict-commands] Failed to add override for '%s' (NO RESTRICTIONS APPLY)", cmd);
@@ -260,38 +269,46 @@ int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
 		rcmd = safe_alloc(sizeof(RestrictedCommand));
 		safe_strdup(rcmd->cmd, cmd);
 		safe_strdup(rcmd->conftag, conftag);
+		rcmd->except = safe_alloc(sizeof(SecurityGroup));
+
 		for (cep2 = cep->items; cep2; cep2 = cep2->next)
 		{
+			if (!strcmp(cep2->name, "except"))
+			{
+				conf_match_block(cf, cep2, &rcmd->except);
+				continue;
+			}
+
 			if (!cep2->value)
 				continue;
 
 			if (!strcmp(cep2->name, "connect-delay"))
 			{
-				rcmd->connect_delay = config_checkval(cep2->value, CFG_TIME);
+				rcmd->except->connect_time = config_checkval(cep2->value, CFG_TIME);
 				continue;
 			}
 
 			if (!strcmp(cep2->name, "exempt-identified"))
 			{
-				rcmd->exempt_identified = config_checkval(cep2->value, CFG_YESNO);
+				rcmd->except->identified = config_checkval(cep2->value, CFG_YESNO);
 				continue;
 			}
 			
 			if (!strcmp(cep2->name, "exempt-webirc"))
 			{
-				rcmd->exempt_webirc = config_checkval(cep2->value, CFG_YESNO);
+				rcmd->except->webirc = config_checkval(cep2->value, CFG_YESNO);
 				continue;
 			}
 
 			if (!strcmp(cep2->name, "exempt-tls"))
 			{
-				rcmd->exempt_tls = config_checkval(cep2->value, CFG_YESNO);
+				rcmd->except->tls = config_checkval(cep2->value, CFG_YESNO);
 				continue;
 			}
 
 			if (!strcmp(cep2->name, "exempt-reputation-score"))
 			{
-				rcmd->exempt_reputation_score = atoi(cep2->value);
+				rcmd->except->reputation_score = atoi(cep2->value);
 				continue;
 			}
 		}
@@ -305,15 +322,7 @@ int rcmd_canbypass(Client *client, RestrictedCommand *rcmd)
 {
 	if (!client || !rcmd)
 		return 1;
-	if (rcmd->exempt_identified && IsLoggedIn(client))
-		return 1;
-	if (rcmd->exempt_webirc && moddata_client_get(client, "webirc"))
-		return 1;
-	if (rcmd->exempt_tls && IsSecureConnect(client))
-		return 1;
-	if (rcmd->exempt_reputation_score > 0 && (GetReputation(client) >= rcmd->exempt_reputation_score))
-		return 1;
-	if (rcmd->connect_delay && client->local && (TStime() - client->local->creationtime >= rcmd->connect_delay))
+	if (user_allowed_by_security_group(client, rcmd->except))
 		return 1;
 	return 0;
 }
@@ -351,11 +360,11 @@ int rcmd_block_message(Client *client, const char *text, SendType sendtype, cons
 	if (rcmd && !rcmd_canbypass(client, rcmd))
 	{
 		int notice = (sendtype == SEND_TYPE_NOTICE ? 1 : 0); // temporary hack FIXME !!!
-		if (rcmd->connect_delay)
+		if (rcmd->except->connect_time)
 		{
 			ircsnprintf(errbuf, sizeof(errbuf),
 				    "You cannot send %ss to %ss until you've been connected for %ld seconds or more",
-				    (notice ? "notice" : "message"), display, rcmd->connect_delay);
+				    (notice ? "notice" : "message"), display, rcmd->except->connect_time);
 		} else {
 			ircsnprintf(errbuf, sizeof(errbuf),
 				    "Sending of %ss to %ss been disabled by the network administrators",
@@ -382,11 +391,11 @@ CMD_OVERRIDE_FUNC(rcmd_override)
 	rcmd = find_restrictions_bycmd(ovr->command->cmd);
 	if (rcmd && !rcmd_canbypass(client, rcmd))
 	{
-		if (rcmd->connect_delay)
+		if (rcmd->except->connect_time)
 		{
 			sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
 			               "%s :You must be connected for at least %ld seconds before you can use this command",
-			               ovr->command->cmd, rcmd->connect_delay);
+			               ovr->command->cmd, rcmd->except->connect_time);
 		} else {
 			sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
 			               "%s :This command is disabled by the network administrator",
diff --git a/src/modules/sapart.c b/src/modules/sapart.c
@@ -167,6 +167,7 @@ CMD_FUNC(cmd_sapart)
 
 	log_sapart(client, target, request, comment);
 
+/***
 	if (comment)
 	{
 		snprintf(commentx, sizeof(commentx), "SAPart: %s", comment);
@@ -174,6 +175,7 @@ CMD_FUNC(cmd_sapart)
 	} else {
 		sendnotice(target, "*** You were forced to part %s", request);
 	}
+***/
 
 	parv[0] = target->name; // nick
 	parv[1] = request; // chan
diff --git a/src/modules/server.c b/src/modules/server.c
@@ -45,7 +45,7 @@ EVENT(server_handshake_timeout);
 void send_channel_modes_sjoin3(Client *to, Channel *channel);
 CMD_FUNC(cmd_server);
 CMD_FUNC(cmd_sid);
-int _verify_link(Client *client, ConfigItem_link **link_out);
+ConfigItem_link *_verify_link(Client *client);
 void _send_protoctl_servers(Client *client, int response);
 void _send_server_message(Client *client);
 void _introduce_user(Client *to, Client *acptr);
@@ -59,7 +59,6 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp);
 static int connect_server_helper(ConfigItem_link *, Client *);
 
 /* Global variables */
-static char buf[BUFSIZE];
 static cfgstruct cfg;
 static char *last_autoconnect_server = NULL;
 
@@ -77,7 +76,7 @@ MOD_TEST()
 	MARK_AS_OFFICIAL_MODULE(modinfo);
 	EfunctionAddVoid(modinfo->handle, EFUNC_SEND_PROTOCTL_SERVERS, _send_protoctl_servers);
 	EfunctionAddVoid(modinfo->handle, EFUNC_SEND_SERVER_MESSAGE, _send_server_message);
-	EfunctionAdd(modinfo->handle, EFUNC_VERIFY_LINK, _verify_link);
+	EfunctionAddPVoid(modinfo->handle, EFUNC_VERIFY_LINK, TO_PVOIDFUNC(_verify_link));
 	EfunctionAddVoid(modinfo->handle, EFUNC_INTRODUCE_USER, _introduce_user);
 	EfunctionAdd(modinfo->handle, EFUNC_CHECK_DENY_VERSION, _check_deny_version);
 	EfunctionAddVoid(modinfo->handle, EFUNC_BROADCAST_SINFO, _broadcast_sinfo);
@@ -254,8 +253,13 @@ int server_needs_linking(ConfigItem_link *aconf)
 	Client *client;
 	ConfigItem_class *class;
 
-	/* We're only interested in autoconnect blocks that are valid. Also, we ignore temporary link blocks. */
-	if (!(aconf->outgoing.options & CONNECT_AUTO) || !aconf->outgoing.hostname || (aconf->flag.temporary == 1))
+	/* We're only interested in autoconnect blocks that also have
+	 * a valid link::outgoing configuration. We also ignore
+	 * temporary link blocks (not that they should exist...).
+	 */
+	if (!(aconf->outgoing.options & CONNECT_AUTO) ||
+	    (!aconf->outgoing.hostname && !aconf->outgoing.file) ||
+	    (aconf->flag.temporary == 1))
 		return 0;
 
 	class = aconf->class;
@@ -635,12 +639,10 @@ void _send_server_message(Client *client)
 
 /** Verify server link.
  * This does authentication and authorization checks.
- * @param cptr The client directly connected to us (cptr).
- * @param client The client which (originally) issued the server command (client).
- * @param link_out Pointer-to-pointer-to-link block. Will be set when auth OK. Caller may pass NULL if he doesn't care.
- * @returns This function returns 1 on successful authentication, 0 otherwise - in which case the client has been killed.
+ * @param client The client which issued the command
+ * @returns On successfull authentication, the link block is returned. On failure NULL is returned (client has been killed!).
  */
-int _verify_link(Client *client, ConfigItem_link **link_out)
+ConfigItem_link *_verify_link(Client *client)
 {
 	ConfigItem_link *link, *orig_link;
 	Client *acptr = NULL, *ocptr = NULL;
@@ -652,15 +654,12 @@ int _verify_link(Client *client, ConfigItem_link **link_out)
 	if (client->local->hostp && client->local->hostp->h_name)
 		set_sockhost(client, client->local->hostp->h_name);
 
-	if (link_out)
-		*link_out = NULL;
-	
 	if (!client->local->passwd)
 	{
 		unreal_log(ULOG_ERROR, "link", "LINK_DENIED_NO_PASSWORD", client,
 			   "Link with server $client.details denied: No password provided. Protocol error.");
 		exit_client(client, NULL, "Missing password");
-		return 0;
+		return NULL;
 	}
 
 	if (client->server && client->server->conf)
@@ -679,7 +678,7 @@ int _verify_link(Client *client, ConfigItem_link **link_out)
 			           log_data_link_block(client->server->conf));
 			exit_client_fmt(client, NULL, "Servername (%s) does not match name in my link block (%s)",
 			                client->name, client->server->conf->servername);
-			return 0;
+			return NULL;
 		}
 		link = client->server->conf;
 		goto skip_host_check;
@@ -695,16 +694,16 @@ int _verify_link(Client *client, ConfigItem_link **link_out)
 		unreal_log(ULOG_ERROR, "link", "LINK_DENIED_UNKNOWN_SERVER", client,
 		           "Link with server $client.details denied: No link block named '$client'");
 		exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG);
-		return 0;
+		return NULL;
 	}
 	
-	if (!link->incoming.mask)
+	if (!link->incoming.match)
 	{
 		unreal_log(ULOG_ERROR, "link", "LINK_DENIED_NO_INCOMING", client,
-		           "Link with server $client.details denied: Link block exists, but there is no link::incoming::mask set.",
+		           "Link with server $client.details denied: Link block exists, but there is no link::incoming::match set.",
 		           log_data_link_block(link));
 		exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG);
-		return 0;
+		return NULL;
 	}
 
 	orig_link = link;
@@ -716,7 +715,7 @@ int _verify_link(Client *client, ConfigItem_link **link_out)
 		           "Link with server $client.details denied: Server is in link block but link::incoming::mask didn't match",
 		           log_data_link_block(orig_link));
 		exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG);
-		return 0;
+		return NULL;
 	}
 
 skip_host_check:
@@ -767,7 +766,7 @@ skip_host_check:
 			           log_data_link_block(link));
 		}
 		exit_client(client, NULL, "Link denied (Authentication failed)");
-		return 0;
+		return NULL;
 	}
 
 	/* Verify the TLS certificate (if requested) */
@@ -782,7 +781,7 @@ skip_host_check:
 			           log_data_string("certificate_failure_msg", "not using TLS"),
 			           log_data_link_block(link));
 			exit_client(client, NULL, "Link denied (Not using TLS)");
-			return 0;
+			return NULL;
 		}
 		if (!verify_certificate(client->local->ssl, link->servername, &errstr))
 		{
@@ -791,7 +790,7 @@ skip_host_check:
 			           log_data_string("certificate_failure_msg", errstr),
 			           log_data_link_block(link));
 			exit_client(client, NULL, "Link denied (Certificate verification failed)");
-			return 0;
+			return NULL;
 		}
 	}
 
@@ -803,7 +802,7 @@ skip_host_check:
 		           log_data_string("ban_reason", bconf->reason),
 		           log_data_link_block(link));
 		exit_client_fmt(client, NULL, "Banned server: %s", bconf->reason);
-		return 0;
+		return NULL;
 	}
 
 	if (link->class->clients + 1 > link->class->maxclients)
@@ -813,7 +812,7 @@ skip_host_check:
 		           "class '$link_block.class' is full",
 		           log_data_link_block(link));
 		exit_client(client, NULL, "Full class");
-		return 0;
+		return NULL;
 	}
 	if (!IsLocalhost(client) && (iConf.plaintext_policy_server == POLICY_DENY) && !IsSecure(client))
 	{
@@ -823,7 +822,7 @@ skip_host_check:
 		           "See https://www.unrealircd.org/docs/FAQ#server-requires-tls",
 		           log_data_link_block(link));
 		exit_client(client, NULL, "Servers need to use TLS (set::plaintext-policy::server is 'deny')");
-		return 0;
+		return NULL;
 	}
 	if (IsSecure(client) && (iConf.outdated_tls_policy_server == POLICY_DENY) && outdated_tls_client(client))
 	{
@@ -834,7 +833,7 @@ skip_host_check:
 		           log_data_link_block(link),
 			   log_data_string("tls_cipher", tls_get_cipher(client)));
 		exit_client(client, NULL, "Server using outdates TLS protocol or cipher (set::outdated-tls-policy::server is 'deny')");
-		return 0;
+		return NULL;
 	}
 	/* This one is at the end, because it causes us to delink another server,
 	 * so we want to be (reasonably) sure that this one will succeed before
@@ -850,7 +849,7 @@ skip_host_check:
 			           log_data_string("me_name", me.name),
 			           log_data_link_block(link));
 			exit_client(client, NULL, "Server Exists (server trying to link with same name as myself)");
-			return 0;
+			return NULL;
 		} else {
 			unreal_log(ULOG_ERROR, "link", "LINK_DROPPED_REINTRODUCED", client,
 				   "Link with server $client.details causes older link "
@@ -861,9 +860,7 @@ skip_host_check:
 		}
 	}
 
-	if (link_out)
-		*link_out = link;
-	return 1;
+	return link;
 }
 
 /** Server command. Only for locally connected servers!!.
@@ -935,7 +932,7 @@ CMD_FUNC(cmd_server)
 	 */
 	strlcpy(client->name, servername, sizeof(client->name));
 
-	if (!verify_link(client, &aconf))
+	if (!(aconf = verify_link(client)))
 		return; /* Rejected */
 
 	/* From this point the server is authenticated, so we can be more verbose
@@ -1200,6 +1197,7 @@ CMD_FUNC(cmd_sid)
 		unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_SERVER_BAN", client,
 		           "Denied remote server $servername which was introduced by $client: "
 		           "Server is banned ($ban_reason)",
+		           log_data_string("servername", servername),
 		           log_data_string("ban_reason", bconf->reason));
 		/* Before UnrealIRCd 6 this would SQUIT the server who introduced
 		 * this server. That seems a bit of an overreaction, so we now
@@ -1330,6 +1328,8 @@ CMD_FUNC(cmd_sid)
 
 void _introduce_user(Client *to, Client *acptr)
 {
+	char buf[512];
+
 	build_umode_string(acptr, 0, SEND_UMODES, buf);
 
 	sendto_one_nickcmd(to, NULL, acptr, buf);
@@ -1807,13 +1807,13 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp)
 {
 	Client *client;
 
-	if (!aconf->outgoing.hostname)
+	if (!aconf->outgoing.hostname && !aconf->outgoing.file)
 	{
 		/* Actually the caller should make sure that this doesn't happen,
 		 * so this error may never be triggered:
 		 */
 		unreal_log(ULOG_ERROR, "link", "LINK_ERROR_NO_OUTGOING", NULL,
-		           "Connect to $link_block failed: link block is for incoming only (no link::outgoing::hostname set)",
+		           "Connect to $link_block failed: link block is for incoming only (no link::outgoing::hostname or link::outgoing::file set)",
 		           log_data_link_block(aconf));
 		return;
 	}
@@ -1827,7 +1827,7 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp)
 	 * If we dont know the IP# for this host and itis a hostname and
 	 * not a ip# string, then try and find the appropriate host record.
 	 */
-	if (!aconf->connect_ip)
+	if (!aconf->connect_ip && !aconf->outgoing.file)
 	{
 		if (is_valid_ip(aconf->outgoing.hostname))
 		{
@@ -1861,7 +1861,7 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp)
 	 * Copy these in so we have something for error detection.
 	 */
 	strlcpy(client->name, aconf->servername, sizeof(client->name));
-	strlcpy(client->local->sockhost, aconf->outgoing.hostname, HOSTLEN + 1);
+	strlcpy(client->local->sockhost, aconf->outgoing.hostname ? aconf->outgoing.hostname : aconf->outgoing.file, HOSTLEN + 1);
 
 	if (!connect_server_helper(aconf, client))
 	{
@@ -1884,7 +1884,7 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp)
 	SetOutgoing(client);
 	irccounts.unknown++;
 	list_add(&client->lclient_node, &unknown_list);
-	set_sockhost(client, aconf->outgoing.hostname);
+	set_sockhost(client, aconf->outgoing.hostname ? aconf->outgoing.hostname : "127.0.0.1");
 	add_client_to_list(client);
 
 	if (aconf->outgoing.options & CONNECT_TLS)
@@ -1896,7 +1896,9 @@ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp)
 		fd_setselect(client->local->fd, FD_SELECT_WRITE, completed_connection, client);
 
 	unreal_log(ULOG_INFO, "link", "LINK_CONNECTING", client,
-		   "Trying to activate link with server $client ($link_block.ip:$link_block.port)...",
+		   aconf->outgoing.file
+		   ? "Trying to activate link with server $client ($link_block.file)..."
+		   : "Trying to activate link with server $client ($link_block.ip:$link_block.port)...",
 		   log_data_link_block(aconf));
 }
 
@@ -1911,21 +1913,23 @@ static int connect_server_helper(ConfigItem_link *aconf, Client *client)
 	char *bindip;
 	char buf[BUFSIZE];
 
-	if (!aconf->connect_ip)
+	if (!aconf->connect_ip && !aconf->outgoing.file)
 	{
 		unreal_log(ULOG_ERROR, "link", "LINK_ERROR_NOIP", client,
-		           "Connect to $client failed: no IP address to connect to",
+		           "Connect to $client failed: no IP address or file to connect to",
 		           log_data_link_block(aconf));
 		return 0; /* handled upstream or shouldn't happen */
 	}
-	
-	if (strchr(aconf->connect_ip, ':'))
+
+	if (aconf->outgoing.file)
+		SetUnixSocket(client);
+	else if (strchr(aconf->connect_ip, ':'))
 		SetIPV6(client);
 	
-	safe_strdup(client->ip, aconf->connect_ip);
+	safe_strdup(client->ip, aconf->connect_ip ? aconf->connect_ip : "127.0.0.1");
 	
 	snprintf(buf, sizeof buf, "Outgoing connection: %s", get_client_name(client, TRUE));
-	client->local->fd = fd_socket(IsIPV6(client) ? AF_INET6 : AF_INET, SOCK_STREAM, 0, buf);
+	client->local->fd = fd_socket(IsUnixSocket(client) ? AF_UNIX : (IsIPV6(client) ? AF_INET6 : AF_INET), SOCK_STREAM, 0, buf);
 	if (client->local->fd < 0)
 	{
 		if (ERRNO == P_EMFILE)
@@ -1949,7 +1953,7 @@ static int connect_server_helper(ConfigItem_link *aconf, Client *client)
 		return 0;
 	}
 
-	set_sockhost(client, aconf->outgoing.hostname);
+	set_sockhost(client, aconf->outgoing.hostname ? aconf->outgoing.hostname : "127.0.0.1");
 
 	if (!aconf->outgoing.bind_ip && iConf.link_bindip)
 		bindip = iConf.link_bindip;
@@ -1971,10 +1975,14 @@ static int connect_server_helper(ConfigItem_link *aconf, Client *client)
 
 	set_sock_opts(client->local->fd, client, IsIPV6(client));
 
-	if (!unreal_connect(client->local->fd, client->ip, aconf->outgoing.port, IsIPV6(client)))
+	if (!unreal_connect(client->local->fd,
+			    aconf->outgoing.file ? aconf->outgoing.file : client->ip,
+			    aconf->outgoing.port, client->local->socket_type))
 	{
 			unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
-				   "Connect to $client ($link_block.ip:$link_block.port) failed: $socket_error",
+				   aconf->outgoing.file
+				   ? "Connect to $client ($link_block.file) failed: $socket_error"
+				   : "Connect to $client ($link_block.ip:$link_block.port) failed: $socket_error",
 				   log_data_socket_error(client->local->fd),
 				   log_data_link_block(aconf));
 		return 0;
diff --git a/src/modules/sethost.c b/src/modules/sethost.c
@@ -143,7 +143,6 @@ CMD_FUNC(cmd_sethost)
 	if (MyConnect(client))
 	{
 		sendto_one(client, NULL, ":%s MODE %s :+xt", client->name, client->name);
-		sendnumeric(client, RPL_HOSTHIDDEN, vhost);
 		sendnotice(client, 
 		    "Your nick!user@host-mask is now (%s!%s@%s) - To disable it type /mode %s -x",
 		     client->name, client->user->username, vhost,
diff --git a/src/modules/sinfo.c b/src/modules/sinfo.c
@@ -1,7 +1,7 @@
 /*
  * cmd_sinfo - Server information
  * (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team.
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
diff --git a/src/modules/stats.c b/src/modules/stats.c
@@ -487,14 +487,14 @@ int stats_except(Client *client, const char *para)
 int stats_allow(Client *client, const char *para)
 {
 	ConfigItem_allow *allows;
-	ConfigItem_mask *m;
+	NameValuePrioList *m;
 
 	for (allows = conf_allow; allows; allows = allows->next)
 	{
-		for (m = allows->mask; m; m = m->next)
+		for (m = allows->match->printable_list; m; m = m->next)
 		{
 			sendnumeric(client, RPL_STATSILINE,
-				    m->mask, "-",
+				    namevalue_nospaces(m), "-",
 				    allows->maxperip,
 				    allows->global_maxperip,
 				    allows->class->name,
@@ -521,14 +521,14 @@ int stats_command(Client *client, const char *para)
 int stats_oper(Client *client, const char *para)
 {
 	ConfigItem_oper *o;
-	ConfigItem_mask *m;
+	NameValuePrioList *m;
 
 	for (o = conf_oper; o; o = o->next)
 	{
-		for (m = o->mask; m; m = m->next)
+		for (m = o->match->printable_list; m; m = m->next)
 		{
 			sendnumeric(client, RPL_STATSOLINE,
-			            'O', m->mask, o->name,
+			            'O', namevalue_nospaces(m), o->name,
 			            o->operclass ? o->operclass: "",
 			            o->class->name ? o->class->name : "");
 		}
@@ -668,15 +668,19 @@ int stats_uline(Client *client, const char *para)
 }
 int stats_vhost(Client *client, const char *para)
 {
-	ConfigItem_mask *m;
 	ConfigItem_vhost *vhosts;
+	NameValuePrioList *m;
 
 	for (vhosts = conf_vhost; vhosts; vhosts = vhosts->next)
 	{
-		for (m = vhosts->mask; m; m = m->next)
+		for (m = vhosts->match->printable_list; m; m = m->next)
 		{
-			sendtxtnumeric(client, "vhost %s%s%s %s %s", vhosts->virtuser ? vhosts->virtuser : "", vhosts->virtuser ? "@" : "",
-			     vhosts->virthost, vhosts->login, m->mask);
+			sendtxtnumeric(client, "vhost %s%s%s %s %s",
+			               vhosts->virtuser ? vhosts->virtuser : "",
+			               vhosts->virtuser ? "@" : "",
+			               vhosts->virthost,
+			               vhosts->login,
+			               namevalue_nospaces(m));
 		}
 	}
 	return 0;
@@ -935,12 +939,16 @@ int stats_set(Client *client, const char *para)
 int stats_tld(Client *client, const char *para)
 {
 	ConfigItem_tld *tld;
-	ConfigItem_mask *m;
+	NameValuePrioList *m;
 
 	for (tld = conf_tld; tld; tld = tld->next)
 	{
-		for (m = tld->mask; m; m = m->next)
-			sendnumeric(client, RPL_STATSTLINE, m->mask, tld->motd_file, tld->rules_file ? tld->rules_file : "none");
+		for (m = tld->match->printable_list; m; m = m->next)
+		{
+			sendnumeric(client, RPL_STATSTLINE, namevalue_nospaces(m),
+			            tld->motd_file,
+			            tld->rules_file ? tld->rules_file : "none");
+		}
 	}
 
 	return 0;
diff --git a/src/modules/svsmode.c b/src/modules/svsmode.c
@@ -395,7 +395,7 @@ void do_svsmode(Client *client, MessageTag *recv_mtags, int parc, const char *pa
 					 * so remove all oper-only modes and snomasks.
 					 */
 					if (MyUser(client))
-						RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL);
+						RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL, NULL);
 					remove_oper_privileges(target, 0);
 				}
 				goto setmodex;
diff --git a/src/modules/svsnoop.c b/src/modules/svsnoop.c
@@ -82,7 +82,7 @@ CMD_FUNC(cmd_svsnoop)
 					if (!list_empty(&acptr->special_node))
 						list_del(&acptr->special_node);
 
-					RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL);
+					RunHook(HOOKTYPE_LOCAL_OPER, client, 0, NULL, NULL);
 					remove_oper_privileges(acptr, 1);
 				}
 			}
diff --git a/src/modules/svso.c b/src/modules/svso.c
@@ -0,0 +1,138 @@
+/* src/modules/svso.c - Grant IRCOp rights (for Services)
+ * (C) Copyright 2022 Bram Matthys (Syzop) and the UnrealIRCd team
+ * License: GPLv2 or later
+ */
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+= {
+	"svso",
+	"6.0.0",
+	"Grant oper privileges via SVSO services command",
+	"UnrealIRCd Team",
+	"unrealircd-6",
+};
+
+/* Forward declarations */
+CMD_FUNC(cmd_svso);
+
+MOD_INIT()
+{
+	MARK_AS_OFFICIAL_MODULE(modinfo);
+
+	CommandAdd(modinfo->handle, "SVSO", cmd_svso, MAXPARA, CMD_USER|CMD_SERVER);
+	return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+	return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+	return MOD_SUCCESS;
+}
+
+/* Syntax: SVSO <uid|nick> <oper account> <operclass> <class> <modes> <snomask> <vhost>
+ * All these parameters need to be set, you cannot leave any of them out,
+ * HOWEVER some can be set to "-" to skip setting them, this is true for:
+ * <class>, <modes>, <snomask>, <vhost>
+ *
+ * In UnrealIRCd the <operclass> will be prefixed by "services:" if not already
+ * present. It is up to you to include or omit it.
+ */
+CMD_FUNC(cmd_svso)
+{
+	Client *acptr;
+	char oper_account[64];
+	const char *operclass;
+	const char *clientclass;
+	ConfigItem_class *clientclass_c;
+	const char *modes;
+	long modes_i = 0;
+	const char *snomask;
+	const char *vhost;
+
+	if (!IsULine(client))
+		return;
+
+	if ((parc < 8) || BadPtr(parv[7]))
+	{
+		sendnumeric(client, ERR_NEEDMOREPARAMS, "SVSO");
+		return;
+	}
+
+	operclass = parv[3];
+	clientclass = parv[4];
+	modes = parv[5];
+	snomask = parv[6];
+	vhost = parv[7];
+
+	acptr = find_user(parv[1], NULL);
+	if (!acptr)
+	{
+		sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
+		return;
+	}
+
+	if (!MyUser(acptr))
+	{
+		/* Forward it to the correct server, and we are done... */
+		sendto_one(acptr, recv_mtags, ":%s SVSO %s %s %s %s %s %s %s",
+		           client->name, acptr->id, parv[2], parv[3], parv[4], parv[5], parv[6], parv[7]);
+		return;
+	}
+
+	/* CAVEAT ALERT !
+	 * Don't mix up 'client' and 'acptr' below...
+	 * 'client' is the server or services pseudouser that requests the change
+	 * 'acptr' is the person that will be made OPER
+	 */
+
+	/* If we get here, we validate the request and then make the user oper. */
+	if (!find_operclass(operclass))
+	{
+		sendnumeric(client, ERR_CANNOTDOCOMMAND, "SVSO", "Operclass does not exist");
+		return;
+	}
+
+	/* Set any items to NULL if they are skipped (on request) */
+	if (!strcmp(clientclass, "-"))
+		clientclass = NULL;
+	if (!strcmp(modes, "-"))
+		modes = NULL;
+	if (!strcmp(snomask, "-"))
+		snomask = NULL;
+	if (!strcmp(vhost, "-"))
+		vhost = NULL;
+
+	/* First, maybe the user is oper already? Then de-oper them.. */
+	if (IsOper(acptr))
+	{
+		int was_hidden_oper = IsHideOper(acptr) ? 1 : 0;
+
+		list_del(&acptr->special_node);
+		RunHook(HOOKTYPE_LOCAL_OPER, acptr, 0, NULL, NULL);
+		remove_oper_privileges(acptr, 1);
+
+		if (!was_hidden_oper)
+			irccounts.operators--;
+		VERIFY_OPERCOUNT(acptr, "svso");
+
+	}
+
+	/* Prefix the oper block name with "services:" if it hasn't already */
+	if (!strncmp(parv[2], "services:", 9))
+		strlcpy(oper_account, parv[2], sizeof(oper_account));
+	else
+		snprintf(oper_account, sizeof(oper_account), "services:%s", parv[2]);
+
+	/* These needs to be looked up... */
+	clientclass_c = find_class(clientclass); /* NULL is fine! */
+	if (modes)
+		modes_i = set_usermode(modes);
+
+	if (!make_oper(acptr, oper_account, operclass, clientclass_c, modes_i, snomask, vhost))
+		sendnumeric(client, ERR_CANNOTDOCOMMAND, "SVSO", "Failed to make user oper");
+}
diff --git a/src/modules/targetfloodprot.c b/src/modules/targetfloodprot.c
@@ -1,6 +1,6 @@
 /* Target flood protection
  * (C)Copyright 2020 Bram Matthys and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
    
 #include "unrealircd.h"
diff --git a/src/modules/tkl.c b/src/modules/tkl.c
@@ -60,8 +60,9 @@ char *_tkl_type_config_string(TKL *tk);
 char *tkl_banexception_configname_to_chars(char *name);
 TKL *_tkl_add_serverban(int type, char *usermask, char *hostmask, char *reason, char *set_by,
                             time_t expire_at, time_t set_at, int soft, int flags);
-TKL *_tkl_add_banexception(int type, char *usermask, char *hostmask, char *reason, char *set_by,
-                               time_t expire_at, time_t set_at, int soft, char *bantypes, int flags);
+TKL *_tkl_add_banexception(int type, char *usermask, char *hostmask, SecurityGroup *match,
+                           char *reason, char *set_by,
+                           time_t expire_at, time_t set_at, int soft, char *bantypes, int flags);
 TKL *_tkl_add_nameban(int type, char *name, int hold, char *reason, char *set_by,
                           time_t expire_at, time_t set_at, int flags);
 TKL *_tkl_add_spamfilter(int type, unsigned short target, BanAction action, Match *match, char *set_by,
@@ -91,6 +92,7 @@ int check_mtag_spamfilters_present(void);
 int _join_viruschan(Client *client, TKL *tk, int type);
 void _spamfilter_build_user_string(char *buf, char *nick, Client *client);
 int _match_user(const char *rmask, Client *client, int options);
+int _unreal_match_iplist(Client *client, NameList *l);
 int _match_user_extended_server_ban(const char *banstr, Client *client);
 void ban_target_to_tkl_layer(BanTarget ban_target, BanAction action, Client *client, const char **tkl_username, const char **tkl_hostname);
 int _tkl_ip_hash(char *ip);
@@ -204,6 +206,7 @@ MOD_TEST()
 	EfunctionAddVoid(modinfo->handle, EFUNC_SENDNOTICE_TKL_DEL, _sendnotice_tkl_del);
 	EfunctionAdd(modinfo->handle, EFUNC_FIND_TKL_EXCEPTION, _find_tkl_exception);
 	EfunctionAddString(modinfo->handle, EFUNC_TKL_UHOST, _tkl_uhost);
+	EfunctionAdd(modinfo->handle, EFUNC_UNREAL_MATCH_IPLIST, _unreal_match_iplist);
 	return MOD_SUCCESS;
 }
 
@@ -678,7 +681,7 @@ int tkl_config_test_except(ConfigFile *cf, ConfigEntry *ce, int configtype, int 
 {
 	ConfigEntry *cep, *cepp;
 	int errors = 0;
-	int has_mask = 0;
+	char has_mask = 0, has_match = 0;
 
 	/* We are only interested in except { } blocks */
 	if (configtype != CONFIG_EXCEPT)
@@ -705,32 +708,18 @@ int tkl_config_test_except(ConfigFile *cf, ConfigEntry *ce, int configtype, int 
 	{
 		if (!strcmp(cep->name, "mask"))
 		{
-			if (cep->items)
-			{
-				/* mask { *@1.1.1.1; *@2.2.2.2; *@3.3.3.3; }; */
-				for (cepp = cep->items; cepp; cepp = cepp->next)
-				{
-					if (!cepp->name)
-					{
-						config_error_empty(cepp->file->filename,
-							cepp->line_number, "except ban", "mask");
-						errors++;
-						continue;
-					}
-					has_mask = 1;
-				}
-			} else
-			if (cep->value)
+			if (cep->value || cep->items)
 			{
-				/* mask *@1.1.1.1; */
-				if (!cep->value)
-				{
-					config_error_empty(cep->file->filename,
-						cep->line_number, "except ban", "mask");
-					errors++;
-					continue;
-				}
 				has_mask = 1;
+				test_match_block(cf, cep, &errors);
+			}
+		} else
+		if (!strcmp(cep->name, "match"))
+		{
+			if (cep->value || cep->items)
+			{
+				has_match = 1;
+				test_match_block(cf, cep, &errors);
 			}
 		} else
 		if (!strcmp(cep->name, "type"))
@@ -766,71 +755,28 @@ int tkl_config_test_except(ConfigFile *cf, ConfigEntry *ce, int configtype, int 
 		}
 	}
 
-	if (!has_mask)
+	if (!has_mask && !has_match)
 	{
 		config_error_missing(ce->file->filename, ce->line_number,
-			"except ban::mask");
+			"except ban::match");
 		errors++;
 	}
-
-	*errs = errors;
-	return errors ? -1 : 1;
-}
-
-void config_create_tkl_except(char *mask, char *bantypes)
-{
-	char *usermask = NULL;
-	char *hostmask = NULL;
-	int soft = 0;
-	char buf[256];
-	char mask1buf[512];
-	char mask2buf[512];
-	char *p;
-
-	if (*mask == '%')
-	{
-		soft = 1;
-		mask++;
-	}
-	strlcpy(buf, mask, sizeof(buf));
-	if (is_extended_server_ban(buf))
-	{
-		char *err = NULL;
-		if (!parse_extended_server_ban(buf, NULL, &err, 0, mask1buf, sizeof(mask1buf), mask2buf, sizeof(mask2buf)))
-		{
-			config_warn("Could not add extended server ban '%s': %s", buf, err);
-			return;
-		}
-		usermask = mask1buf;
-		hostmask = mask2buf;
-	} else
-	{
-		p = strchr(buf, '@');
-		if (!p)
-		{
-			usermask = "*";
-			hostmask = buf;
-		} else {
-			*p++ = '\0';
-			usermask = buf;
-			hostmask = p;
-		}
-	}
-
-	if ((*usermask == ':') || (*hostmask == ':'))
+	if (has_mask && has_match)
 	{
-		config_error("Cannot add illegal ban '%s': for a given user@host - neither "
-		             "user nor host may start with a : character (semicolon)", mask);
-		return;
+		config_error("%s:%d: You cannot have both ::mask and ::match. "
+		             "You should only use except::match.",
+		             ce->file->filename, ce->line_number);
+		errors++;
 	}
 
-	tkl_add_banexception(TKL_EXCEPTION, usermask, hostmask, "Added in configuration file",
-	                     "-config-", 0, TStime(), soft, bantypes, TKL_FLAG_CONFIG);
+	*errs = errors;
+	return errors ? -1 : 1;
 }
 
 int tkl_config_run_except(ConfigFile *cf, ConfigEntry *ce, int configtype)
 {
 	ConfigEntry *cep, *cepp;
+	SecurityGroup *match = NULL;
 	char bantypes[64];
 
 	/* We are only interested in except { } blocks */
@@ -867,6 +813,10 @@ int tkl_config_run_except(ConfigFile *cf, ConfigEntry *ce, int configtype)
 				char *str = tkl_banexception_configname_to_chars(cep->value);
 				strlcat(bantypes, str, sizeof(bantypes));
 			}
+		} else
+		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
+		{
+			conf_match_block(cf, cep, &match);
 		}
 	}
 
@@ -885,24 +835,8 @@ int tkl_config_run_except(ConfigFile *cf, ConfigEntry *ce, int configtype)
 			abort(); /* someone can't code */
 	}
 
-	/* Now walk through all mask entries */
-	for (cep = ce->items; cep; cep = cep->next)
-	{
-		if (!strcmp(cep->name, "mask"))
-		{
-			if (cep->items)
-			{
-				/* mask { *@1.1.1.1; *@2.2.2.2; *@3.3.3.3; }; */
-				for (cepp = cep->items; cepp; cepp = cepp->next)
-					config_create_tkl_except(cepp->name, bantypes);
-			} else
-			if (cep->value)
-			{
-				/* mask *@1.1.1.1; */
-				config_create_tkl_except(cep->value, bantypes);
-			}
-		}
-	}
+	tkl_add_banexception(TKL_EXCEPTION, "-", "-", match, "Added in configuration file",
+	                     "-config-", 0, TStime(), 0, bantypes, TKL_FLAG_CONFIG);
 
 	return 1;
 }
@@ -2564,6 +2498,9 @@ TKL *_tkl_add_serverban(int type, char *usermask, char *hostmask, char *reason, 
  * @param type                TKL_EXCEPTION or TKLEXCEPT|TKL_GLOBAL.
  * @param usermask            The user mask
  * @param hostmask            The host mask
+ * @param match               A securitygroup used for matching (can be NULL,
+ *                            if not NULL then this field is used as-is and not copied
+ *                            so caller should not free!)
  * @param reason              The reason for the ban
  * @param set_by              Who (or what) set the ban
  * @param expire_at           When will the ban expire (0 for permanent)
@@ -2577,15 +2514,15 @@ TKL *_tkl_add_serverban(int type, char *usermask, char *hostmask, char *reason, 
  * Be sure not to call this function for spamfilters,
  * qlines or exempts, which have their own function!
  */
-TKL *_tkl_add_banexception(int type, char *usermask, char *hostmask, char *reason, char *set_by,
-                               time_t expire_at, time_t set_at, int soft, char *bantypes, int flags)
+TKL *_tkl_add_banexception(int type, char *usermask, char *hostmask, SecurityGroup *match,
+                           char *reason, char *set_by,
+                           time_t expire_at, time_t set_at, int soft, char *bantypes, int flags)
 {
 	TKL *tkl;
 	int index, index2;
 
 	if (!TKLIsBanExceptionType(type))
 		abort();
-
 	tkl = safe_alloc(sizeof(TKL));
 	/* First the common fields */
 	tkl->type = type;
@@ -2597,6 +2534,7 @@ TKL *_tkl_add_banexception(int type, char *usermask, char *hostmask, char *reaso
 	tkl->ptr.banexception = safe_alloc(sizeof(BanException));
 	safe_strdup(tkl->ptr.banexception->usermask, usermask);
 	safe_strdup(tkl->ptr.banexception->hostmask, hostmask);
+	tkl->ptr.banexception->match = match;
 	if (soft)
 		tkl->ptr.banexception->subtype = TKL_SUBTYPE_SOFT;
 	safe_strdup(tkl->ptr.banexception->bantypes, bantypes);
@@ -2702,6 +2640,8 @@ void _free_tkl(TKL *tkl)
 	{
 		safe_free(tkl->ptr.banexception->usermask);
 		safe_free(tkl->ptr.banexception->hostmask);
+		if (tkl->ptr.banexception->match)
+			free_security_group(tkl->ptr.banexception->match);
 		safe_free(tkl->ptr.banexception->bantypes);
 		safe_free(tkl->ptr.banexception->reason);
 		safe_free(tkl->ptr.banexception);
@@ -2770,7 +2710,7 @@ static void add_default_exempts(void)
 	 * Currently the list is: gline, kline, gzline, zline, shun, blacklist,
 	 *                        connect-flood, handshake-data-flood.
 	 */
-	tkl_add_banexception(TKL_EXCEPTION, "*", "127.0.0.0/8", "localhost is always exempt",
+	tkl_add_banexception(TKL_EXCEPTION, "*", "127.0.0.0/8", NULL, "localhost is always exempt",
 	                     "-default-", 0, TStime(), 0, "GkZzsbcd", TKL_FLAG_CONFIG);
 }
 
@@ -2962,6 +2902,10 @@ static int find_tkl_exception_matcher(Client *client, int ban_type, TKL *except_
 	if (!tkl_banexception_matches_type(except_tkl, ban_type))
 		return 0;
 
+	/* For config file except ban { } we use security groups instead of simple user/host */
+	if (except_tkl->ptr.banexception->match)
+		return user_allowed_by_security_group(client, except_tkl->ptr.banexception->match);
+
 	tkl_uhost(except_tkl, uhost, sizeof(uhost), NO_SOFT_PREFIX);
 
 	if (match_user(uhost, client, MATCH_CHECK_REAL))
@@ -3565,12 +3509,26 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
 	} else
 	if (TKLIsBanException(tkl))
 	{
-		char uhostbuf[BUFSIZE];
-		char *uhost = tkl_uhost(tkl, uhostbuf, sizeof(uhostbuf), 0);
-		sendnumeric(client, RPL_STATSEXCEPTTKL, uhost,
-			   tkl->ptr.banexception->bantypes,
-			   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
-			   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason);
+		if (tkl->ptr.banexception->match)
+		{
+			/* Config-added: uses security groups */
+			NameValuePrioList *m;
+			for (m = tkl->ptr.banexception->match->printable_list; m; m = m->next)
+			{
+				sendnumeric(client, RPL_STATSEXCEPTTKL, namevalue_nospaces(m),
+					   tkl->ptr.banexception->bantypes,
+					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
+					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason);
+			}
+		} else {
+			/* IRC-added: uses simple user/host mask */
+			char uhostbuf[BUFSIZE];
+			char *uhost = tkl_uhost(tkl, uhostbuf, sizeof(uhostbuf), 0);
+			sendnumeric(client, RPL_STATSEXCEPTTKL, uhost,
+				   tkl->ptr.banexception->bantypes,
+				   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
+				   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason);
+		}
 	} else
 	{
 		/* That's weird, unknown TKL type */
@@ -3706,7 +3664,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
 	} else
 	{
 		unreal_log(ULOG_FATAL, "tkl", "BUG_TKL_SYNC_SEND_ENTRY", NULL,
-			   "[BUG] tkl_sync_send_entry() called for '%s' but unknown type: $tkl.type_string ($tkl_type_int)",
+			   "[BUG] tkl_sync_send_entry() called, but unknown type: $tkl.type_string ($tkl_type_int)",
 			   log_data_tkl("tkl", tkl),
 			   log_data_integer("tkl_type_int", typ));
 		abort();
@@ -4072,7 +4030,7 @@ CMD_FUNC(cmd_tkl_add)
 		{
 			tkl_entry_exists = 1;
 		} else {
-			tkl = tkl_add_banexception(type, usermask, hostmask, reason,
+			tkl = tkl_add_banexception(type, usermask, hostmask, NULL, reason,
 			                           set_by, expire_at, set_at, softban, bantypes, 0);
 		}
 	} else
@@ -4123,7 +4081,7 @@ CMD_FUNC(cmd_tkl_add)
 		{
 			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
 				"Invalid TKL entry from $client: "
-				"Spamfilter '$spamfilter_string' has unkown match-type '$spamfilter_type'",
+				"Spamfilter '$spamfilter_string' has unknown match-type '$spamfilter_type'",
 				log_data_string("spamfilter_string", match_string),
 				log_data_string("spamfilter_type", parv[10]));
 			return;
@@ -4133,7 +4091,7 @@ CMD_FUNC(cmd_tkl_add)
 		{
 			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
 				"Invalid TKL entry from $client: "
-				"Spamfilter '$spamfilter_string' has unkown targets '$spamfilter_targets'",
+				"Spamfilter '$spamfilter_string' has unknown targets '$spamfilter_targets'",
 				log_data_string("spamfilter_string", match_string),
 				log_data_string("spamfilter_targets", parv[3]));
 			return;
@@ -4143,7 +4101,7 @@ CMD_FUNC(cmd_tkl_add)
 		{
 			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
 				"Invalid TKL entry from $client: "
-				"Spamfilter '$spamfilter_string' has unkown action '$spamfilter_action'",
+				"Spamfilter '$spamfilter_string' has unknown action '$spamfilter_action'",
 				log_data_string("spamfilter_string", match_string),
 				log_data_string("spamfilter_action", parv[4]));
 			return;
@@ -4312,7 +4270,7 @@ CMD_FUNC(cmd_tkl_del)
 		{
 			unreal_log(ULOG_WARNING, "tkl", "TKL_DEL_INVALID", client,
 				"Invalid TKL deletion request from $client: "
-				"Spamfilter '$spamfilter_string' has unkown targets '$spamfilter_targets'",
+				"Spamfilter '$spamfilter_string' has unknown targets '$spamfilter_targets'",
 				log_data_string("spamfilter_string", match_string),
 				log_data_string("spamfilter_targets", parv[3]));
 			return;
@@ -4322,7 +4280,7 @@ CMD_FUNC(cmd_tkl_del)
 		{
 			unreal_log(ULOG_WARNING, "tkl", "TKL_DEL_INVALID", client,
 				"Invalid TKL deletion request from $client: "
-				"Spamfilter '$spamfilter_string' has unkown action '$spamfilter_action'",
+				"Spamfilter '$spamfilter_string' has unknown action '$spamfilter_action'",
 				log_data_string("spamfilter_string", match_string),
 				log_data_string("spamfilter_action", parv[4]));
 			return;
@@ -5147,6 +5105,102 @@ int _match_user(const char *rmask, Client *client, int options)
 	return 0; /* NOMATCH: nothing of the above matched */
 }
 
+/** Returns 1 if the user is allowed by any of the security groups in the named list.
+ * This is only used by security-group::security-group and
+ * security-group::exclude-security-group.
+ * @param client	Client to check
+ * @param l		The NameList
+ * @returns 1 if any of the security groups match, 0 if none of them matched.
+ */
+int _unreal_match_iplist(Client *client, NameList *l)
+{
+	char client_ipv6 = 0;
+	char clientip[IPSZ], maskip[IPSZ];
+
+	if (!client->ip)
+		return 0; /* unusual, maybe services? */
+
+	if (strchr(client->ip, ':'))
+	{
+		client_ipv6 = 1;
+		if (!inet_pton(AF_INET6, client->ip, clientip))
+			return 0; /* unusual failure */
+	} else {
+		if (!inet_pton(AF_INET, client->ip, clientip))
+			return 0; /* unusual failure */
+	}
+
+	for (; l; l = l->next)
+	{
+		char mask[512], *p;
+		int cidr = -1; /* CIDR length, -1 for no CIDR */
+
+		strlcpy(mask, l->name, sizeof(mask));
+		p = strchr(mask, '/');
+		if (p)
+		{
+			*p++ = '\0';
+			cidr = atoi(p);
+			if (cidr <= 0)
+				return 0; /* NOMATCH: invalid CIDR */
+		}
+
+		/* Three possible types: wildcard, ipv6, ipv4 */
+
+		if (strchr(mask, '*') || strchr(mask, '?'))
+		{
+			/* Wildcards */
+			if (match_simple(mask, client->ip))
+				return 1; /* MATCH by wildcard IP */
+		}
+		else if (strchr(mask, ':'))
+		{
+			/* IPv6 */
+			if (!client_ipv6)
+				continue; /* NOMATCH: client is IPv4 */
+			if (!inet_pton(AF_INET6, mask, maskip))
+				continue; /* NOMATCH: invalid IPv6 IP in mask */
+			if (cidr < 0)
+			{
+				/* Try to match by exact IP */
+				if (comp_with_mask(clientip, maskip, 128))
+					return 1; /* MATCH by exact IP */
+			} else
+			if (cidr > 128)
+			{
+				continue; /* NOMATCH: invalid CIDR */
+			} else
+			if (comp_with_mask(clientip, maskip, cidr))
+			{
+				return 1; /* MATCH by CIDR */
+			}
+		} else
+		{
+			/* IPv4 */
+			if (client_ipv6)
+				continue; /* NOMATCH: client is IPv6 */
+			if (!inet_pton(AF_INET, mask, maskip))
+				continue; /* NOMATCH: invalid IPv6 IP in mask */
+			if (cidr < 0)
+			{
+				/* Try to match by exact IP */
+				if (comp_with_mask(clientip, maskip, 32))
+					return 1; /* MATCH: by exact IP */
+			} else
+			if (cidr > 32)
+			{
+				continue; /* NOMATCH: invalid CIDR */
+			} else
+			if (comp_with_mask(clientip, maskip, cidr))
+			{
+				return 1; /* MATCH by CIDR */
+			}
+		}
+	}
+	return 0;
+}
+
+
 int _match_user_extended_server_ban(const char *banstr, Client *client)
 {
 	const char *nextbanstr;
diff --git a/src/modules/tkldb.c b/src/modules/tkldb.c
@@ -625,6 +625,7 @@ int read_tkldb(void)
 			{
 				tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask,
 				                     tkl->ptr.banexception->hostmask,
+				                     NULL,
 				                     tkl->ptr.banexception->reason,
 				                     tkl->set_by, tkl->expire_at,
 				                     tkl->set_at, softban,
diff --git a/src/modules/tls_antidos.c b/src/modules/tls_antidos.c
@@ -5,7 +5,7 @@
  *
  * (C) Copyright 2015- Bram Matthys and the UnrealIRCd team.
  * 
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
diff --git a/src/modules/tls_cipher.c b/src/modules/tls_cipher.c
@@ -1,7 +1,7 @@
 /*
  * Store TLS cipher in ModData
  * (C) Copyright 2021-.. Syzop and The UnrealIRCd Team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -22,6 +22,7 @@ void tls_cipher_unserialize(const char *str, ModData *m);
 int tls_cipher_handshake(Client *client);
 int tls_cipher_connect(Client *client);
 int tls_cipher_whois(Client *client, Client *target);
+int log_tls_cipher(Client *client, int detail, json_t *j);
 
 ModDataInfo *tls_cipher_md; /* Module Data structure which we acquire */
 
@@ -45,6 +46,8 @@ ModDataInfo mreq;
 	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, tls_cipher_handshake);
 	HookAdd(modinfo->handle, HOOKTYPE_SERVER_HANDSHAKE_OUT, 0, tls_cipher_handshake);
 
+	HookAdd(modinfo->handle, HOOKTYPE_JSON_EXPAND_CLIENT, 0, log_tls_cipher);
+
 	return MOD_SUCCESS;
 }
 
@@ -89,3 +92,24 @@ void tls_cipher_unserialize(const char *str, ModData *m)
 {
 	safe_strdup(m->str, str);
 }
+
+int log_tls_cipher(Client *client, int detail, json_t *j)
+{
+	json_t *tls;
+	const char *str;
+
+	str = moddata_client_get(client, "tls_cipher");
+	if (!str)
+		return 0;
+
+	tls = json_object_get(j, "tls");
+	if (!tls)
+	{
+		tls = json_object();
+		json_object_set_new(j, "tls", tls);
+	}
+
+	json_object_set_new(tls, "cipher", json_string_unreal(str));
+
+	return 0;
+}
diff --git a/src/modules/unreal_server_compat.c b/src/modules/unreal_server_compat.c
@@ -1,7 +1,7 @@
 /*
  * unreal_server_compat - Compatibility with pre-U6 servers
  * (C) Copyright 2016-2021 Bram Matthys (Syzop)
- * License: GPLv2
+ * License: GPLv2 or later
  *
  * Currently the only purpose of this module is to rewrite MODE
  * and SJOIN lines to older servers so any bans/exempts/invex
diff --git a/src/modules/vhost.c b/src/modules/vhost.c
@@ -89,7 +89,7 @@ CMD_FUNC(cmd_vhost)
 		return;
 	}
 	
-	if (!unreal_mask_match(client, vhost->mask))
+	if (!user_allowed_by_security_group(client, vhost->match))
 	{
 		unreal_log(ULOG_WARNING, "vhost", "VHOST_FAILED", client,
 		           "Failed VHOST attempt by $client.details [reason: $reason] [vhost-block: $vhost_block]",
@@ -157,7 +157,6 @@ CMD_FUNC(cmd_vhost)
 		for (s = vhost->swhois; s; s = s->next)
 			swhois_add(client, "vhost", -100, s->line, &me, NULL);
 	}
-	sendnumeric(client, RPL_HOSTHIDDEN, vhost->virthost);
 	sendnotice(client, "*** Your vhost is now %s%s%s",
 		vhost->virtuser ? vhost->virtuser : "",
 		vhost->virtuser ? "@" : "",
diff --git a/src/modules/watch-backend.c b/src/modules/watch-backend.c
@@ -85,7 +85,7 @@ MOD_INIT()
 	}
 	LoadPersistentPointer(modinfo, watchTable, watch_generic_free);
 	if (watchTable == NULL)
-		watchTable = safe_alloc(sizeof(Watch) * WATCH_HASH_TABLE_SIZE);
+		watchTable = safe_alloc(sizeof(Watch *) * WATCH_HASH_TABLE_SIZE);
 
 	memset(&mreq, 0 , sizeof(mreq));
 	mreq.type = MODDATATYPE_LOCAL_CLIENT;
diff --git a/src/modules/watch.c b/src/modules/watch.c
@@ -119,8 +119,6 @@ static void show_watch_removed(Client *client, char *name)
 	}
 }
 
-static char buf[BUFSIZE];
-
 #define WATCHES(client) (moddata_local_client(client, watchCounterMD).i)
 #define WATCH(client) (moddata_local_client(client, watchListMD).ptr)
 
@@ -130,6 +128,7 @@ static char buf[BUFSIZE];
 CMD_FUNC(cmd_watch)
 {
 	char request[BUFSIZE];
+	char buf[BUFSIZE];
 	Client *target;
 	char *s, *user;
 	char *p = NULL, *def = "l";
diff --git a/src/modules/websocket.c b/src/modules/websocket.c
@@ -1,7 +1,7 @@
 /*
  * websocket - WebSocket support (RFC6455)
  * (C)Copyright 2016 Bram Matthys and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  * This module was sponsored by Aberrant Software Inc.
  */
    
diff --git a/src/modules/whois.c b/src/modules/whois.c
@@ -52,7 +52,6 @@ struct WhoisConfig {
 };
 
 /* Global variables */
-static char buf[BUFSIZE];
 WhoisConfig *whoisconfig = NULL;
 
 /* Forward declarations */
@@ -147,6 +146,8 @@ static void whois_config_setdefaults(void)
 
 	whois_config_add("reputation", WHOIS_CONFIG_USER_OPER, WHOIS_CONFIG_DETAILS_FULL);
 
+	whois_config_add("security-groups", WHOIS_CONFIG_USER_OPER, WHOIS_CONFIG_DETAILS_FULL);
+
 	whois_config_add("geo", WHOIS_CONFIG_USER_OPER, WHOIS_CONFIG_DETAILS_FULL);
 
 	whois_config_add("certfp", WHOIS_CONFIG_USER_EVERYONE, WHOIS_CONFIG_DETAILS_FULL);
@@ -294,6 +295,7 @@ CMD_FUNC(cmd_whois)
 	char *p = NULL;
 	int len, mlen;
 	char querybuf[BUFSIZE];
+	char buf[BUFSIZE];
 	int ntargets = 0;
 	int maxtargets = max_targets_for_command("WHOIS");
 
@@ -561,6 +563,51 @@ CMD_FUNC(cmd_whois)
 			}
 		}
 
+		/* The following code deals with security-groups */
+		policy = whois_get_policy(client, target, "security-groups");
+		if ((policy > WHOIS_CONFIG_DETAILS_NONE) && !IsULine(target))
+		{
+			SecurityGroup *s;
+			int security_groups_whois_lines = 0;
+
+			mlen = strlen(me.name) + strlen(client->name) + 10 + strlen(target->name) + strlen("is in security-groups: ");
+
+			if (user_allowed_by_security_group_name(target, "known-users"))
+				strlcpy(buf, "known-users,", sizeof(buf));
+			else
+				strlcpy(buf, "unknown-users,", sizeof(buf));
+			len = strlen(buf);
+
+			for (s = securitygroups; s; s = s->next)
+			{
+				if (len + strlen(s->name) > (size_t)BUFSIZE - 4 - mlen)
+				{
+					buf[len-1] = '\0';
+					add_nvplist_numeric_fmt(&list, -15000-security_groups_whois_lines, "security-groups",
+					                        target, RPL_WHOISSPECIAL,
+								"%s :is in security-groups: %s", target->name, buf);
+					security_groups_whois_lines++;
+					*buf = '\0';
+					len = 0;
+				}
+				if (strcmp(s->name, "known-users") && user_allowed_by_security_group(target, s))
+				{
+					strcpy(buf + len, s->name);
+					len += strlen(buf+len);
+					strcpy(buf + len, ",");
+					len++;
+				}
+			}
+
+			if (*buf)
+			{
+				buf[len-1] = '\0';
+				add_nvplist_numeric_fmt(&list, -15000-security_groups_whois_lines, "security-groups",
+				                        client, RPL_WHOISSPECIAL,
+							"%s :is in security-groups: %s", target->name, buf);
+				security_groups_whois_lines++;
+			}
+		}
 		if (MyUser(target) && IsShunned(target) && (whois_get_policy(client, target, "shunned") > WHOIS_CONFIG_DETAILS_NONE))
 		{
 			add_nvplist_numeric(&list, -20000, "shunned", client, RPL_WHOISSPECIAL,
diff --git a/src/modules/whox.c b/src/modules/whox.c
@@ -3,7 +3,7 @@
  * was originally made for tircd and modified to work with u4.
  * - 2018 i <ircd@servx.org>
  * - 2019 Bram Matthys (Syzop) <syzop@vulnscan.org>
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
diff --git a/src/operclass.c b/src/operclass.c
@@ -1,6 +1,6 @@
 /** Oper classes code.
  * (C) Copyright 2015-present tmcarthur and the UnrealIRCd team
- * License: GPLv2
+ * License: GPLv2 or later
  */
 
 #include "unrealircd.h"
@@ -282,6 +282,7 @@ OperPermission ValidatePermissionsForPathEx(OperClassACL *acl, OperClassACLPath 
 OperPermission ValidatePermissionsForPath(const char *path, Client *client, Client *victim, Channel *channel, const void *extra)
 {
 	ConfigItem_oper *ce_oper;
+	const char *operclass;
 	ConfigItem_operclass *ce_operClass;
 	OperClass *oc = NULL;
 	OperClassACLPath *operPath;
@@ -299,14 +300,17 @@ OperPermission ValidatePermissionsForPath(const char *path, Client *client, Clie
 	ce_oper = find_oper(client->user->operlogin);
 	if (!ce_oper)
 	{
-		return OPER_DENY;
+		operclass = moddata_client_get(client, "operclass");
+		if (!operclass)
+			return OPER_DENY;
+	} else
+	{
+		operclass = ce_oper->operclass;
 	}
-	
-	ce_operClass = find_operclass(ce_oper->operclass);
+
+	ce_operClass = find_operclass(operclass);
 	if (!ce_operClass)
-	{
 		return OPER_DENY;
-	}
 
 	oc = ce_operClass->classStruct;
 	operPath = OperClass_parsePath(path);
diff --git a/src/securitygroup.c b/src/securitygroup.c
@@ -0,0 +1,840 @@
+/*
+ * Mask & security-group routines.
+ * (C) Copyright 2015-.. Syzop and the UnrealIRCd team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "unrealircd.h"
+
+/* Global variables */
+SecurityGroup *securitygroups = NULL;
+
+/** Free all masks in the mask list */
+void unreal_delete_masks(ConfigItem_mask *m)
+{
+	ConfigItem_mask *m_next;
+
+	for (; m; m = m_next)
+	{
+		m_next = m->next;
+
+		safe_free(m->mask);
+
+		safe_free(m);
+	}
+}
+
+/** Internal function to add one individual mask to the list */
+static void unreal_add_mask(ConfigItem_mask **head, ConfigEntry *ce)
+{
+	ConfigItem_mask *m = safe_alloc(sizeof(ConfigItem_mask));
+
+	/* Since we allow both mask "xyz"; and mask { abc; def; };... */
+	if (ce->value)
+		safe_strdup(m->mask, ce->value);
+	else
+		safe_strdup(m->mask, ce->name);
+
+	add_ListItem((ListStruct *)m, (ListStruct **)head);
+}
+
+/** Add mask entries from config */
+void unreal_add_masks(ConfigItem_mask **head, ConfigEntry *ce)
+{
+	if (ce->items)
+	{
+		ConfigEntry *cep;
+		for (cep = ce->items; cep; cep = cep->next)
+			unreal_add_mask(head, cep);
+	} else
+	{
+		unreal_add_mask(head, ce);
+	}
+}
+
+/** Check if a client matches any of the masks in the mask list.
+ * The following rules apply:
+ * - If you have only negating entries, like '!abc' and '!def', then
+ *   we assume an implicit * rule first, since that is clearly what
+ *   the user wants.
+ * - If you have a mix, like '*.com', '!irc1*', '!irc2*' then the
+ *   implicit * is dropped and we assume you only want to match *.com,
+ *   with the exception of irc1*.com and irc2*.com.
+ * - If you only have normal entries without ! then things are
+ *   as they always are.
+ * @param client	The client to run the mask match against
+ * @param mask		The mask entry from the config file
+ * @returns 1 on match, 0 on non-match.
+ */
+int unreal_mask_match(Client *client, ConfigItem_mask *mask)
+{
+	int retval = 1;
+	ConfigItem_mask *m;
+
+	if (!mask)
+		return 0; /* Empty mask block is no match */
+
+	/* First check normal matches (without ! prefix) */
+	for (m = mask; m; m = m->next)
+	{
+		if (m->mask[0] != '!')
+		{
+			retval = 0; /* no implicit * */
+			if (match_user(m->mask, client, MATCH_CHECK_REAL|MATCH_CHECK_EXTENDED))
+			{
+				retval = 1;
+				break;
+			}
+		}
+	}
+
+	if (retval)
+	{
+		/* We matched. Check for exceptions (with ! prefix) */
+		for (m = mask; m; m = m->next)
+		{
+			if ((m->mask[0] == '!') && match_user(m->mask+1, client, MATCH_CHECK_REAL|MATCH_CHECK_EXTENDED))
+				return 0;
+		}
+	}
+
+	return retval;
+}
+
+/** Check if a string matches any of the masks in the mask list.
+ * The following rules apply:
+ * - If you have only negating entries, like '!abc' and '!def', then
+ *   we assume an implicit * rule first, since that is clearly what
+ *   the user wants.
+ * - If you have a mix, like '*.com', '!irc1*', '!irc2*' then the
+ *   implicit * is dropped and we assume you only want to match *.com,
+ *   with the exception of irc1*.com and irc2*.com.
+ * - If you only have normal entries without ! then things are
+ *   as they always are.
+ * @param name	The name to run the mask matching on
+ * @param mask	The mask entry from the config file
+ * @returns 1 on match, 0 on non-match.
+ */
+int unreal_mask_match_string(const char *name, ConfigItem_mask *mask)
+{
+	int retval = 1;
+	ConfigItem_mask *m;
+
+	if (!mask)
+		return 0; /* Empty mask block is no match */
+
+	/* First check normal matches (without ! prefix) */
+	for (m = mask; m; m = m->next)
+	{
+		if (m->mask[0] != '!')
+		{
+			retval = 0; /* no implicit * */
+			if (match_simple(m->mask, name))
+			{
+				retval = 1;
+				break;
+			}
+		}
+	}
+
+	if (retval)
+	{
+		/* We matched. Check for exceptions (with ! prefix) */
+		for (m = mask; m; m = m->next)
+		{
+			if ((m->mask[0] == '!') && match_simple(m->mask+1, name))
+				return 0;
+		}
+	}
+
+	return retval;
+}
+
+#define CheckNullX(x) if ((!(x)->value) || (!(*((x)->value)))) { config_error("%s:%i: missing parameter", (x)->file->filename, (x)->line_number); *errors = *errors + 1; return 0; }
+int test_match_item(ConfigFile *conf, ConfigEntry *cep, int *errors)
+{
+	ConfigEntry *cepp;
+
+	if (!strcmp(cep->name, "webirc") || !strcmp(cep->name, "exclude-webirc"))
+	{
+		CheckNullX(cep);
+	} else
+	if (!strcmp(cep->name, "identified") || !strcmp(cep->name, "exclude-identified"))
+	{
+		CheckNullX(cep);
+	} else
+	if (!strcmp(cep->name, "tls") || !strcmp(cep->name, "exclude-tls"))
+	{
+		CheckNullX(cep);
+	} else
+	if (!strcmp(cep->name, "reputation-score") || !strcmp(cep->name, "exclude-reputation-score"))
+	{
+		const char *str = cep->value;
+		int v;
+		CheckNullX(cep);
+		if (*str == '<')
+			str++;
+		v = atoi(str);
+		if ((v < 1) || (v > 10000))
+		{
+			config_error("%s:%i: %s needs to be a value of 1-10000",
+				cep->file->filename, cep->line_number, cep->name);
+			*errors = *errors + 1;
+		}
+	} else
+	if (!strcmp(cep->name, "connect-time") || !strcmp(cep->name, "exclude-connect-time"))
+	{
+		const char *str = cep->value;
+		long v;
+		CheckNullX(cep);
+		if (*str == '<')
+			str++;
+		v = config_checkval(str, CFG_TIME);
+		if (v < 1)
+		{
+			config_error("%s:%i: %s needs to be a time value (and more than 0 seconds)",
+				cep->file->filename, cep->line_number, cep->name);
+			*errors = *errors + 1;
+		}
+	} else
+	if (!strcmp(cep->name, "mask") || !strcmp(cep->name, "include-mask") || !strcmp(cep->name, "exclude-mask"))
+	{
+		for (cepp = cep->items; cepp; cepp = cepp->next)
+		{
+			if (!strcmp(cepp->name, "mask"))
+				continue;
+			if (cepp->items || cepp->value)
+			{
+				config_error("%s:%i: security-group::mask should contain hostmasks only. "
+				             "Perhaps you meant to use it in security-group { %s ... } directly?",
+				             cepp->file->filename, cepp->line_number,
+				             cepp->name);
+				*errors = *errors + 1;
+			}
+		}
+	} else
+	if (!strcmp(cep->name, "ip"))
+	{
+	} else
+	if (!strcmp(cep->name, "security-group") || !strcmp(cep->name, "exclude-security-group"))
+	{
+	} else
+	{
+		/* Let's see if an extended server ban exists for this item... */
+		Extban *extban;
+		if (!strncmp(cep->name, "exclude-", 8))
+			extban = findmod_by_bantype_raw(cep->name+8, strlen(cep->name+8));
+		else
+			extban = findmod_by_bantype_raw(cep->name, strlen(cep->name));
+		if (extban && (extban->options & EXTBOPT_TKL) && (extban->is_banned_events & BANCHK_TKL))
+		{
+			test_extended_list(extban, cep, errors);
+			return 1; /* Yup, handled */
+		}
+		return 0; /* Unhandled: unknown item for us */
+	}
+	return 1; /* Handled, but there could be errors */
+}
+
+int test_match_block(ConfigFile *conf, ConfigEntry *ce, int *errors_out)
+{
+	int errors = 0;
+	ConfigEntry *cep;
+
+	/* (If there is only a ce->value, trust that it is OK) */
+
+	/* Test ce->items... */
+	for (cep = ce->items; cep; cep = cep->next)
+	{
+		/* Only complain about things with values,
+		 * as valueless things like "10.0.0.0/8" are treated as a mask.
+		 */
+		if (!test_match_item(conf, cep, &errors) && cep->value)
+		{
+			config_error_unknown(cep->file->filename, cep->line_number,
+				ce->name, cep->name);
+			errors++;
+			continue;
+		}
+	}
+
+	*errors_out = *errors_out + errors;
+	return errors ? 0 : 1;
+}
+
+#define tmbbw_is_wildcard(x)	(!strcmp(x, "*") || !strcmp(x, "*@*"))
+int test_match_block_too_broad(ConfigFile *conf, ConfigEntry *ce)
+{
+	ConfigEntry *cep, *cepp;
+
+	// match *;
+	if (ce->value && tmbbw_is_wildcard(ce->value))
+		return 1;
+
+	for (cep = ce->items; cep; cep = cep->next)
+	{
+		// match { *; }
+		if (!cep->value && tmbbw_is_wildcard(cep->name))
+			return 1;
+		if (!strcmp(cep->name, "mask") || !strcmp(cep->name, "include-mask") || !strcmp(cep->name, "ip"))
+		{
+			// match { mask *; }
+			if (cep->value && tmbbw_is_wildcard(cep->value))
+				return 1;
+			// match { mask { *; } }
+			for (cepp = cep->items; cepp; cepp = cepp->next)
+				if (tmbbw_is_wildcard(cepp->name))
+					return 1;
+		}
+	}
+
+	return 0;
+}
+
+int _test_security_group(ConfigFile *conf, ConfigEntry *ce)
+{
+	int errors = 0;
+	ConfigEntry *cep;
+
+	/* First, check the name of the security group */
+	if (!ce->value)
+	{
+		config_error("%s:%i: security-group block needs a name, eg: security-group web-users {",
+			ce->file->filename, ce->line_number);
+		errors++;
+	} else {
+		if (!strcasecmp(ce->value, "unknown-users"))
+		{
+			config_error("%s:%i: The 'unknown-users' group is a special group that is the "
+			             "inverse of 'known-users', you cannot create or adjust it in the "
+			             "config file, as it is created automatically by UnrealIRCd.",
+			             ce->file->filename, ce->line_number);
+			errors++;
+			return errors;
+		}
+		if (!security_group_valid_name(ce->value))
+		{
+			config_error("%s:%i: security-group block name '%s' contains invalid characters or is too long. "
+			             "Only letters, numbers, underscore and hyphen are allowed.",
+			             ce->file->filename, ce->line_number, ce->value);
+			errors++;
+		}
+	}
+
+	for (cep = ce->items; cep; cep = cep->next)
+	{
+		if (!test_match_item(conf, cep, &errors))
+		{
+			config_error_unknown(cep->file->filename, cep->line_number,
+				"security-group", cep->name);
+			errors++;
+			continue;
+		}
+	}
+
+	return errors;
+}
+
+int conf_match_item(ConfigFile *conf, ConfigEntry *cep, SecurityGroup **block)
+{
+	int errors = 0; /* unused */
+	SecurityGroup *s = *block;
+
+	/* The following code is there so we don't create a security group
+	 * unless there is actually a valid config item for it encountered.
+	 * This so the security group '*s' can stay NULL if there are zero
+	 * items, so we don't waste any CPU if it is unused.
+	 */
+	if (*block == NULL)
+	{
+		/* Yeah we call a TEST routine from a CONFIG RUN routine ;). */
+		if (!test_match_item(conf, cep, &errors))
+			return 0; /* not for us */
+		/* If we are still here then we must create the security group */
+		*block = s = safe_alloc(sizeof(SecurityGroup));
+	}
+
+	if (!strcmp(cep->name, "webirc"))
+		s->webirc = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "identified"))
+		s->identified = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "tls"))
+		s->tls = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "reputation-score"))
+	{
+		if (*cep->value == '<')
+			s->reputation_score = 0 - atoi(cep->value+1);
+		else
+			s->reputation_score = atoi(cep->value);
+	}
+	else if (!strcmp(cep->name, "connect-time"))
+	{
+		if (*cep->value == '<')
+			s->connect_time = 0 - config_checkval(cep->value+1, CFG_TIME);
+		else
+			s->connect_time = config_checkval(cep->value, CFG_TIME);
+	}
+	else if (!strcmp(cep->name, "mask") || !strcmp(cep->name, "include-mask"))
+	{
+		unreal_add_masks(&s->mask, cep);
+	}
+	else if (!strcmp(cep->name, "ip"))
+	{
+		unreal_add_names(&s->ip, cep);
+	}
+	else if (!strcmp(cep->name, "security-group"))
+	{
+		unreal_add_names(&s->security_group, cep);
+	}
+	else if (!strcmp(cep->name, "exclude-webirc"))
+		s->exclude_webirc = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "exclude-identified"))
+		s->exclude_identified = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "exclude-tls"))
+		s->exclude_tls = config_checkval(cep->value, CFG_YESNO);
+	else if (!strcmp(cep->name, "exclude-reputation-score"))
+	{
+		if (*cep->value == '<')
+			s->exclude_reputation_score = 0 - atoi(cep->value+1);
+		else
+			s->exclude_reputation_score = atoi(cep->value);
+	}
+	else if (!strcmp(cep->name, "exclude-mask"))
+	{
+		unreal_add_masks(&s->exclude_mask, cep);
+	}
+	else if (!strcmp(cep->name, "exclude-security-group"))
+	{
+		unreal_add_names(&s->security_group, cep);
+	}
+	else
+	{
+		/* Let's see if an extended server ban exists for this item... this needs to be LAST! */
+		Extban *extban;
+		const char *name = cep->name;
+
+		if (!strncmp(cep->name, "exclude-", 8))
+		{
+			/* Extended (exclusive) ? */
+			name = cep->name + 8;
+			if (findmod_by_bantype_raw(name, strlen(name)))
+				unreal_add_name_values(&s->exclude_extended, name, cep);
+			else
+				return 0; /* Unhandled */
+		} else {
+			/* Extended (inclusive) */
+			if (findmod_by_bantype_raw(name, strlen(name)))
+				unreal_add_name_values(&s->extended, name, cep);
+			else
+				return 0; /* Unhandled */
+		}
+	}
+
+	add_nvplist(&s->printable_list, s->printable_list_counter++, cep->name, cep->value);
+
+	return 1; /* Handled by us (guaranteed earlier) */
+}
+
+int conf_match_block(ConfigFile *conf, ConfigEntry *ce, SecurityGroup **block)
+{
+	ConfigEntry *cep;
+	SecurityGroup *s = *block;
+
+	if (*block == NULL)
+		*block = s = safe_alloc(sizeof(SecurityGroup));
+
+	/* Check for simple form: match *; / mask *; */
+	if (ce->value)
+	{
+		unreal_add_masks(&s->mask, ce);
+		add_nvplist(&s->printable_list, s->printable_list_counter++, "mask", ce->value);
+	}
+
+	/* Check for long form: match { .... } / mask { .... } */
+	for (cep = ce->items; cep; cep = cep->next)
+	{
+		if (!conf_match_item(conf, cep, &s) && !cep->value && !cep->items)
+		{
+			/* Valueless? Then it must be a mask like 10.0.0.0/8 */
+			unreal_add_masks(&s->mask, cep);
+			add_nvplist(&s->printable_list, s->printable_list_counter++, "mask", cep->name);
+		}
+	}
+	return 1;
+}
+
+int _conf_security_group(ConfigFile *conf, ConfigEntry *ce)
+{
+	ConfigEntry *cep;
+	SecurityGroup *s = add_security_group(ce->value, 1);
+
+	for (cep = ce->items; cep; cep = cep->next)
+	{
+		if (!strcmp(cep->name, "priority"))
+		{
+			s->priority = atoi(cep->value);
+			DelListItem(s, securitygroups);
+			AddListItemPrio(s, securitygroups, s->priority);
+		} else
+			conf_match_item(conf, cep, &s);
+	}
+	return 1;
+}
+
+/** Check if the name of the security-group contains only valid characters.
+ * @param name	The name of the group
+ * @returns 1 if name is valid, 0 if not (eg: illegal characters)
+ */
+int security_group_valid_name(const char *name)
+{
+	const char *p;
+
+	if (strlen(name) > SECURITYGROUPLEN)
+		return 0; /* Too long */
+
+	for (p = name; *p; p++)
+	{
+		if (!isalnum(*p) && !strchr("_-", *p))
+			return 0; /* Character not allowed */
+	}
+	return 1;
+}
+
+/** Find a security-group.
+ * @param name	The name of the security group
+ * @returns A SecurityGroup struct, or NULL if not found.
+ */
+SecurityGroup *find_security_group(const char *name)
+{
+	SecurityGroup *s;
+	for (s = securitygroups; s; s = s->next)
+		if (!strcasecmp(name, s->name))
+			return s;
+	return NULL;
+}
+
+/** Checks if a security-group exists.
+ * This function takes the 'unknown-users' magic group into account as well.
+ * @param name	The name of the security group
+ * @returns 1 if it exists, 0 if not
+ */
+int security_group_exists(const char *name)
+{
+	if (!strcmp(name, "unknown-users") || find_security_group(name))
+		return 1;
+	return 0;
+}
+
+/** Add a new security-group and add it to the list, but search for existing one first.
+ * @param name	The name of the security group
+ * @returns A SecurityGroup struct (already added to the 'securitygroups' linked list)
+ */
+SecurityGroup *add_security_group(const char *name, int priority)
+{
+	SecurityGroup *s = find_security_group(name);
+
+	/* Existing? */
+	if (s)
+		return s;
+
+	/* Otherwise, create a new entry */
+	s = safe_alloc(sizeof(SecurityGroup));
+	strlcpy(s->name, name, sizeof(s->name));
+	s->priority = priority;
+	AddListItemPrio(s, securitygroups, priority);
+	return s;
+}
+
+/** Free a SecurityGroup struct */
+void free_security_group(SecurityGroup *s)
+{
+	if (s == NULL)
+		return;
+	unreal_delete_masks(s->mask);
+	unreal_delete_masks(s->exclude_mask);
+	free_entire_name_list(s->security_group);
+	free_entire_name_list(s->exclude_security_group);
+	free_entire_name_list(s->ip);
+	free_entire_name_list(s->exclude_ip);
+	free_nvplist(s->extended);
+	free_nvplist(s->exclude_extended);
+	free_nvplist(s->printable_list);
+	safe_free(s);
+}
+
+/** Initialize the default security-group blocks */
+void set_security_group_defaults(void)
+{
+	SecurityGroup *s, *s_next;
+
+	/* First free all security groups */
+	for (s = securitygroups; s; s = s_next)
+	{
+		s_next = s->next;
+		free_security_group(s);
+	}
+	securitygroups = NULL;
+
+	/* Default group: webirc */
+	s = add_security_group("webirc-users", 50);
+	s->webirc = 1;
+
+	/* Default group: known-users */
+	s = add_security_group("known-users", 100);
+	s->identified = 1;
+	s->reputation_score = 25;
+	s->webirc = 0;
+
+	/* Default group: tls-and-known-users */
+	s = add_security_group("tls-and-known-users", 200);
+	s->identified = 1;
+	s->reputation_score = 25;
+	s->webirc = 0;
+	s->tls = 1;
+
+	/* Default group: tls-users */
+	s = add_security_group("tls-users", 300);
+	s->tls = 1;
+}
+
+int user_matches_extended_list(Client *client, NameValuePrioList *e)
+{
+	Extban *extban;
+	BanContext b;
+
+	for (; e; e = e->next)
+	{
+		extban = findmod_by_bantype_raw(e->name, strlen(e->name));
+		if (!extban ||
+		    !(extban->options & EXTBOPT_TKL) ||
+		    !(extban->is_banned_events & BANCHK_TKL))
+		{
+			continue; /* extban not found or of incorrect type */
+		}
+
+		memset(&b, 0, sizeof(BanContext));
+		b.client = client;
+		b.banstr = e->value;
+		b.ban_check_types = BANCHK_TKL;
+		if (extban->is_banned(&b))
+			return 1;
+	}
+
+	return 0;
+}
+
+int test_extended_list(Extban *extban, ConfigEntry *cep, int *errors)
+{
+	BanContext b;
+
+	if (cep->value)
+	{
+		memset(&b, 0, sizeof(BanContext));
+		b.banstr = cep->value;
+		b.ban_check_types = BANCHK_TKL;
+		b.what = MODE_ADD;
+		if (!extban->conv_param(&b, extban))
+		{
+			config_error("%s:%i: %s has an invalid value",
+			             cep->file->filename, cep->line_number, cep->name);
+			*errors = *errors + 1;
+			return 0;
+		}
+	}
+
+	for (cep = cep->items; cep; cep = cep->next)
+	{
+		memset(&b, 0, sizeof(BanContext));
+		b.banstr = cep->name;
+		b.ban_check_types = BANCHK_TKL;
+		b.what = MODE_ADD;
+		if (!extban->conv_param(&b, extban))
+		{
+			config_error("%s:%i: %s has an invalid value",
+			             cep->file->filename, cep->line_number, cep->name);
+			*errors = *errors + 1;
+			return 0;
+		}
+	}
+
+	return 1;
+}
+
+/** Returns 1 if the user is allowed by any of the security groups in the named list.
+ * This is only used by security-group::security-group and
+ * security-group::exclude-security-group.
+ * @param client	Client to check
+ * @param l		The NameList
+ * @returns 1 if any of the security groups match, 0 if none of them matched.
+ */
+int user_allowed_by_security_group_list(Client *client, NameList *l)
+{
+	for (; l; l = l->next)
+		if (user_allowed_by_security_group_name(client, l->name))
+			return 1;
+	return 0;
+}
+
+/** Returns 1 if the user is OK as far as the security-group is concerned.
+ * @param client	The client to check
+ * @param s		The security-group to check against
+ * @retval 1 if user is allowed by security-group, 0 if not.
+ */
+int user_allowed_by_security_group(Client *client, SecurityGroup *s)
+{
+	static int recursion_security_group = 0;
+
+	/* Allow NULL securitygroup, makes it easier in the code elsewhere */
+	if (!s)
+		return 0;
+
+	if (recursion_security_group > 8)
+	{
+		unreal_log(ULOG_WARNING, "main", "SECURITY_GROUP_LOOP_DETECTED", client,
+		           "Loop detected while processing security-group '$security_group' -- "
+		           "are you perhaps referencing a security-group from a security-group?",
+		           log_data_string("security_group", s->name));
+		return 0;
+	}
+	recursion_security_group++;
+
+	/* DO NOT USE 'return' IN CODE BELOW!!!!!!!!!
+	 * - use 'goto user_not_allowed' to reject
+	 * - use 'goto user_allowed' to accept
+	 */
+
+	/* Process EXCLUSION criteria first... */
+	if (s->exclude_identified && IsLoggedIn(client))
+		goto user_not_allowed;
+	if (s->exclude_webirc && moddata_client_get(client, "webirc"))
+		goto user_not_allowed;
+	if ((s->exclude_reputation_score > 0) && (GetReputation(client) >= s->exclude_reputation_score))
+		goto user_not_allowed;
+	if ((s->exclude_reputation_score < 0) && (GetReputation(client) < 0 - s->exclude_reputation_score))
+		goto user_not_allowed;
+	if (s->exclude_connect_time != 0)
+	{
+		long connect_time = get_connected_time(client);
+		if ((s->exclude_connect_time > 0) && (connect_time >= s->exclude_connect_time))
+			goto user_not_allowed;
+		if ((s->exclude_connect_time < 0) && (connect_time < 0 - s->exclude_connect_time))
+			goto user_not_allowed;
+	}
+	if (s->exclude_tls && (IsSecureConnect(client) || (MyConnect(client) && IsSecure(client))))
+		goto user_not_allowed;
+	if (s->exclude_mask && unreal_mask_match(client, s->exclude_mask))
+		goto user_not_allowed;
+	if (s->exclude_ip && unreal_match_iplist(client, s->exclude_ip))
+		goto user_not_allowed;
+	if (s->exclude_security_group && user_allowed_by_security_group_list(client, s->exclude_security_group))
+		goto user_not_allowed;
+	if (s->exclude_extended && user_matches_extended_list(client, s->exclude_extended))
+		goto user_not_allowed;
+
+	/* Then process INCLUSION criteria... */
+	if (s->identified && IsLoggedIn(client))
+		goto user_allowed;
+	if (s->webirc && moddata_client_get(client, "webirc"))
+		goto user_allowed;
+	if ((s->reputation_score > 0) && (GetReputation(client) >= s->reputation_score))
+		goto user_allowed;
+	if ((s->reputation_score < 0) && (GetReputation(client) < 0 - s->reputation_score))
+		goto user_allowed;
+	if (s->connect_time != 0)
+	{
+		long connect_time = get_connected_time(client);
+		if ((s->connect_time > 0) && (connect_time >= s->connect_time))
+			goto user_allowed;
+		if ((s->connect_time < 0) && (connect_time < 0 - s->connect_time))
+			goto user_allowed;
+	}
+	if (s->tls && (IsSecureConnect(client) || (MyConnect(client) && IsSecure(client))))
+		goto user_allowed;
+	if (s->mask && unreal_mask_match(client, s->mask))
+		goto user_allowed;
+	if (s->ip && unreal_match_iplist(client, s->ip))
+		goto user_allowed;
+	if (s->security_group && user_allowed_by_security_group_list(client, s->security_group))
+		goto user_allowed;
+	if (s->extended && user_matches_extended_list(client, s->extended))
+		goto user_allowed;
+
+user_not_allowed:
+	recursion_security_group--;
+	return 0;
+
+user_allowed:
+	recursion_security_group--;
+	return 1;
+}
+
+/** Returns 1 if the user is OK as far as the security-group is concerned - "by name" version.
+ * @param client	The client to check
+ * @param secgroupname	The name of the security-group to check against
+ * @retval 1 if user is allowed by security-group, 0 if not.
+ */
+int user_allowed_by_security_group_name(Client *client, const char *secgroupname)
+{
+	SecurityGroup *s;
+
+	/* Handle the magical 'unknown-users' case. */
+	if (!strcmp(secgroupname, "unknown-users"))
+	{
+		/* This is simply the inverse of 'known-users' */
+		s = find_security_group("known-users");
+		if (!s)
+			return 0; /* that's weird!? pretty impossible. */
+		return !user_allowed_by_security_group(client, s);
+	}
+
+	/* Find the group and evaluate it */
+	s = find_security_group(secgroupname);
+	if (!s)
+		return 0; /* security group not found: no match */
+	return user_allowed_by_security_group(client, s);
+}
+
+/** Get comma separated list of matching security groups for 'client'.
+ * This is usually only used for displaying purposes.
+ * @returns string like "unknown-users,tls-users" from a static buffer.
+ */
+const char *get_security_groups(Client *client)
+{
+	SecurityGroup *s;
+	static char buf[512];
+
+	*buf = '\0';
+
+	/* We put known-users or unknown-users at the beginning.
+	 * The latter is special and doesn't actually exist
+	 * in the linked list, hence the special code here,
+	 * and again later in the for loop to skip it.
+	 */
+	if (user_allowed_by_security_group_name(client, "known-users"))
+		strlcat(buf, "known-users,", sizeof(buf));
+	else
+		strlcat(buf, "unknown-users,", sizeof(buf));
+
+	for (s = securitygroups; s; s = s->next)
+	{
+		if (strcmp(s->name, "known-users") &&
+		    user_allowed_by_security_group(client, s))
+		{
+			strlcat(buf, s->name, sizeof(buf));
+			strlcat(buf, ",", sizeof(buf));
+		}
+	}
+
+	if (*buf)
+		buf[strlen(buf)-1] = '\0';
+	return buf;
+}
diff --git a/src/serv.c b/src/serv.c
@@ -1208,7 +1208,9 @@ void lost_server_link(Client *client, const char *tls_error_string)
 			if (client->server->conf)
 			{
 				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
-					   "Unable to link with server $client [$link_block.ip:$link_block.port]: $tls_error_string",
+					   client->server->conf->outgoing.file
+					   ? "Unable to link with server $client [$link_block.file]: $tls_error_string"
+					   : "Unable to link with server $client [$link_block.ip:$link_block.port]: $tls_error_string",
 					   log_data_string("tls_error_string", tls_error_string),
 					   log_data_link_block(client->server->conf));
 			} else {
@@ -1221,7 +1223,9 @@ void lost_server_link(Client *client, const char *tls_error_string)
 			if (client->server->conf)
 			{
 				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
-					   "Unable to link with server $client [$link_block.ip:$link_block.port]: $socket_error",
+					   client->server->conf->outgoing.file
+					   ? "Unable to link with server $client [$link_block.file]: $socket_error"
+					   : "Unable to link with server $client [$link_block.ip:$link_block.port]: $socket_error",
 					   log_data_socket_error(client->local->fd),
 					   log_data_link_block(client->server->conf));
 			} else {
diff --git a/src/socket.c b/src/socket.c
@@ -1300,11 +1300,11 @@ int deliver_it(Client *client, char *str, int len, int *want_read)
 }
 
 /** Initiate an outgoing connection, the actual connect() call. */
-int unreal_connect(int fd, const char *ip, int port, int ipv6)
+int unreal_connect(int fd, const char *ip, int port, SocketType socket_type)
 {
 	int n;
 
-	if (ipv6)
+	if (socket_type == SOCKET_TYPE_IPV6)
 	{
 		struct sockaddr_in6 server;
 		memset(&server, 0, sizeof(server));
@@ -1312,13 +1312,22 @@ int unreal_connect(int fd, const char *ip, int port, int ipv6)
 		inet_pton(AF_INET6, ip, &server.sin6_addr);
 		server.sin6_port = htons(port);
 		n = connect(fd, (struct sockaddr *)&server, sizeof(server));
-	} else {
+	}
+	else if (socket_type == SOCKET_TYPE_IPV4)
+	{
 		struct sockaddr_in server;
 		memset(&server, 0, sizeof(server));
 		server.sin_family = AF_INET;
 		inet_pton(AF_INET, ip, &server.sin_addr);
 		server.sin_port = htons(port);
 		n = connect(fd, (struct sockaddr *)&server, sizeof(server));
+	} else
+	{
+		struct sockaddr_un server;
+		memset(&server, 0, sizeof(server));
+		server.sun_family = AF_UNIX;
+		strlcpy(server.sun_path, ip, sizeof(server.sun_path));
+		n = connect(fd, (struct sockaddr *)&server, sizeof(server));
 	}
 
 #ifndef _WIN32
diff --git a/src/support.c b/src/support.c
@@ -1269,7 +1269,7 @@ void buildvarstring(const char *inbuf, char *outbuf, size_t len, const char *nam
 			/* find variable name in list */
 			found = 0;
 			for (cnt = 0; name[cnt]; cnt++)
-				if (!strncasecmp(name[cnt], i, p - i))
+				if (!strncasecmp(name[cnt], i, strlen(name[cnt])))
 				{
 					/* Found */
 					found = 1;
diff --git a/src/tls.c b/src/tls.c
@@ -354,7 +354,7 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server)
 	if (SSL_CTX_set_ciphersuites(ctx, tlsoptions->ciphersuites) == 0)
 	{
 		unreal_log(ULOG_ERROR, "config", "TLS_INVALID_CIPHERSUITES_LIST", NULL,
-		           "Failed to set TLS ciphersuites list '$tls_ciphers_list'\n$tls_error.all",
+		           "Failed to set TLS ciphersuites list '$tls_ciphersuites_list'\n$tls_error.all",
 		           log_data_string("tls_ciphersuites_list", tlsoptions->ciphersuites),
 		           log_data_tls_error());
 		goto fail;
diff --git a/src/user.c b/src/user.c
@@ -30,7 +30,6 @@
 #include "unrealircd.h"
 
 MODVAR int dontspread = 0;
-static char buf[BUFSIZE];
 
 /** Inhibit labeled/response reply. This means it will result in an empty ACK
  *  because we cannot handle the command via labeled-reponse. Rare, but
@@ -69,11 +68,9 @@ void iNAH_host(Client *client, const char *host)
 	safe_strdup(client->user->virthost, host);
 	if (MyConnect(client))
 		sendto_server(NULL, 0, 0, NULL, ":%s SETHOST :%s", client->id, client->user->virthost);
-	client->umodes |= UMODE_SETHOST;
+	client->umodes |= UMODE_SETHOST|UMODE_HIDE;
 
 	userhost_changed(client);
-
-	sendnumeric(client, RPL_HOSTHIDDEN, client->user->virthost);
 }
 
 /** Convert a user mode string to a bitmask - only used by config.
@@ -420,6 +417,7 @@ void build_umode_string(Client *client, long old, long sendmask, char *umode_buf
 void send_umode_out(Client *client, int show_to_user, long old)
 {
 	Client *acptr;
+	char buf[512];
 
 	build_umode_string(client, old, SEND_UMODES, buf);
 
@@ -739,192 +737,24 @@ int hide_idle_time(Client *client, Client *target)
 	}
 }
 
-/** Check if the name of the security-group contains only valid characters.
- * @param name	The name of the group
- * @returns 1 if name is valid, 0 if not (eg: illegal characters)
- */
-int security_group_valid_name(const char *name)
-{
-	const char *p;
-
-	if (strlen(name) > SECURITYGROUPLEN)
-		return 0; /* Too long */
-
-	for (p = name; *p; p++)
-	{
-		if (!isalnum(*p) && !strchr("_-", *p))
-			return 0; /* Character not allowed */
-	}
-	return 1;
-}
-
-/** Find a security-group.
- * @param name	The name of the security group
- * @returns A SecurityGroup struct, or NULL if not found.
- */
-SecurityGroup *find_security_group(const char *name)
-{
-	SecurityGroup *s;
-	for (s = securitygroups; s; s = s->next)
-		if (!strcasecmp(name, s->name))
-			return s;
-	return NULL;
-}
-
-/** Checks if a security-group exists.
- * This function takes the 'unknown-users' magic group into account as well.
- * @param name	The name of the security group
- * @returns 1 if it exists, 0 if not
- */
-int security_group_exists(const char *name)
-{
-	if (!strcmp(name, "unknown-users") || find_security_group(name))
-		return 1;
-	return 0;
-}
-
-/** Add a new security-group and add it to the list, but search for existing one first.
- * @param name	The name of the security group
- * @returns A SecurityGroup struct (already added to the 'securitygroups' linked list)
- */
-SecurityGroup *add_security_group(const char *name, int priority)
-{
-	SecurityGroup *s = find_security_group(name);
-
-	/* Existing? */
-	if (s)
-		return s;
-
-	/* Otherwise, create a new entry */
-	s = safe_alloc(sizeof(SecurityGroup));
-	strlcpy(s->name, name, sizeof(s->name));
-	s->priority = priority;
-	AddListItemPrio(s, securitygroups, priority);
-	return s;
-}
-
-/** Free a SecurityGroup struct */
-void free_security_group(SecurityGroup *s)
-{
-	unreal_delete_masks(s->include_mask);
-	safe_free(s);
-}
-
-/** Initialize the default security-group blocks */
-void set_security_group_defaults(void)
-{
-	SecurityGroup *s, *s_next;
-
-	/* First free all security groups */
-	for (s = securitygroups; s; s = s_next)
-	{
-		s_next = s->next;
-		free_security_group(s);
-	}
-	securitygroups = NULL;
-
-	/* Default group: webirc */
-	s = add_security_group("webirc-users", 50);
-	s->webirc = 1;
-
-	/* Default group: known-users */
-	s = add_security_group("known-users", 100);
-	s->identified = 1;
-	s->reputation_score = 25;
-	s->webirc = 0;
-
-	/* Default group: tls-and-known-users */
-	s = add_security_group("tls-and-known-users", 200);
-	s->identified = 1;
-	s->reputation_score = 25;
-	s->webirc = 0;
-	s->tls = 1;
-
-	/* Default group: tls-users */
-	s = add_security_group("tls-users", 300);
-	s->tls = 1;
-}
-
-/** Returns 1 if the user is OK as far as the security-group is concerned.
- * @param client	The client to check
- * @param s		The security-group to check against
- * @retval 1 if user is allowed by security-group, 0 if not.
- */
-int user_allowed_by_security_group(Client *client, SecurityGroup *s)
-{
-	if (s->identified && IsLoggedIn(client))
-		return 1;
-	if (s->webirc && moddata_client_get(client, "webirc"))
-		return 1;
-	if (s->reputation_score && (GetReputation(client) >= s->reputation_score))
-		return 1;
-	if (s->tls && (IsSecureConnect(client) || (MyConnect(client) && IsSecure(client))))
-		return 1;
-	if (s->include_mask && unreal_mask_match(client, s->include_mask))
-		return 1;
-	return 0;
-}
-
-/** Returns 1 if the user is OK as far as the security-group is concerned - "by name" version.
+/** Get how long a client is connected to IRC.
  * @param client	The client to check
- * @param secgroupname	The name of the security-group to check against
- * @retval 1 if user is allowed by security-group, 0 if not.
- */
-int user_allowed_by_security_group_name(Client *client, const char *secgroupname)
-{
-	SecurityGroup *s;
-
-	/* Handle the magical 'unknown-users' case. */
-	if (!strcmp(secgroupname, "unknown-users"))
-	{
-		/* This is simply the inverse of 'known-users' */
-		s = find_security_group("known-users");
-		if (!s)
-			return 0; /* that's weird!? pretty impossible. */
-		return !user_allowed_by_security_group(client, s);
-	}
-
-	/* Find the group and evaluate it */
-	s = find_security_group(secgroupname);
-	if (!s)
-		return 0; /* security group not found: no match */
-	return user_allowed_by_security_group(client, s);
-}
-
-/** Get comma separated list of matching security groups for 'client'.
- * This is usually only used for displaying purposes.
- * @returns string like "unknown-users,tls-users" from a static buffer.
+ * @returns how long the client is connected to IRC (number of seconds)
  */
-const char *get_security_groups(Client *client)
+long get_connected_time(Client *client)
 {
-	SecurityGroup *s;
-	static char buf[512];
-
-	*buf = '\0';
-
-	/* We put known-users or unknown-users at the beginning.
-	 * The latter is special and doesn't actually exist
-	 * in the linked list, hence the special code here,
-	 * and again later in the for loop to skip it.
-	 */
-	if (user_allowed_by_security_group_name(client, "known-users"))
-		strlcat(buf, "known-users,", sizeof(buf));
-	else
-		strlcat(buf, "unknown-users,", sizeof(buf));
+	const char *str;
+	long connect_time = 0;
 
-	for (s = securitygroups; s; s = s->next)
-	{
-		if (strcmp(s->name, "known-users") &&
-		    user_allowed_by_security_group(client, s))
-		{
-			strlcat(buf, s->name, sizeof(buf));
-			strlcat(buf, ",", sizeof(buf));
-		}
-	}
+	/* Shortcut for local clients */
+	if (client->local)
+		return TStime() - client->local->creationtime;
 
-	if (*buf)
-		buf[strlen(buf)-1] = '\0';
-	return buf;
+	/* Otherwise, hopefully available through this... */
+	str = moddata_client_get(client, "creationtime");
+	if (!BadPtr(str) && (*str != '0'))
+		return TStime() - atoll(str);
+	return 0;
 }
 
 /** Return extended information about user for the "Client connecting" line.
@@ -966,6 +796,10 @@ const char *get_connect_extinfo(Client *client)
 	secgroups = get_security_groups(client);
 	if (secgroups)
 		add_nvplist(&list, 100, "security-groups", secgroups);
+	
+	/* tkl shunned */
+	if (IsShunned(client))
+		add_nvplist(&list, 110, "shunned", NULL);
 
 	*retbuf = '\0';
 	for (e = list; e; e = e->next)
@@ -977,8 +811,8 @@ const char *get_connect_extinfo(Client *client)
 		strlcat(retbuf, tmp, sizeof(retbuf));
 	}
 	/* Cut off last space (unless empty string) */
-	if (*buf)
-		buf[strlen(buf)-1] = '\0';
+	if (*retbuf)
+		retbuf[strlen(retbuf)-1] = '\0';
 
 	/* Free the list, as it was only used to build retbuf */
 	free_nvplist(list);
diff --git a/src/version.c.SH b/src/version.c.SH
@@ -7,7 +7,7 @@ echo "Extracting src/version.c..."
 if [ -d ../.git ]; then
 	SUFFIX="-$(git rev-parse --short HEAD)"
 fi
-id="6.0.3$SUFFIX"
+id="6.0.4.2$SUFFIX"
 echo "$id"
 
 if test -r version.c
diff --git a/src/windows/UnrealIRCd.exe.manifest b/src/windows/UnrealIRCd.exe.manifest
@@ -3,7 +3,7 @@
 <assemblyIdentity
     processorArchitecture="amd64"
     name="UnrealIRCd.UnrealIRCd.6"
-    version="6.0.3.0"
+    version="6.0.4.2"
     type="win32"
 />
 <description>Internet Relay Chat Daemon</description>
diff --git a/src/windows/gui.c b/src/windows/gui.c
@@ -536,25 +536,7 @@ LRESULT CALLBACK MainDLG(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
 					return AskCloseUnreal(hDlg);
 				case IDM_RHALL:
 					MessageBox(NULL, "Rehashing all files", "Rehashing", MB_OK);
-					request_rehash(NULL);
-					break;
-				case IDM_RHCONF:
-					MessageBox(NULL, "Rehashing the Config file", "Rehashing", MB_OK);
-					request_rehash(NULL);
-					break;
-				case IDM_RHMOTD: 
-				{
-					MessageBox(NULL, "Rehashing all MOTD and Rules files", "Rehashing", MB_OK);
-					rehash_motdrules();
-					break;
-				}
-				case IDM_RHOMOTD:
-					MessageBox(NULL, "Rehashing the OperMOTD", "Rehashing", MB_OK);
-					read_motd(conf_files->opermotd_file, &opermotd);
-					break;
-				case IDM_RHBMOTD:
-					MessageBox(NULL, "Rehashing the BotMOTD", "Rehashing", MB_OK);
-					read_motd(conf_files->botmotd_file, &botmotd);
+					dorehash = 1;
 					break;
 				case IDM_LICENSE: 
 					DialogBox(hInst, "FromVar", hDlg, (DLGPROC)LicenseDLG);
diff --git a/src/windows/unrealinst.iss b/src/windows/unrealinst.iss
@@ -6,7 +6,7 @@
 
 [Setup]
 AppName=UnrealIRCd 6
-AppVerName=UnrealIRCd 6.0.3
+AppVerName=UnrealIRCd 6.0.4.2
 AppPublisher=UnrealIRCd Team
 AppPublisherURL=https://www.unrealircd.org
 AppSupportURL=https://www.unrealircd.org
diff --git a/src/windows/wingui.rc b/src/windows/wingui.rc
@@ -362,12 +362,7 @@ MENU_REHASH MENU
 BEGIN
     POPUP "Rehash"
     BEGIN
-        MENUITEM "&All Files",                  IDM_RHALL
-        MENUITEM SEPARATOR
-        MENUITEM "&Config file",                IDM_RHCONF
-        MENUITEM "&All MOTD and Rules",         IDM_RHMOTD
-        MENUITEM "&OperMOTD",                   IDM_RHOMOTD
-        MENUITEM "&BotMOTD",                    IDM_RHBMOTD
+        MENUITEM "Rehash &All Files",           IDM_RHALL
     END
 END