diff --git a/Config b/Config index 0a06f8909f..025219a8e4 100755 --- a/Config +++ b/Config @@ -50,9 +50,12 @@ if [ -z "$BINDIR" -o -z "$DATADIR" -o -z "$CONFDIR" -o -z "$MODULESDIR" -o -z "$ fi +mkdir -p $BASEPATH mkdir -p $TMPDIR -mkdir -p $PRIVATELIBDIR mkdir -p $CONFDIR +chmod 0700 $BASEPATH +chmod 0700 $TMPDIR +chmod 0700 $CONFDIR # Do this even if we're not in advanced mode if [ "$ADVANCED" = "1" ] ; then @@ -188,7 +191,7 @@ if [ "$QUICK" != "1" ] ; then echo "*******************************************************************************" echo "Press ENTER to continue" read cc - $MAKE pem + ./unrealircd mkcert echo "Certificate created successfully." sleep 1 else diff --git a/Makefile.in b/Makefile.in index 732459ea4f..d9dc41f094 100644 --- a/Makefile.in +++ b/Makefile.in @@ -224,6 +224,7 @@ install: all fi $(INSTALL) -m 0700 -d $(DESTDIR)@CONFDIR@/tls $(INSTALL) -m 0600 doc/conf/tls/curl-ca-bundle.crt $(DESTDIR)@CONFDIR@/tls + $(INSTALL) -m 0600 doc/conf/tls/tls.cnf $(DESTDIR)@CONFDIR@/tls @# delete modules/cap directory, to avoid confusing with U4 to U5 upgrades: rm -rf $(DESTDIR)@MODULESDIR@/cap $(INSTALL) -m 0700 -d $(DESTDIR)@MODULESDIR@/third @@ -234,11 +235,6 @@ install: all $(INSTALL) -m 0700 -d $(DESTDIR)@CACHEDIR@ $(INSTALL) -m 0700 -d $(DESTDIR)@PERMDATADIR@ $(INSTALL) -m 0700 -d $(DESTDIR)@LOGDIR@ - -@if [ ! -f "$(DESTDIR)@CONFDIR@/tls/server.cert.pem" ] ; then \ - $(INSTALL) -m 0600 server.req.pem $(DESTDIR)@CONFDIR@/tls ; \ - $(INSTALL) -m 0600 server.key.pem $(DESTDIR)@CONFDIR@/tls ; \ - $(INSTALL) -m 0600 server.cert.pem $(DESTDIR)@CONFDIR@/tls ; \ - fi @rm -f $(DESTDIR)@SCRIPTDIR@/source ln -s @BUILDDIR@ $(DESTDIR)@SCRIPTDIR@/source @echo '' @@ -268,19 +264,9 @@ install: all echo 'Again, be sure to change to the @SCRIPTDIR@ directory!' ; \ fi -pem: extras/tls.cnf - @echo "Generating server key..." - $(OPENSSLPATH) ecparam -out server.key.pem -name secp384r1 -genkey - @echo "Generating certificate request..." - $(OPENSSLPATH) req -new \ - -config extras/tls.cnf -sha256 -out server.req.pem \ - -key server.key.pem -nodes - @echo "Generating self-signed certificate..." - $(OPENSSLPATH) req -x509 -days 3650 -sha256 -nodes -in server.req.pem \ - -key server.key.pem -out server.cert.pem - @echo "Setting permissions on server.*.pem files..." - chmod o-rwx server.req.pem server.key.pem server.cert.pem - chmod g-rwx server.req.pem server.key.pem server.cert.pem +pem: + @echo "The command 'make pem' is no longer used to generate the TLS certificate." + @echo "Please run './unrealircd mkcert' instead." Makefile: config.status Makefile.in ./config.status diff --git a/SECURITY.md b/SECURITY.md index f7144792c6..ffc7861dd5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,19 +3,68 @@ ## Supported Versions * The latest *stable* release of UnrealIRCd 6 -See [UnrealIRCd releases](https://www.unrealircd.org/docs/UnrealIRCd_releases) for information on older versions and End Of Life dates. +See [UnrealIRCd releases](https://www.unrealircd.org/docs/UnrealIRCd_releases) +for information on older versions and End Of Life dates. + +## Scope + +In general, issues triggered by regular users involving memory safety issues +(such as OOB read/write or UAF), sensitive information disclosure, privilege elevation, +Denial of Service (e.g. a crash), or remote code execution fall within the scope of +this security policy. + +Issues that require IRCOp rights, server-to-server traffic, or editing of config +files may still fall within scope, but are classified case by case depending on +the impact and circumstances. + +Issues that require shell access as the same user running UnrealIRCd are not +considered security issues. See the +[full policy](https://www.unrealircd.org/docs/Policy:_Handling_of_security_issues) +for details. + +## Use of AI or other tools + +It is normal and acceptable to use tools for finding security vulnerabilities. +We use them ourselves as well: AI, static code analyzers, fuzzing. This is all fine. + +If a tool flagged an issue then we ask only **one extra thing**: that you +**reproduce the issue** on your own local server. So: confirm the issue by +actually running UnrealIRCd with a reproducer (which usually means: by sending +IRC traffic to trigger the bug). This is because tools regularly flag something +as an issue but in practice it may be impossible to happen because of some extra +check somewhere or other requirements. + +If you are trying to reproduce an issue, then we suggest running `./Config` and +answering `Yes` to the near-last question about AddressSanitizer (ASan), +especially for memory safety issues. Please include the reproducer and any +relevant ASan output in the bug report. ASan output is useful even if a normal +build does not visibly crash. It helps us a lot. + +If you used AI, static code analyzers, fuzzing, or similar tools and fail to +follow the procedure above, expect us to ask you again to reproduce the issue. +If you refuse to do so, don't respond in a timely manner, or keep sending reports +without doing so after we asked, then we will close the bug report. For repeat +offenders, we may proceed with putting you on ignore, banning or deleting your +account, or similar. Giving a reproducer is not a big ask and is normal procedure +nowadays. It should be part of your standard workflow if you are a security +researcher. ## Reporting a Vulnerability -Please report issues on the [bug tracker](https://bugs.unrealircd.org) and in the bug submit form **set the 'View Status' to 'private'**. +Please report issues on the [bug tracker](https://bugs.unrealircd.org) and in +the bug submit form **set the 'View Status' to 'private'**. -Do not report security issues on the forums or in a public IRC channel such as #unreal-support. -If you insist on e-mail then you can use syzop@unrealircd.org or security@unrealircd.org. Again, the bug tracker is preferred. +Do not report security issues as a Pull Request, on the forums or in a public +IRC channel such as #unreal-support. If you insist on e-mail then you can use +syzop@unrealircd.org or security@unrealircd.org. Again, the bug tracker is +preferred. -If you are *unsure* if something is a security issue, then report it at the bug tracker as a 'private' bug anyway. Better safe than sorry. +If you found a real issue but are *unsure* if it is a security issue, then +report it at the bug tracker as a 'private' bug anyway. Better safe than sorry. Do not ask around in public channels or forums. -You should get a response or at least an acknowledgement soon. If you don't hear back within 24 hours, then please try to contact us again. +You should get a response or at least an acknowledgement soon. If you don't hear +back within 24 hours, then please try to contact us again. ## Full policy See https://www.unrealircd.org/docs/Policy:_Handling_of_security_issues for full information. diff --git a/configure b/configure index 85fb202588..9ab2c15e36 100755 --- a/configure +++ b/configure @@ -7582,6 +7582,11 @@ else case e in #( e) printf "%s\n" "#define PRIVATELIBDIR \"$PRIVATELIBDIR\"" >>confdefs.h + # Create the private library directory now with restrictive permissions. + # This must happen here rather than in the Makefile because the bundled + # libraries are installed into it during configure. It must also work when + # configure is run directly without ./Config having created it first. + mkdir -p "$PRIVATELIBDIR" && chmod 0700 "$PRIVATELIBDIR" LDFLAGS_PRIVATELIBS="-Wl,-rpath,$PRIVATELIBDIR" LDFLAGS="$LDFLAGS $LDFLAGS_PRIVATELIBS" export LDFLAGS ;; @@ -8893,7 +8898,7 @@ $ac_cv_prog_MAKER install PREFIX=$cur_dir/extras/argon2 || exit 1 # lead to a crash of the currently running IRCd. rm -f "$PRIVATELIBDIR/"libargon2* # Now copy the new library files: -cp -av $cur_dir/extras/argon2/lib/* $PRIVATELIBDIR/ +cp -av $cur_dir/extras/argon2/lib/* $PRIVATELIBDIR/ || exit 1 CFLAGS="$save_cflags" LDFLAGS="$save_ldflags" ARGON2_CFLAGS="-I$cur_dir/extras/argon2/include" diff --git a/configure.ac b/configure.ac index acd3eab63b..e986f6c28d 100644 --- a/configure.ac +++ b/configure.ac @@ -558,6 +558,11 @@ AS_IF([test "x$with_privatelibdir" = "xno"], AS_IF([test "x$PRIVATELIBDIR" = "x"], [LDFLAGS_PRIVATELIBS=""], [AC_DEFINE_UNQUOTED([PRIVATELIBDIR], ["$PRIVATELIBDIR"], [Define the location of private libraries]) + # Create the private library directory now with restrictive permissions. + # This must happen here rather than in the Makefile because the bundled + # libraries are installed into it during configure. It must also work when + # configure is run directly without ./Config having created it first. + mkdir -p "$PRIVATELIBDIR" && chmod 0700 "$PRIVATELIBDIR" LDFLAGS_PRIVATELIBS="-Wl,-rpath,$PRIVATELIBDIR" LDFLAGS="$LDFLAGS $LDFLAGS_PRIVATELIBS" export LDFLAGS]) @@ -728,7 +733,7 @@ $ac_cv_prog_MAKER install PREFIX=$cur_dir/extras/argon2 || exit 1 # lead to a crash of the currently running IRCd. rm -f "$PRIVATELIBDIR/"libargon2* # Now copy the new library files: -cp -av $cur_dir/extras/argon2/lib/* $PRIVATELIBDIR/ +cp -av $cur_dir/extras/argon2/lib/* $PRIVATELIBDIR/ || exit 1 CFLAGS="$save_cflags" LDFLAGS="$save_ldflags" ARGON2_CFLAGS="-I$cur_dir/extras/argon2/include" diff --git a/doc/Config.header b/doc/Config.header index ffb6aa2f7b..37037770f4 100644 --- a/doc/Config.header +++ b/doc/Config.header @@ -7,7 +7,7 @@ \___/|_| |_|_| \___|\__,_|_|\___/\_| \_| \____/\__,_| Configuration Program - for UnrealIRCd 6.2.6 + for UnrealIRCd 6.2.6-rc1 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 index 4da1dac05c..27eadcde27 100644 --- a/doc/RELEASE-NOTES.md +++ b/doc/RELEASE-NOTES.md @@ -1,26 +1,182 @@ UnrealIRCd 6.2.6 -================= +===================== + +This is the Release Candidate for future version 6.2.6. You can help us by +testing this release and reporting bugs to https://bugs.unrealircd.org/ -This is the git version (development version) for future UnrealIRCd 6.2.6. -This is work in progress and may not always be a stable version. +This version enables multiline by default, adds TKL IDs and tracking of +hit counts on *LINES/Spamfilter. New crule functions were added to fetch +server flood counts. Guidance to admins for server linking with 'spkifp' +has been improved. ### Enhancements: +* [IRCv3 draft/multiline](https://ircv3.net/specs/extensions/multiline) + is now enabled by default. See the [UnrealIRCd 6.2.4 release](#unrealircd-624) + for all information on what this feature does, the default limits, etc. + To turn it off completely you can use: `blacklist-module multiline;` +* Server bans and Spamfilters now have a unique ID, like `G7K2MP9WQX3`: + * The first letter denotes the type: `G` for gline, `K` for kline, + `Z` for (g)zline, `H` for shun. Spamfilter IDs start with `SPAM`. + * The ID is shown to the affected user, so they can paste the ID back to + network staff. It is `$banid` in + [set::reject-message](https://www.unrealircd.org/docs/Set_block#set::reject-message) + and included by default. If you have a custom set::reject-message in + your config file then you will have to add it in manually. + * IRCOps see the ID in `STATS gline` and similar and can search for it + with e.g. `STATS gline +i G7K2MP9WQX3`. + * Spamfilters also have an ID, and we already had something similar that + could be used for `SPAMFILTER del ` that was only local-server. + For new spamfilters this is now a network wide ID. + * When a server ban was placed by a spamfilter, `STATS gline` shows the + originating spamfilter's ID, so it can be traced back. + * The TKL ID and related Spamfilter ID (if any), also appear in log + messages `TKL_ADD`, `TKL_DEL`, `TKL_EXPIRE` and `SPAMFILTER_MATCH`. + * The `STATS gline` and other TKL stats changed format, see + *Developers and protocol* if you use scripts or your client depends + on the exact numeric format. + * For TKL IDs to reach all servers, all servers need to be on 6.2.6 + or later, especially the hubs. If there is one server in-between + that is older, then TKL IDs don't propagate properly and the ID + will be empty. +* Server bans and Spamfilters now track how often they are hit and the time + of the last hit, eg in `STATS gline` for GLINEs. These counts happen on + each individual server and are not network-wide. This allows IRCOps to see + which entries never get any hits and can potentially be removed. + * Important exception: config-based spamfilters/bans lose their counters + on restart. + * For non-config TKLs, the hit count and last hit timestamp are preserved + across reboots (via tkldb). + * Again, see *Developers and protocol* for the exact STATS field. +* New [crule functions](https://www.unrealircd.org/docs/Crule) that return + the number of times a flood was blocked for a user. + * `server_flood_count('..setting..')` returns the number of times + set::anti-flood settings were exceeded by that user. Available are: + `away`, `nick`, `join`, `invite`, `knock`, `vhost` and `conversations`. + Plus, there is `all` for the sum of all items. + * `total_channel_flood_count('..setting..')` returns the number of + times `+f`/`+F` limits were exceeded by that user in all channels + the user is or was in. Available are: `nick`, `join`, `knock`, `msg`, + `ctcp`, `text`, `repeat` and `paste` (and `all` for the sum). + * All these are counted for the duration of the session (user connection). + * These can be used in a security-group::rule or spamfilter::rule. Eg: + `spamfilter { rule "server_flood_count('nick')>4"; reason "Nick flood"; action gline; ban-time 1h; }` +* Central Spamreport now also receives those flood counters. ### Changes: +* Server linking and certificates: we now treat listener blocks that are + `serversonly` (such as port 6900 in the example.conf) and link { } blocks + in a different way than regular listen { } blocks: + * If there are different certificates used in the serversonly listen block + vs link blocks, then this is almost always means server linking is broken, + so we now print a warning on boot and rehash. + * We also print an 'advice' if any of these are not using (long-lived) + self-signed certificate. This is because CA issued certificates are + typically not suitable because they typically rotate keys and thus change + the `spkifp`. Changing spkifp breaks server linking. We will now print + an advice along with command and config block instructions to fix it. + * We now use `set::server-linking::tls-options` for link { } blocks + and listen { } blocks that are `serversonly`. All the rest uses the + `set::tls` settings by default (eg the regular listen { } block on 6697). + * This means our guide on + [Using Let's Encrypt with UnrealIRCd](https://www.unrealircd.org/docs/Using_Let's_Encrypt_with_UnrealIRCd) + and generic usage is more intuitive. You just set both set settings + and then no longer need to use any tls-options in listen blocks or link + blocks. The example conf has also been updated with this. + * If `set::server-linking::tls-options` is not configured, it defaults + to `set::tls`, so there is no unexpected behavior change for anyone. + * In a future release we will make server linking with `spkifp` mandatory, + so all of this helps with getting people ready for that, making such + a future transition smooth. + * The command `./unrealircd genlinkblock` now also deals with ECC + ML-DSA + where you have multiple `password ".." { spkifp; }` entries. +* Spamfilter regexes now use more sensible defaults in terms of "max effort", + similar to what PHP has been using for years. This means very slow regexes + will now raise a `SPAMFILTER_REGEX_ERROR` warning during execution if + this happens (should be extremely rare). +* The UnrealIRCd base directory (eg `~/unrealircd/`) is now created with + 0700 permissions, just like most subdirectories were. +* We now have `./unrealircd mkcert` which replaces `make pem` + certificate/key generation. +* Translation updates: `help.fr.conf` +* CHATHISTORY now sends a `draft/chathistory-end` if the end of history + has been reached ([a recent spec improvement](https://github.com/unrealircd/unrealircd/pull/337)). +* The IRCv3 [reply](https://ircv3.net/specs/client-tags/reply) client tag + and [no-implicit-names](https://ircv3.net/specs/extensions/no-implicit-names) + extensions have been ratified. During the transition period we support both. ### Fixes: -* Harden the built-in HTTPS client +* The following config items previously raised a config error: + allow channel::except, deny channel::except and spamfilter::except. +* deny channel::mask with a [Mask item](https://www.unrealircd.org/docs/Mask_item) + caused a config error. +* Long multiline messages could be cut off when sent to clients that do not + support multiline. +* Hardening of the built-in HTTPS client +* JSON-RPC: Remote RPC was broken and causing "not authorized" error messages. + This was used by `server.rehash` and `server.module_list`. Plus, + this release `user.get` under some circumstances. This is now + fixed but requires the target server to be on UnrealIRCd 6.2.6. + If the target server does not meet this condition then we error + telling the server "does not support remote JSON-RPC". ### Developers and protocol: +* If you use `CAP LS` instead of `CAP LS 302` then you will now miss various + capabilities. We had to trim it down because only 302 and later allow + responses that span multiple lines. If your client is still not using 302 + then please do so soon (the IRCv3 spec is from Nov'2017). For this first + change, you won't miss out much, but somewhere in the future this can + become a real problem for you. * URL API: The OutgoingWebRequest `max_size` (introduced last release) now also caps file-backed downloads. Default for file-backed when left at 0 is 50MB (`DOWNLOAD_MAX_SIZE_FILE_BACKED`). For memory-backed, it stays at 1MB like in 6.2.5 (`DOWNLOAD_MAX_SIZE_MEMORY_BACKED`). +* The `unreal_match()` function now has a 3rd argument `const char **error` + for communicating regex errors back. Just set to `NULL` if you don't care. +* Similarly, `unreal_create_match()` last argument is now `const char **error` + (const was added). So when callers use that, their variable `char *err` + needs to become `const char *err`. * If you do something to a user that would (potentially) move the user from `unknown-users` to `known-users` (or vice versa) then you should call `update_known_user_cache(client);` to update the known users cache. For example, if you have some authentication module that marks a user as IsLoggedIn. +* TKL entries can now use message tags to carry extra data, similar to how + `s2s-md` worked for early moddata, we have `s2s-tkl`. We have two at + the moment: + * `s2s-tkl/id`: network-wide unique ID for the TKL entry + * `s2s-tkl/spamfilter_id`: for server bans, if they were set by spamfilter, + the spamfilter TKL ID. + * Example: `@s2s-tkl/id=xxxx;s2s-tkl/spamfilter_id=yyyy :server TKL .....` + * Older servers will not pass these along, so for this functionality to + work all servers need to be on latest version, especially your hub. +* The TKL `STATS` numerics has more fields now, they are added right before + last (so before the `:reason` or `:regex`): + * `RPL_STATSGLINE` (223) ends with `hits lasthit spamfilter_id id :reason` + * `RPL_STATSQLINE` (217) ends with `hits lasthit id :reason` + * `RPL_STATSSPAMF` (229) ends with `lasthit lasthit_except id :regex` + (which comes right after `hits hits_except`, which was already there) + * `RPL_STATSEXCEPTTKL` (230) ends with `id :reason` + * An absent id or spamfilter_id is sent as `-` + * The hits/lasthit/lasthit_except show how often the TKL was hit and + the timestamp of the last hit (the usual, unix time), or 0 for never. + These counts are local to each server. +* `banned_client()` has an extra parameter `const char *tklid` for the + TKL ID (or NULL if none). +* The tkldb database version is now 6260 and stores the id, spamfilter_id and + the hit statistics (hit count and last-hit time per ban). + Older databases still load. Downside: you cannot downgrade UnrealIRCd. +* JSON for TKL entries (server logs and JSON-RPC) now includes `id`, and + `spamfilter_id` for spamfilter-created server bans. +* JSON-RPC `user.get` can now expose flood counters (see next). +* JSON-RPC: We can now route `user.get` requests to the server that user is + on. This so we can fetch all fields for that user (including flood + counters, idle time, snomask) that are normally not available remotely. + * We do this automatically in `user.get` when `object_detail_level` is 5+. + * You can force this explicitly with `object_remote_fetch` set to `true`. + So you can also use it with detail level 2 if you want, e.g. if you + don't need the flood counters but do want the idle time. + * When RRPC is not available we answer ourselves (so safe fallback, but + you won't have the local-only fields). UnrealIRCd 6.2.5 ----------------- diff --git a/doc/conf/examples/example.conf b/doc/conf/examples/example.conf index 0b930b0e17..ff6a12787b 100644 --- a/doc/conf/examples/example.conf +++ b/doc/conf/examples/example.conf @@ -600,6 +600,39 @@ set { } } +/* RECOMMENDED: + * Everyone should be using IRC over SSL/TLS on port 6697. However, to use + * it properly, you have to get a "real" certificate instead of the + * self-signed default certificate that was generated by the installer. + * The Let's Encrypt initiative allows you to get a free certificate that is + * issued by a trusted Certificate Authority. Instructions are at: + * https://www.unrealircd.org/docs/Using_Let's_Encrypt_with_UnrealIRCd + * + * When you follow that guide you will have a "dual certificate" setup: + * set::tls: + * Your trusted CA certificate, served to clients on port 6697. + * (key and certificate change and renew every xx days automatically) + * set::server-linking::tls-options + * A long-lived self-signed certificate for server linking, with + * a stable 'spkifp' signature that you use in link blocks. + * This certificate is used automatically in "serversonly" listen blocks + * (port 6900 in this configuration file) and automatically used for all + * link { } blocks. + * + */ +//set { +// tls { +// certificate "/etc/letsencrypt/live/irc.example.org/fullchain.pem"; +// key "/etc/letsencrypt/live/irc.example.org/privkey.pem"; +// } +// server-linking { +// tls-options { +// certificate "tls/server.cert.pem"; +// key "tls/server.key.pem"; +// } +// } +//} + /* * The following will configure connection throttling of "unknown users". * diff --git a/doc/conf/help/help.fr.conf b/doc/conf/help/help.fr.conf index 775bce2270..b77f60165b 100644 --- a/doc/conf/help/help.fr.conf +++ b/doc/conf/help/help.fr.conf @@ -3,7 +3,7 @@ * Révisé par CC (07/2002) * Ancien traducteur français : babass * Tradution française : Alef Burzmali - http://www.burzmali.com - * Dernière mise à jour : 2010-09-11 + * Dernière mise à jour : 2025-06-05 par Le_Coyote * $Id$ * * Ceci est une mise à jour pratique du système /HELPOP @@ -128,21 +128,27 @@ help Snomasks { "-"; " Ci-dessous, une liste des snomasks disponibles :"; " ==-------------------------oOo-----------------------=="; - " b = Voir les hits sur les blacklists"; - " c = Voir les connexions/déconnexions sur le serveur local"; - " e = Voir les messages 'Eyes' du serveur (OperOverride, utilisation de /CHG* et /SET*, ...)"; - " f = Voir les alertes de flood"; - " F = Voir les connexions/déconnexions sur les serveurs distants (exceptés les U-lines)"; - " G = Voir les notices TKL (Gline, GZline, Shun, etc)"; - " j = Voir les notices Junk (non recommendé pour un usage normal)"; - " k = Voir les notices KILL"; - " n = Voir les changements de pseudo sur le serveur local"; - " N = Voir les changements de pseudo sur les serveurs distants"; - " o = Voir les notices d'identification des opérateurs"; - " q = Voir les rejets de changements de pseudo dus aux Q:lines"; - " s = Voir les notices générales"; - " S = Voir les correspondances au spamfilter"; - " v = Voir l'usage de la commande /VHOST"; + " b = Bans serveurs (Gline, GZline, Shun, etc)"; + " B = Messages du module DNS Blacklist"; + " c = Connexions/déconnexions sur le serveur local"; + " C = Connexions/déconnexions sur les serveurs distants (exceptés les U-lines)"; + " d = DCCs rejetés par les blocs Deny dcc"; + " D = Notices de debugging / junk (NON recommandé)"; + " f = Notices de flood"; + " j = Join, parts et kicks"; + " k = Notices de KILL"; + " n = Changements de pseudo sur le serveur local"; + " N = Changements de pseudo sur les serveurs distants"; + " q = Pseudos rejetés par les Q:lines"; + " s = Notices générales qui ne sont pas dans les autres snomasks"; + " (inclut des messages très importants, donc fortement recommandé)"; + " S = Correspondances aux spamfilters"; + " o = IRCOp utilisant OperOverride sur un canal"; + " O = IRCOp faisant un changement de propriété (/CGHNAME, /CHGIDENT, /CHGHOST, ...)"; + " ou forçant une commande (/SAJOIN, /SAPART) sur un utilisateur"; + " R = Utilisation de JSON-RPC"; + " v = Utilisation de la commande /VHOST"; + " x = Connexions rejetées (maxperip, connthrottle)"; " ==-------------------------oOo------------------------=="; } @@ -894,6 +900,53 @@ help Gzline { " NOTE: requiert le flag oper can_gzline"; } +help Eline { + " Ajoute une exception à un ban, pour que certains utilisateurs ne soient"; + " pas affectés par des K-Lines, G-Lines et autres types de bans."; + " Syntaxe: ELINE (Ajoute une E-Line)"; + " ELINE - (Enlève une E-Line)"; + " Exemple: ELINE *@unrealircd.org kGF 0 Cet utilisateur est exempté"; + " Les valides sont :"; + " ==-Type--------Nom----------------------------Explication-----------------------=="; + " k | K-Line | Exception de K-Lines "; + " G | G-Line | Exception de G-Lines "; + " z | Z-Line | Exception de Z-Lines "; + " Z | GZ-Line | Exception de Z-Lines globales "; + " Q | Q-Line | Exception de Q-Lines "; + " s | shun | Exception de Shuns "; + " F | spamfilter | Exception de spamfilter "; + " b | blacklist | Exception de blacklist "; + " c | connect flood | Exception sur set::anti-flood::connect-flood "; + " d | handshake flood | Exception de test de flood sur les données handhsake "; + " | | (pas de ZLINE si flood avant enregistrement) "; + " m | maxperip | Exeptmion de restricion allow::maxperip restriction "; + " r | antirandom | Exception sur le module antirandom "; + " 8 | antimixedutf8 | Exception sur le module antimixedutf8 "; + " v | version | Exception des blocs ban version { } "; + " ==------------------------------------------------------------------------------=="; + " -"; + " Bans serveurs étendus :"; + " Permet une correspondance sur des critères autres que user/host/ip."; + " Syntaxe: ELINE ~: "; + " Exemple: ELINE ~certfp:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef kGF 0 Utilisateur de confiance avec cette empreinte"; + " Voir /HELPOP EXTSERVERBANS pour les critères, ou la doc en ligne à"; + " https://www.unrealircd.org/docs/Extended_server_bans"; +} + +help Tline { + " Indique le nombre de clients correspondant à un masque de ban serveur"; + " Commande réservée aux IRC Opérateurs."; + " Syntaxe: TLINE "; + " Exemple: TLINE *!*@127.0.0.0/8"; + " -"; + " Bans serveurs étendus :"; + " Permet une correspondance sur des critères autres que user/host/ip."; + " Syntaxe: TLINE ~:"; + " Exemple: TLINE ~realname:*Stupid_bot_script*"; + " Voir /HELPOP EXTSERVERBANS pour les critères, ou la doc en ligne à"; + " https://www.unrealircd.org/docs/Extended_server_bans"; +} + help Rehash { " Fait relire les fichiers de configuration au serveur."; " Commande réservée aux IRC Operateurs."; @@ -1239,7 +1292,7 @@ help Svsmotd { help Svsnline { " Ajoute un ban global sur un realname."; " Doit être envoyé à travers un serveur avec U:Line."; - " La raison doit être un seul paramètre c'est pour quoi"; + " La raison doit être un seul paramètre, c'est pourquoi"; " les espaces sont indiqués par des _, UnrealIRCd les traduira"; " en interne par des espaces"; " -"; diff --git a/doc/conf/modules.default.conf b/doc/conf/modules.default.conf index c6f34be3fc..01bda134b2 100644 --- a/doc/conf/modules.default.conf +++ b/doc/conf/modules.default.conf @@ -258,6 +258,7 @@ loadmodule "extended-monitor"; /* add away status, gecos and userhost changes to loadmodule "standard-replies"; /* Standard Replies */ loadmodule "no-implicit-names"; /* Opt out of receiving implicit NAMES when joining a channel */ loadmodule "extended-isupport"; /* draft/extended-isupport */ +loadmodule "multiline"; /* draft/multiline */ /*** RPC modules ***/ // There's a JSON-RPC interface that can be used to communicate with ObbyIRCd diff --git a/extras/tls.cnf b/doc/conf/tls/tls.cnf similarity index 92% rename from extras/tls.cnf rename to doc/conf/tls/tls.cnf index 46a9611596..d768febe82 100644 --- a/extras/tls.cnf +++ b/doc/conf/tls/tls.cnf @@ -4,7 +4,6 @@ # Note: RSA bits is ignored, as we use ECC now default_bits = 2048 distinguished_name = req_dn -x509_extensions = cert_type [ req_dn ] countryName = Country Name @@ -26,6 +25,3 @@ organizationalUnitName_default = IRCd 0.commonName = Common Name (Full domain of your server) 1.commonName_value = localhost -[ cert_type ] -nsCertType = server - diff --git a/extras/build-tests/nix/build b/extras/build-tests/nix/build index f381fd94f3..689d64e705 100755 --- a/extras/build-tests/nix/build +++ b/extras/build-tests/nix/build @@ -41,11 +41,11 @@ if [ "$SSLDIR" != "" ]; then fi ./Config -quick || (tail -n 5000 config.log; exit 1) $MAKE -yes ''|$MAKE pem $MAKE || exit 1 $MAKE install || exit 1 # The control binary is installed into BASEPATH and is named obbyircd after the # rebrand; run it from there rather than the (binary-less) source dir. +(cd ~/unrealircd && (echo 'Y'; yes '')|./obbyircd mkcert) || exit 1 (cd ~/unrealircd && ./obbyircd module install third/dumpcmds) || exit 1 set +x diff --git a/include/config.h b/include/config.h index 952889706b..7d3308abfc 100644 --- a/include/config.h +++ b/include/config.h @@ -238,6 +238,16 @@ #define SPAMFILTER_DETECTSLOW #endif +/* Limits for PCRE2 regex matching (eg. spamfilter, badwords). A regex that + * exceeds these is aborted and treated as no match, instead of running + * unbounded. The match limit is honoured by JIT. The depth limit only applies + * to the non-JIT interpreter, since PCRE2 ignores it under JIT. + * We use the same defaults that PHP has been using for a long time (which is + * actually 10 times lower than PCRE2 defaults, as of 2026). + */ +#define UNREAL_PCRE2_MATCH_LIMIT 1000000 +#define UNREAL_PCRE2_DEPTH_LIMIT 100000 + /* Maximum number of ModData objects that may be attached to an object */ /* UnrealIRCd 4.0.0: 8, 8, 4, 4 * UnrealIRCd 4.0.14: 12, 8, 4, 4 diff --git a/include/dynconf.h b/include/dynconf.h index 4385c2fa54..1f83146a05 100644 --- a/include/dynconf.h +++ b/include/dynconf.h @@ -100,6 +100,9 @@ struct Configuration { int maxdccallow; int anti_spam_quit_message_time; TLSOptions *tls_options; + TLSOptions *server_linking_tls_options; /**< set::server-linking::tls-options, NULL if not configured */ + int server_linking_mixed_certificates; /**< set::server-linking::mixed-certificates: suppress the s2s cert mismatch warning */ + int server_linking_allow_ca_certificate; /**< set::server-linking::allow-ca-certificate: suppress the "CA cert used for linking" advice */ Policy plaintext_policy_user; MultiLine *plaintext_policy_user_message; Policy plaintext_policy_oper; diff --git a/include/h.h b/include/h.h index e2e5c9372e..8434a334d9 100644 --- a/include/h.h +++ b/include/h.h @@ -847,6 +847,9 @@ extern MODVAR void (*free_tkl)(TKL *tkl); extern MODVAR void (*tkl_del_line)(TKL *tkl); extern MODVAR void (*tkl_check_local_remove_shun)(TKL *tmp); extern MODVAR int (*find_tkline_match)(Client *cptr, int skip_soft); +extern MODVAR void (*tkl_hit)(Client *client, TKL *tkl); +extern MODVAR void (*remove_config_tkls)(int flag); +extern MODVAR void (*config_tkl_hits_restore)(void); extern MODVAR int (*find_shun)(Client *cptr); extern MODVAR int (*find_spamfilter_user)(Client *client, int flags); extern MODVAR TKL *(*find_qline)(Client *cptr, const char *nick, int *ishold); @@ -857,6 +860,7 @@ extern MODVAR void (*cmd_tkl)(ClientContext *clictx, Client *client, MessageTag extern MODVAR int (*take_action)(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped); extern MODVAR int (*match_spamfilter)(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, ClientContext *clictx, TKL **rettk); extern MODVAR int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd); +extern MODVAR void (*run_deferred_rule_only_spamfilters)(Client *client); extern MODVAR int (*join_viruschan)(Client *client, TKL *tk, int type); extern MODVAR const char *(*StripColors)(const char *text); extern MODVAR void (*spamfilter_build_user_string)(char *buf, const char *nick, Client *acptr); @@ -953,7 +957,7 @@ extern MODVAR int (*decode_authenticate_plain)(const char *param, char **authori extern MODVAR void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); extern MODVAR void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); extern MODVAR void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); +extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); extern MODVAR char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); extern MODVAR char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); extern MODVAR const char *(*utf8_get_block_name)(int i); @@ -963,6 +967,8 @@ extern MODVAR void (*isupport_check_for_changes)(void); extern MODVAR int (*get_connections_from_ip)(Client *client); extern MODVAR int (*get_floodprot_channel_max_lines)(Channel *channel); extern MODVAR int (*floodprot_check_multiline_batch)(Channel *channel, Client *client, int line_count); +extern MODVAR int (*channel_flood_blocked_count)(Client *client, const char *type); +extern MODVAR void (*channel_flood_expand_json)(json_t *root, Client *client); /* /Efuncs */ /* TLS functions */ @@ -977,6 +983,12 @@ extern const char *ssl_error_str(int err, int my_errno); extern void unreal_tls_client_handshake(int, int, void *); extern void SSL_set_nonblocking(SSL *s); extern SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server); +extern SSL_CTX *tls_ctx_for_listener(ConfigItem_listen *listener); +extern SSL_CTX *tls_ctx_for_outgoing_link(ConfigItem_link *link); +extern TLSOptions *tls_options_for_listener(ConfigItem_listen *listener); +extern TLSOptions *tls_options_for_outgoing_link(ConfigItem_link *link); +extern void test_tlsblock(ConfigFile *conf, ConfigEntry *cep, int *totalerrors); +extern void set_server_linking_tlsoptions_ce(ConfigEntry *ce); extern const char *tls_get_cipher(Client *client); extern TLSOptions *get_tls_options_for_client(Client *acptr); extern int outdated_tls_client(Client *acptr); @@ -1004,6 +1016,8 @@ extern int is_silenced_default_handler(Client *client, Client *acptr); extern int get_connections_from_ip_default_handler(Client *client); extern int get_floodprot_channel_max_lines_default_handler(Channel *channel); extern int floodprot_check_multiline_batch_default_handler(Channel *channel, Client *client, int line_count); +extern int channel_flood_blocked_count_default_handler(Client *client, const char *type); +extern void channel_flood_expand_json_default_handler(json_t *root, Client *client); 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, const char *autojoin_channels); extern void webserver_send_response_default_handler(Client *client, int status, char *msg); @@ -1091,9 +1105,11 @@ extern void read_packet(int fd, int revents, void *data); extern int process_packet(Client *cptr, char *readbuf, int length, int killsafely); extern int parse_chanmode(ParseMode *pm, const char *modebuf_in, const char *parabuf_in); extern int dead_socket(Client *to, const char *notice); -extern Match *unreal_create_match(MatchType type, const char *str, char **error); +extern MODVAR pcre2_match_context *unreal_pcre2_match_ctx; +extern void init_match(void); +extern Match *unreal_create_match(MatchType type, const char *str, const char **error); extern void unreal_delete_match(Match *m); -extern int unreal_match(Match *m, const char *str); +extern int unreal_match(Match *m, const char *str, const char **error); extern int unreal_match_method_strtoval(const char *str); extern char *unreal_match_method_valtostr(int val); #ifdef _WIN32 @@ -1128,6 +1144,7 @@ extern MODVAR json_t *json_rehash_log; extern MODVAR int debugfd; extern void convert_to_absolute_path(char **path, const char *reldir); extern char *convert_to_absolute_path_duplicate(char *path, char *reldir); +extern const char *display_path(const char *path, const char *reldir); extern int has_user_mode(Client *acptr, char mode); extern int has_channel_mode(Channel *channel, char mode); extern int has_channel_mode_raw(Cmode_t m, char mode); @@ -1177,6 +1194,7 @@ extern void sendbufto_one(Client *to, char *msg, unsigned int quick); extern MODVAR int current_serial; extern const char *spki_fingerprint(Client *acptr); extern const char *spki_fingerprint_ex(X509 *x509_cert); +extern int is_trusted_cert(SSL_CTX *ctx); extern int is_module_loaded(const char *name); extern int is_blacklisted_module(const char *name); extern void close_std_descriptors(void); @@ -1223,7 +1241,7 @@ extern HistoryResult *history_request(const char *object, HistoryFilter *filter) extern int history_delete(const char *object, HistoryFilter *filter, int *rejected_deletes); extern int history_destroy(const char *object); extern int can_receive_history(Client *client); -extern void history_send_result(Client *client, HistoryResult *r); +extern void history_send_result(Client *client, HistoryResult *r, int end_of_pagination); extern void free_history_result(HistoryResult *r); extern void free_history_filter(HistoryFilter *f); extern void special_delayed_unloading(void); @@ -1237,6 +1255,7 @@ extern int write_str(FILE *fd, const char *x); extern int read_str(FILE *fd, char **x); extern void _free_entire_name_list(NameList *n); extern void _add_name_list(NameList **list, const char *name); +extern void _append_name_list(NameList **list, const char *name); extern void _del_name_list(NameList **list, const char *name); extern NameList *duplicate_name_list(NameList *e); extern NameList *find_name_list(NameList *list, const char *name); @@ -1334,6 +1353,7 @@ extern void json_expand_client_security_groups(json_t *parent, Client *client); extern void json_expand_channel(json_t *j, const char *key, Channel *channel, int detail); extern void json_expand_tkl(json_t *j, const char *key, TKL *tkl, int detail); extern void json_expand_textanalysis(json_t *root, const char *key, TextAnalysis *ta, int detail); +extern void json_expand_flood_counts(json_t *root, const char *key, Client *client); extern void json_expand_mask_list(json_t *parent, const char *key, ConfigItem_mask *mask); extern void json_expand_name_list(json_t *parent, const char *key, NameList *list); extern void json_expand_nvplist(json_t *parent, const char *key, NameValuePrioList *list); @@ -1414,7 +1434,9 @@ extern int flood_limit_exceeded(Client *client, FloodOption opt); extern FloodSettings *find_floodsettings_block(const char *name); extern FloodSettings *get_floodsettings_for_user(Client *client, FloodOption opt); extern MODVAR const char *floodoption_names[]; +extern MODVAR const char *floodoption_shortnames[]; extern void flood_limit_exceeded_log(Client *client, const char *floodname); +extern void flood_blocked_increment(Client *client, FloodOption opt); /* logging */ extern int config_test_log(ConfigFile *conf, ConfigEntry *ce); extern int config_run_log(ConfigFile *conf, ConfigEntry *ce); @@ -1459,6 +1481,8 @@ extern void do_unreal_log(LogLevel loglevel, const char *subsystem, const char * extern void do_unreal_log_raw(LogLevel loglevel, const char *subsystem, const char *event_id, Client *client, const char *msg, ...); extern void do_unreal_log_internal_from_remote(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, json_t *json, const char *json_serialized, Client *from_server); extern LogData *log_data_string(const char *key, const char *str); +extern LogData *log_data_optional_name_value(const char *key, const char *label, const char *value); +extern LogData *log_data_optional_string(const char *key, const char *value); extern LogData *log_data_char(const char *key, const char c); extern LogData *log_data_integer(const char *key, int64_t integer); extern LogData *log_data_timestamp(const char *key, time_t ts); diff --git a/include/modules.h b/include/modules.h index fe7c2259d5..def4b9dfdb 100644 --- a/include/modules.h +++ b/include/modules.h @@ -529,6 +529,7 @@ struct ClientCapability { MessageTagHandler *mtag_handler; /**< For reverse dependency */ Module *owner; /**< Module introducing this CAP. */ char unloaded; /**< Internal flag to indicate module is being unloaded */ + int minimum_cap_version; /**< Minimum CAP version to show this CAP */ }; typedef struct { @@ -536,6 +537,7 @@ typedef struct { int flags; int (*visible)(Client *); const char *(*parameter)(Client *); + int minimum_cap_version; } ClientCapabilityInfo; /** @defgroup MessagetagAPI Message tag API @@ -633,6 +635,7 @@ struct HistoryResult { HistoryLogLine *log_tail; /**< Last entry in the log lines */ int num_lines; /**< Total number of lines in the result */ int num_bytes; /**< Total bytes of all lines in the result */ + int reached_end; /**< Set by backend: 1 = no more history beyond this page, 0 = more may exist (or unknown). Used by the draft/chathistory-end tag. Default 0. */ }; /** History Backend */ @@ -1576,6 +1579,8 @@ extern APICallback *APICallbackAdd(Module *module, APICallback *mreq); #define HOOKTYPE_KNOWN_USER_CACHE_CHANGE 131 /** See hooktype_chanmsg_multiline() */ #define HOOKTYPE_CHANMSG_MULTILINE 132 +/** See hooktype_postconf() */ +#define HOOKTYPE_POSTCONF 133 /* Adding a new hook here? @@ -2062,6 +2067,13 @@ int hooktype_rehash(void); */ int hooktype_rehash_complete(void); +/** Called at the end of postconf(), after the configuration is fully applied and + * TLS contexts are (re)built, on both boot and rehash (function prototype for + * HOOKTYPE_POSTCONF). + * @return The return value is ignored (use return 0) + */ +int hooktype_postconf(void); + /** Called when searching for a test function for a specific configuration item (function prototype for HOOKTYPE_CONFIGTEST). * This is part of the configuration API, which is better documented at the * wiki at https://www.unrealircd.org/docs/Dev:Configuration_API @@ -2806,6 +2818,7 @@ _UNREAL_ERROR(_hook_error_incompatible, "Incompatible hook function. Check argum ((hooktype == HOOKTYPE_UMODE_CHANGE) && !ValidateHook(hooktype_umode_change, func)) || \ ((hooktype == HOOKTYPE_TOPIC) && !ValidateHook(hooktype_topic, func)) || \ ((hooktype == HOOKTYPE_REHASH_COMPLETE) && !ValidateHook(hooktype_rehash_complete, func)) || \ + ((hooktype == HOOKTYPE_POSTCONF) && !ValidateHook(hooktype_postconf, func)) || \ ((hooktype == HOOKTYPE_TKL_ADD) && !ValidateHook(hooktype_tkl_add, func)) || \ ((hooktype == HOOKTYPE_TKL_DEL) && !ValidateHook(hooktype_tkl_del, func)) || \ ((hooktype == HOOKTYPE_LOCAL_KILL) && !ValidateHook(hooktype_local_kill, func)) || \ @@ -2945,6 +2958,7 @@ enum EfunctionType { EFUNC_TAKE_ACTION, EFUNC_MATCH_SPAMFILTER, EFUNC_MATCH_SPAMFILTER_MTAGS, + EFUNC_RUN_DEFERRED_RULE_ONLY_SPAMFILTERS, EFUNC_JOIN_VIRUSCHAN, EFUNC_FIND_TKLINE_MATCH_ZAP_EX, EFUNC_SEND_LIST, @@ -3067,6 +3081,11 @@ enum EfunctionType { EFUNC_GET_CONNECTIONS_FROM_IP, EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, + EFUNC_CHANNEL_FLOOD_BLOCKED_COUNT, + EFUNC_CHANNEL_FLOOD_EXPAND_JSON, + EFUNC_TKL_HIT, + EFUNC_REMOVE_CONFIG_TKLS, + EFUNC_CONFIG_TKL_HITS_RESTORE, }; /* Module flags */ diff --git a/include/numeric.h b/include/numeric.h index f0e0997c13..3d29373a6e 100644 --- a/include/numeric.h +++ b/include/numeric.h @@ -383,7 +383,7 @@ #define STR_RPL_STATSCOMMANDS /* 212 */ "%s %u %lu" #define STR_RPL_STATSCLINE /* 213 */ "%c %s * %s %d %d %s" #define STR_RPL_STATSILINE /* 215 */ "I %s %s %d %d %s %s %d" -#define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s :%s" +#define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s %lld %lld %s :%s" #define STR_RPL_STATSYLINE /* 218 */ "Y %s %d %d %d %d %d" #define STR_RPL_ENDOFSTATS /* 219 */ "%c :End of /STATS report" #define STR_RPL_UMODEIS /* 221 */ "%s" @@ -393,7 +393,7 @@ #define STR_RPL_STATSVLINE /* 227 */ "v %s %s %s" #define STR_RPL_STATSBANVER /* 228 */ "%s %s" #define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld %lld %lld %s :%s" -#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s :%s" +#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s %s :%s" #define STR_RPL_RULES /* 232 */ ":- %s" #define STR_RPL_STATSLLINE /* 241 */ "%c %s * %s %d %d" #define STR_RPL_STATSUPTIME /* 242 */ ":Server Up %lld days, %lld:%02lld:%02lld" diff --git a/include/struct.h b/include/struct.h index b9b83d28b2..eafe16fd02 100644 --- a/include/struct.h +++ b/include/struct.h @@ -204,7 +204,6 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia #define MAXCCUSERS 20 /* Maximum for set::anti-flood::max-concurrent-conversations */ #define BATCHLEN 22 #define MAXBATCHREFLEN 48 /* Max length of client-sent batch reference tags */ -#define MAXSPAMFILTERIDLEN 24 /* * Watch it - Don't change this unless you also change the ERR_TOOMANYWATCH @@ -229,6 +228,8 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia typedef enum LogFieldType { LOG_FIELD_INTEGER, // and unsigned? LOG_FIELD_STRING, + LOG_FIELD_OPTIONAL_NAME_VALUE, + LOG_FIELD_OPTIONAL_STRING, LOG_FIELD_CLIENT, LOG_FIELD_CHANNEL, LOG_FIELD_OBJECT, @@ -238,6 +239,7 @@ typedef enum LogFieldType { typedef struct LogData { LogFieldType type; char *key; + char *label; /**< Display label for LOG_FIELD_OPTIONAL_NAME_VALUE, NULL otherwise */ union { int64_t integer; char *string; @@ -800,7 +802,7 @@ typedef struct NameList NameList; * that only have a name and no other properties. * * Use the following functions to add, find and delete entries: - * add_name_list(), find_name_list(), del_name_list(), free_entire_name_list() + * add_name_list(), append_name_list(), find_name_list(), del_name_list(), free_entire_name_list() */ struct NameList { NameList *prev, *next; @@ -813,6 +815,8 @@ struct NameList { /** Add an entry to a NameList */ #define add_name_list(list, str) _add_name_list(&list, str) +/** Append an entry to the end of a NameList */ +#define append_name_list(list, str) _append_name_list(&list, str) /** Delete an entry from a NameList - AND free it */ #define del_name_list(list, str) _del_name_list(&list, str) @@ -1292,8 +1296,8 @@ struct Spamfilter { char *tkl_reason; /**< Reason to use for bans placed by this spamfilter, escaped by unreal_encodespace(). */ time_t tkl_duration; /**< Duration of bans placed by this spamfilter */ char *id; /**< ID */ - long long hits; /**< Spamfilter hits (except exempts) */ long long hits_except; /**< Spamfilter hits by exempt clients */ + time_t lasthit_except; /**< When an exempt client last hit this spamfilter, or 0 if never */ SecurityGroup *except; /**< Don't run this spamfilter at all for these users (not counting towards hits_except btw) */ int input_conversion; /**< How we should handle the input */ /** For overriding set::spamfilter::show-message-content-on-hit @@ -1319,9 +1323,9 @@ struct BanException { #define TKL_FLAG_CONFIG 0x0001 /* Entry from configuration file. Cannot be removed by using commands. */ #define TKL_FLAG_CENTRAL_SPAMFILTER 0x0002 /* Entry from central spamfilter. */ +#define TKLIDLEN 16 /**< TKL id buffer size: native 'prefix+body' or an external id. Max 15 chars + NUL. */ + /** A TKL entry, such as a KLINE, GLINE, Spamfilter, QLINE, Exception, .. */ -/** Length of the locally-generated TKL identifier (prefix char + 10 base32 chars + NUL). */ -#define TKLIDLEN 12 struct TKL { TKL *prev, *next; @@ -1330,11 +1334,10 @@ struct TKL { char *set_by; /**< By who was this entry added */ time_t set_at; /**< When this entry was added */ time_t expire_at; /**< When this entry will expire */ - char id[TKLIDLEN]; /**< Stable per-TKL identifier exposed in /STATS and the reject message: - * "+base32(SHA1(stable fields))". Deterministic, so peer servers - * independently compute the same id for the same TKL. */ - long long hits; /**< How many times this TKL has fired locally since boot. */ - time_t lasthit; /**< Absolute unix timestamp of the most recent local hit, or 0 if never. */ + char id[TKLIDLEN]; /**< Unique ID: assigned (random, generated by originating server) or external (eg from services), or empty string if none */ + char spamfilter_id[TKLIDLEN]; /**< For server bans created by a spamfilter: that spamfilter's id. Empty otherwise. */ + long long hits; /**< Number of times this TKL was enforced (counted locally on this server) */ + time_t lasthit; /**< When this TKL was last enforced, or 0 if never */ union { Spamfilter *spamfilter; ServerBan *serverban; @@ -1457,8 +1460,9 @@ extern void moddatatype_dump(Client *client); /** Flood counters for local clients */ typedef struct FloodCounter { - int count; - long t; + int count; /**< Maximum allowed number per... */ + long t; /**< ..time period */ + int blocked; /**< How many times this flood limit was hit */ } FloodCounter; /** This is the list of different flood counters that we keep for local clients. */ @@ -1566,6 +1570,7 @@ struct LocalClient { RPCClient *rpc; /**< RPC Client, or NULL */ Tag *tags; /**< Tags from spamfilter */ int tags_serial; /**< To keep track of 'tags' changes */ + int spamfilter_run_tags_serial; /**< The last time (well, tags_serial) that rule-only-spamfilters ran. */ }; /** User information (persons, not servers), you use client->user to access these (see also @link Client @endlink). @@ -1893,6 +1898,7 @@ typedef struct TLSOptions TLSOptions; struct TLSOptions { NameList *certificate_files; NameList *key_files; + NameList *spkifp; /**< spkifp of each loaded certificate, computed by init_ctx() */ char *trusted_ca_file; unsigned int protocols; char *ciphers; @@ -2802,6 +2808,7 @@ typedef enum JsonRpcError { #define BUILDVARSTRING_URLENCODE 0x1 #define BUILDVARSTRING_XML 0x2 #define BUILDVARSTRING_UNKNOWN_VAR_IS_EMPTY 0x4 +#define BUILDVARSTRING_KEEP_SPACE_FOR_EMPTY_VAR 0x8 #endif /* __struct_include__ */ diff --git a/obbyircd.in b/obbyircd.in index c3a1f25dcb..a2cbeceed2 100644 --- a/obbyircd.in +++ b/obbyircd.in @@ -11,6 +11,7 @@ TMPDIR="@TMPDIR@" SCRIPTDIR="@SCRIPTDIR@" MODULESDIR="@MODULESDIR@" DOCDIR="@DOCDIR@" +OPENSSLPATH="@OPENSSLPATH@" # When built with --with-asan, ASan does not dump core by default because # older gcc/clang might dump a 16TB core file. We explicitly enable it here. @@ -19,7 +20,7 @@ DOCDIR="@DOCDIR@" # 'Mod_Header' global and trips ASan's cross-DSO ODR checker on a false match. export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1:log_path=$TMPDIR/unrealircd_asan:detect_leaks=0:detect_odr_violation=0" -if [ ! -f $IRCD ]; then +if [ "$1" != "mkcert" ] && [ ! -f $IRCD ]; then echo "ERROR: Could not find the IRCd binary ($IRCD)" echo "This could mean two things:" echo "1) You forgot to run 'make install' after running 'make'" @@ -271,6 +272,84 @@ __EOF__ echo "Thanks!" elif [ "$1" = "spki" -o "$1" = "spkifp" ] ; then $UNREALIRCDCTL $* +elif [ "$1" = "mkcert" ] ; then + TLSDIR="$CONFDIR/tls" + BASE="$CONFDIR/tls/server" + if [ "$2" != "" ]; then + # Optional second argument: path prefix for the certificate/key + BASE="$2" + fi + KEY="$BASE.key.pem" + CERT="$BASE.cert.pem" + OUTDIR=`dirname "$CERT"` + + # Locate the OpenSSL configuration template. After 'make install' it + # lives in the TLS directory. During initial setup (./Config), before + # 'make install' has run, we use the copy in the source directory. + if [ -f "$TLSDIR/tls.cnf" ]; then + CNF="$TLSDIR/tls.cnf" + elif [ -f "$BUILDDIR/doc/conf/tls/tls.cnf" ]; then + CNF="$BUILDDIR/doc/conf/tls/tls.cnf" + else + echo "ERROR: Could not find the OpenSSL template tls.cnf" + echo "(Neither $TLSDIR/tls.cnf nor $BUILDDIR/doc/conf/tls/tls.cnf exists)" + exit 1 + fi + + if [ ! -d "$OUTDIR" ]; then + mkdir -p "$OUTDIR" || exit 1 + chmod 0700 "$OUTDIR" + fi + + REPLACED=0 + if [ -f "$CERT" ] || [ -f "$KEY" ]; then + echo "This command will replace your existing certificate and key." + echo "(in $OUTDIR)" + echo -n "Do you wish to proceed? [Y|N] " + read answer + case "$answer" in + [Yy]*) + ;; + *) + echo "Aborted." + exit 1 + ;; + esac + REPLACED=1 + fi + + # Keep a backup of the previous certificate and key, so it can be + # restored if the newly generated one turns out to be unsuitable. + if [ "$REPLACED" = 1 ]; then + for f in "$KEY" "$CERT"; do + if [ -f "$f" ]; then + cp -p "$f" "$f.old" + fi + done + fi + + # Make sure the private key is not briefly world/group readable while + # it is being generated. + umask 077 + + echo "Generating server key..." + "$OPENSSLPATH" ecparam -out "$KEY" -name secp384r1 -genkey || exit 1 + echo "Generating self-signed certificate..." + "$OPENSSLPATH" req -new -x509 -key "$KEY" -config "$CNF" -days 3650 -sha256 -out "$CERT" || exit 1 + + echo "Setting permissions on the certificate and key..." + chmod o-rwx "$KEY" "$CERT" + chmod g-rwx "$KEY" "$CERT" + + echo "" + echo "A new self-signed certificate and key have been generated in $OUTDIR" + if [ "$REPLACED" = 1 ]; then + echo "Your previous certificate and key were backed up with a .old suffix." + fi + echo "Note: the SPKI fingerprint has changed. If other servers link to you and" + echo " verify a fingerprint, you need to update the link { } block on their side." + echo "If UnrealIRCd is currently running, load the new certificate with:" + echo " $0 reloadtls" elif [ "$1" = "hot-patch" -o "$1" = "cold-patch" ] ; then if [ ! -d "$BUILDDIR" ]; then echo "ObbyIRCd source not found. Sorry, it is not possible to patch." @@ -443,6 +522,8 @@ else echo "obbyircd upgrade Upgrade ObbyIRCd to the latest version (or the next available" echo " release candidate if the --rc argument is used)" echo "obbyircd mkpasswd Hash a password" + echo "obbyircd mkcert Create or replace the self-signed TLS certificate" + echo " (optionally pass a path prefix; normal path rules apply)" echo "obbyircd version Display the ObbyIRCd version" echo "obbyircd module Install and uninstall 3rd party modules" echo "obbyircd croncheck For use in crontab: this checks if the server" diff --git a/src/aliases.c b/src/aliases.c index 6b63904987..b66154f6f0 100644 --- a/src/aliases.c +++ b/src/aliases.c @@ -143,7 +143,7 @@ void cmd_alias(ClientContext *clictx, Client *client, MessageTag *mtags, int par for (format = alias->format; format; format = format->next) { - if (unreal_match(format->expr, ptr)) + if (unreal_match(format->expr, ptr, NULL)) { /* Parse the parameters */ int i = 0, j = 0, k = 1; diff --git a/src/api-clicap.c b/src/api-clicap.c index bb6991c97f..5c6060cf21 100644 --- a/src/api-clicap.c +++ b/src/api-clicap.c @@ -204,6 +204,7 @@ ClientCapability *ClientCapabilityAdd(Module *module, ClientCapabilityInfo *clic /* Add or update the following fields: */ clicap->owner = module; clicap->flags = clicap_request->flags; + clicap->minimum_cap_version = clicap_request->minimum_cap_version; clicap->visible = clicap_request->visible; clicap->parameter = clicap_request->parameter; diff --git a/src/api-efunctions.c b/src/api-efunctions.c index 359a775f49..386bd05480 100644 --- a/src/api-efunctions.c +++ b/src/api-efunctions.c @@ -73,6 +73,9 @@ TKL *(*tkl_add_banexception)(int type, const char *usermask, const char *hostmas void (*tkl_del_line)(TKL *tkl); void (*tkl_check_local_remove_shun)(TKL *tmp); int (*find_tkline_match)(Client *client, int skip_soft); +void (*tkl_hit)(Client *client, TKL *tkl); +void (*remove_config_tkls)(int flag); +void (*config_tkl_hits_restore)(void); int (*find_shun)(Client *client); int(*find_spamfilter_user)(Client *client, int flags); TKL *(*find_qline)(Client *client, const char *nick, int *ishold); @@ -83,6 +86,7 @@ void (*cmd_tkl)(ClientContext *clictx, Client *client, MessageTag *mtags, int pa int (*take_action)(Client *client, BanAction *action, const char *reason, long duration, int take_action_flags, int *stopped); int (*match_spamfilter)(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, ClientContext *clictx, TKL **rettk); int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd); +void (*run_deferred_rule_only_spamfilters)(Client *client); int (*join_viruschan)(Client *client, TKL *tk, int type); const char *(*StripColors)(const char *text); void (*spamfilter_build_user_string)(char *buf, const char *nick, Client *client); @@ -184,7 +188,7 @@ int (*decode_authenticate_plain)(const char *param, char **authorization_id, cha void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...); void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); +void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); const char *(*utf8_get_block_name)(int i); @@ -194,6 +198,8 @@ void (*isupport_check_for_changes)(void); int (*get_connections_from_ip)(Client *client); int (*get_floodprot_channel_max_lines)(Channel *channel); int (*floodprot_check_multiline_batch)(Channel *channel, Client *client, int line_count); +int (*channel_flood_blocked_count)(Client *client, const char *type); +void (*channel_flood_expand_json)(json_t *root, Client *client); Efunction *EfunctionAddMain(Module *module, EfunctionType eftype, int (*func)(), void (*vfunc)(), void *(*pvfunc)(), char *(*stringfunc)(), const char *(*conststringfunc)()) { @@ -422,6 +428,9 @@ void efunctions_init(void) efunc_init_function(EFUNC_TKL_DEL_LINE, tkl_del_line, NULL, 0); efunc_init_function(EFUNC_TKL_CHECK_LOCAL_REMOVE_SHUN, tkl_check_local_remove_shun, NULL, 0); efunc_init_function(EFUNC_FIND_TKLINE_MATCH, find_tkline_match, NULL, 0); + efunc_init_function(EFUNC_TKL_HIT, tkl_hit, NULL, 0); + efunc_init_function(EFUNC_REMOVE_CONFIG_TKLS, remove_config_tkls, NULL, 0); + efunc_init_function(EFUNC_CONFIG_TKL_HITS_RESTORE, config_tkl_hits_restore, NULL, 0); efunc_init_function(EFUNC_FIND_SHUN, find_shun, NULL, 0); efunc_init_function(EFUNC_FIND_SPAMFILTER_USER, find_spamfilter_user, NULL, 0); efunc_init_function(EFUNC_FIND_QLINE, find_qline, NULL, 0); @@ -432,6 +441,7 @@ void efunctions_init(void) efunc_init_function(EFUNC_TAKE_ACTION, take_action, NULL, 0); efunc_init_function(EFUNC_MATCH_SPAMFILTER, match_spamfilter, NULL, 0); efunc_init_function(EFUNC_MATCH_SPAMFILTER_MTAGS, match_spamfilter_mtags, NULL, 0); + efunc_init_function(EFUNC_RUN_DEFERRED_RULE_ONLY_SPAMFILTERS, run_deferred_rule_only_spamfilters, NULL, 0); efunc_init_function(EFUNC_JOIN_VIRUSCHAN, join_viruschan, NULL, 0); efunc_init_function(EFUNC_STRIPCOLORS, StripColors, NULL, 0); efunc_init_function(EFUNC_SPAMFILTER_BUILD_USER_STRING, spamfilter_build_user_string, NULL, 0); @@ -550,4 +560,6 @@ void efunctions_init(void) efunc_init_function(EFUNC_GET_CONNECTIONS_FROM_IP, get_connections_from_ip, get_connections_from_ip_default_handler, 0); efunc_init_function(EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, get_floodprot_channel_max_lines, get_floodprot_channel_max_lines_default_handler, 0); efunc_init_function(EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, floodprot_check_multiline_batch, floodprot_check_multiline_batch_default_handler, 0); + efunc_init_function(EFUNC_CHANNEL_FLOOD_BLOCKED_COUNT, channel_flood_blocked_count, channel_flood_blocked_count_default_handler, 0); + efunc_init_function(EFUNC_CHANNEL_FLOOD_EXPAND_JSON, channel_flood_expand_json, channel_flood_expand_json_default_handler, 0); } diff --git a/src/api-history-backend.c b/src/api-history-backend.c index adb5c6a2a8..aae33d4d1f 100644 --- a/src/api-history-backend.c +++ b/src/api-history-backend.c @@ -389,11 +389,12 @@ static void history_send_result_multiline_fallback(Client *client, HistoryLogLin * @param client The client to send to. * @param r The history result retrieved via history_request(). */ -void history_send_result(Client *client, HistoryResult *r) +void history_send_result(Client *client, HistoryResult *r, int end_of_pagination) { char batch[BATCHLEN+1]; HistoryLogLine *l; int has_multiline; + MessageTag *batch_open_mtags = NULL; if (!can_receive_history(client)) return; @@ -403,7 +404,14 @@ void history_send_result(Client *client, HistoryResult *r) { /* Start a new batch */ generate_batch_id(batch); - sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, r->object); + if (end_of_pagination) + { + batch_open_mtags = safe_alloc(sizeof(MessageTag)); + safe_strdup(batch_open_mtags->name, "draft/chathistory-end"); + } + sendto_one(client, batch_open_mtags, ":%s BATCH +%s chathistory %s", me.name, batch, r->object); + if (batch_open_mtags) + free_message_tags(batch_open_mtags); } has_multiline = HasCapability(client, "draft/multiline") && HasCapability(client, "batch"); diff --git a/src/conf.c b/src/conf.c index c992ae9ad0..e3fed927df 100644 --- a/src/conf.c +++ b/src/conf.c @@ -201,8 +201,7 @@ int reloadable_perm_module_unloaded(void); int tls_tests(void); /* Conf sub-sub-functions */ -void test_tlsblock(ConfigFile *conf, ConfigEntry *cep, int *totalerrors); -void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions); +void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions, TLSOptions *inherit_from); void free_tls_options(TLSOptions *tlsoptions); /* @@ -248,6 +247,7 @@ Configuration iConf; Configuration tempiConf; BestPractices bestpractices; MODVAR ConfigFile *conf = NULL; +static ConfigEntry *server_linking_tlsoptions_ce = NULL; /* Hack to get set::server-linking::tls-options */ extern NameValueList *config_defines; MODVAR int ipv6_disabled = 0; MODVAR Client *remote_rehash_client = NULL; @@ -274,7 +274,6 @@ int rehash_internal(Client *client); int is_blacklisted_module(const char *name); int modules_default_conf_modified(const char *filebuf); int config_item_allowed_for_config_file(const char *resource, const char *item); -void remove_config_tkls(int flag); void free_operclass_struct(OperClass *o); /** Return the printable string of a 'cep' location, such as set::something::xyz */ @@ -726,11 +725,12 @@ void chmode_str(struct ChMode *modes, char *mbuf, char *pbuf, size_t mbuf_size, { Cmode *cm; - if (!(mbuf_size && pbuf_size)) + if ((mbuf_size < 2) || !pbuf_size) return; *pbuf = 0; *mbuf++ = '+'; + mbuf_size--; for (cm=channelmodes; cm; cm = cm->next) { @@ -1771,6 +1771,8 @@ void free_iConf(Configuration *i) free_tls_options(i->tls_options); i->tls_options = NULL; safe_free(i->tls_options); + free_tls_options(i->server_linking_tls_options); + i->server_linking_tls_options = NULL; safe_free_multiline(i->plaintext_policy_user_message); safe_free_multiline(i->plaintext_policy_oper_message); safe_free(i->outdated_tls_policy_user_message); @@ -1906,6 +1908,7 @@ void config_setdefaultsettings(Configuration *i) add_log_throttle_config(&i->log_throttle, "BUG_CT_BUCKET_MISSING", 5, 60, 0); add_log_throttle_config(&i->log_throttle, "BUG_CT_NEGATIVE_COUNTER", 5, 60, 0); add_log_throttle_config(&i->log_throttle, "BUG_DECREASE_IPUSERS_BUCKET", 5, 60, 0); + add_log_throttle_config(&i->log_throttle, "SPAMFILTER_REGEX_ERROR", 5, 60, 0); /* TLS options */ i->tls_options = safe_alloc(sizeof(TLSOptions)); @@ -1921,6 +1924,9 @@ void config_setdefaultsettings(Configuration *i) */ safe_strdup(i->tls_options->outdated_ciphers, "AES*,RC4*,DES*"); i->tls_options->certificate_expiry_notification = 1; + i->server_linking_tls_options = NULL; /* set::server-linking::tls-options, not configured by default */ + i->server_linking_mixed_certificates = 0; + i->server_linking_allow_ca_certificate = 0; i->plaintext_policy_user = POLICY_ALLOW; i->plaintext_policy_oper = POLICY_DENY; i->plaintext_policy_server = POLICY_DENY; @@ -1934,8 +1940,8 @@ void config_setdefaultsettings(Configuration *i) safe_strdup(i->reject_message_too_many_new_connections_ipv6_range, "Too many new connections from this IPv6 range ($prefix_addr/$prefix_len) [connthrottle]"); safe_strdup(i->reject_message_server_full, "This server is full"); safe_strdup(i->reject_message_unauthorized, "You are not authorized to connect to this server"); - safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information."); - safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information."); + safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information. $banid"); + safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information. $banid"); i->topic_setter = SETTER_NICK_USER_HOST; i->ban_setter = SETTER_NICK_USER_HOST; @@ -2101,6 +2107,8 @@ void postconf(void) log_data_integer("port", bestpractices.listen_nontls_port)); bestpractices.listen_nontls_port_hits++; } + + RunHook(HOOKTYPE_POSTCONF); } int isanyserverlinked(void) @@ -2558,41 +2566,6 @@ int config_read_file(const char *filename, const char *display_name) } } -/** Remove all TKL's that were added by the config file(s). - * This is done after config passed testing and right before - * adding the (new) entries. - */ -void remove_config_tkls(int flag) -{ - TKL *tk, *tk_next; - int index, index2; - - /* IP hashed TKL list */ - for (index = 0; index < TKLIPHASHLEN1; index++) - { - for (index2 = 0; index2 < TKLIPHASHLEN2; index2++) - { - for (tk = tklines_ip_hash[index][index2]; tk; tk = tk_next) - { - tk_next = tk->next; - if (tk->flags & flag) - tkl_del_line(tk); - } - } - } - - /* Generic TKL list */ - for (index = 0; index < TKLISTLEN; index++) - { - for (tk = tklines[index]; tk; tk = tk_next) - { - tk_next = tk->next; - if (tk->flags & flag) - tkl_del_line(tk); - } - } -} - void free_proxy_block(ConfigItem_proxy *e) { free_security_group(e->mask); @@ -2758,8 +2731,6 @@ void config_rehash() safe_free(tld_ptr); } - remove_config_tkls(TKL_FLAG_CONFIG); - for (deny_version_ptr = conf_deny_version; deny_version_ptr; deny_version_ptr = (ConfigItem_deny_version *) next) { next = (ListStruct *)deny_version_ptr->next; safe_free(deny_version_ptr->mask); @@ -2966,6 +2937,8 @@ int config_item_allowed_for_config_file(const char *resource, const char *item) /* Special hardcoded handling for central spamfilter */ if (!strcmp(resource, "central_spamfilter.conf")) { + if (!item) + return 0; if (!strcmp(item, "spamfilter") || !strcmp(item, "ban")) return 1; @@ -2982,7 +2955,7 @@ int config_item_allowed_for_config_file(const char *resource, const char *item) if (rs->restrict_config == NULL) return 1; /* No restrictions */ - if (item == NULL) + if (!item) return 0; if (find_name_list(rs->restrict_config, item)) @@ -3199,6 +3172,15 @@ int config_run_blocks_generic(ConfigFile *cfptr, int skip_priority_blocks) return processed; } +/** Remember the set::server-linking::tls-options block so core can work on it + * after all the other set blocks have been processed (ordering issue). + * This is a bit of a hack, but... yeah... + */ +void set_server_linking_tlsoptions_ce(ConfigEntry *ce) +{ + server_linking_tlsoptions_ce = ce; +} + int config_run_blocks(void) { ConfigEntry *ce; @@ -3209,6 +3191,8 @@ int config_run_blocks(void) Hook *h; ConfigItem_allow *allow; + server_linking_tlsoptions_ce = NULL; /* processed late, see the set:: handling below */ + /* Stage 1: first the priority blocks, in the order as specified * in config_run_priority_blocks[] */ @@ -3258,6 +3242,23 @@ int config_run_blocks(void) snprintf(tmp, sizeof(tmp), "%s/tls/server.key.pem", CONFDIR); add_name_list(tempiConf.tls_options->key_files, tmp); } + + /* And NOW that set::tls is fully done (including the default + * certificate above), process set::server-linking::tls-options. + * We do this here and not at parse time in _conf_set(), because + * this could inherit from set::tls, so we have to do it in this + * particular order. + */ + if (server_linking_tlsoptions_ce) + { + tempiConf.server_linking_tls_options = safe_alloc(sizeof(TLSOptions)); + conf_tlsblock(conf, server_linking_tlsoptions_ce, tempiConf.server_linking_tls_options, tempiConf.tls_options); + server_linking_tlsoptions_ce = NULL; + /* ctx_link_server and ctx_link_client is + * created/updated by init_tls() and reinit_tls(). Those + * also compute the cached spkifp. + */ + } } } @@ -3604,6 +3605,24 @@ void convert_to_absolute_path(char **path, const char *reldir) *path = s; } +/** Return 'path' relative to 'reldir' if it lives below it, otherwise return + * 'path' unchanged (absolute path, URL, etc). + * This can be used to turn a path into relative again if convert_to_absolute_path() + * previously worked on it. + * @returns a pointer into 'path' (read-only). + */ +const char *display_path(const char *path, const char *reldir) +{ + size_t len; + + if (!path || !reldir) + return path; + len = strlen(reldir); + if (!strncmp(path, reldir, len) && (path[len] == '/' || path[len] == '\\')) + return path + len + 1; + return path; +} + /* Similar to convert_to_absolute_path() but returns a duplicated string. * Don't forget to free! */ @@ -5596,9 +5615,15 @@ void conf_listen_configure(const char *ip, int port, SocketType socket_type, int if (tlsconfig) { listen->tls_options = safe_alloc(sizeof(TLSOptions)); - conf_tlsblock(conf, tlsconfig, listen->tls_options); + conf_tlsblock(conf, tlsconfig, listen->tls_options, + ((options & LISTENER_SERVERSONLY) && tempiConf.server_linking_tls_options) ? + tempiConf.server_linking_tls_options : tempiConf.tls_options); listen->ssl_ctx = init_ctx(listen->tls_options, 1); } + /* A serversonly listener with no tls-options of its own uses the shared + * set::server-linking context at runtime (tls_ctx_for_listener()), so we + * deliberately do NOT build a per-listener context here. + */ /* For modules that hook CONFIG_LISTEN and CONFIG_LISTEN_OPTIONS. * Yeah, ugly we have this here.. @@ -6418,13 +6443,22 @@ int _test_allow_channel(ConfigFile *conf, ConfigEntry *ce) for (cep = ce->items; cep; cep = cep->next) { - if (config_is_blankorempty(cep, "allow channel")) + 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 if (config_is_blankorempty(cep, "allow channel")) { errors++; continue; } - - if (!strcmp(cep->name, "channel")) + else if (!strcmp(cep->name, "channel")) { has_channel = 1; } @@ -6439,16 +6473,6 @@ 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 { config_error_unknown(cep->file->filename, cep->line_number, @@ -6601,7 +6625,7 @@ int _conf_sni(ConfigFile *conf, ConfigEntry *ce) sni = safe_alloc(sizeof(ConfigItem_listen)); safe_strdup(sni->name, name); sni->tls_options = safe_alloc(sizeof(TLSOptions)); - conf_tlsblock(conf, tlsconfig, sni->tls_options); + conf_tlsblock(conf, tlsconfig, sni->tls_options, tempiConf.tls_options); sni->ssl_ctx = init_ctx(sni->tls_options, 1); AddListItem(sni, conf_sni); @@ -6703,7 +6727,9 @@ int _conf_link(ConfigFile *conf, ConfigEntry *ce) else if (!strcmp(cepp->name, "ssl-options") || !strcmp(cepp->name, "tls-options")) { link->tls_options = safe_alloc(sizeof(TLSOptions)); - conf_tlsblock(conf, cepp, link->tls_options); + conf_tlsblock(conf, cepp, link->tls_options, + tempiConf.server_linking_tls_options ? + tempiConf.server_linking_tls_options : tempiConf.tls_options); link->ssl_ctx = init_ctx(link->tls_options, 0); } } @@ -6749,6 +6775,11 @@ int _conf_link(ConfigFile *conf, ConfigEntry *ce) if (!link->hub && !link->leaf) safe_strdup(link->hub, "*"); + /* An outgoing link with no link::outgoing::tls-options of its own uses the + * shared set::server-linking context at connect time + * (tls_ctx_for_outgoing_link()), so we do NOT build a per-link context here. + */ + AppendListItem(link, conf_link); return 0; } @@ -7648,7 +7679,7 @@ void test_tlsblock(ConfigFile *conf, ConfigEntry *cep, int *totalerrors) TLSOptions *tlsoptions = safe_alloc(sizeof(TLSOptions)); SSL_CTX *ctx; - conf_tlsblock(conf, cep, tlsoptions); + conf_tlsblock(conf, cep, tlsoptions, tempiConf.tls_options); ctx = init_ctx(tlsoptions, 1); free_tls_options(tlsoptions); @@ -7668,6 +7699,7 @@ void free_tls_options(TLSOptions *tlsoptions) safe_free_name_list(tlsoptions->certificate_files); safe_free_name_list(tlsoptions->key_files); + safe_free_name_list(tlsoptions->spkifp); safe_free(tlsoptions->trusted_ca_file); safe_free(tlsoptions->ciphers); safe_free(tlsoptions->ciphersuites); @@ -7679,31 +7711,34 @@ void free_tls_options(TLSOptions *tlsoptions) safe_free(tlsoptions); } -void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions) +void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions, TLSOptions *inherit_from) { ConfigEntry *cepp, *ceppp; NameValue *ofl; - /* First, inherit settings from set::options::tls */ - if (tlsoptions != tempiConf.tls_options) + /* First, inherit settings from the base TLS options (inherit_from). + * This is either set::tls (the usual case), or for link blocks and + * serversonly listeners it's set::server-linking::tls-options. + */ + if (tlsoptions != inherit_from) { // certificate_files: done at end of function // key_files: done at end of function - safe_strdup(tlsoptions->trusted_ca_file, tempiConf.tls_options->trusted_ca_file); - tlsoptions->protocols = tempiConf.tls_options->protocols; - safe_strdup(tlsoptions->ciphers, tempiConf.tls_options->ciphers); - safe_strdup(tlsoptions->ciphersuites, tempiConf.tls_options->ciphersuites); - safe_strdup(tlsoptions->groups, tempiConf.tls_options->groups); - safe_strdup(tlsoptions->signature_algorithms, tempiConf.tls_options->signature_algorithms); - safe_strdup(tlsoptions->outdated_protocols, tempiConf.tls_options->outdated_protocols); - safe_strdup(tlsoptions->outdated_ciphers, tempiConf.tls_options->outdated_ciphers); - tlsoptions->options = tempiConf.tls_options->options; - tlsoptions->renegotiate_bytes = tempiConf.tls_options->renegotiate_bytes; - tlsoptions->renegotiate_timeout = tempiConf.tls_options->renegotiate_timeout; - tlsoptions->sts_port = tempiConf.tls_options->sts_port; - tlsoptions->sts_duration = tempiConf.tls_options->sts_duration; - tlsoptions->sts_preload = tempiConf.tls_options->sts_preload; - tlsoptions->certificate_expiry_notification = tempiConf.tls_options->certificate_expiry_notification; + safe_strdup(tlsoptions->trusted_ca_file, inherit_from->trusted_ca_file); + tlsoptions->protocols = inherit_from->protocols; + safe_strdup(tlsoptions->ciphers, inherit_from->ciphers); + safe_strdup(tlsoptions->ciphersuites, inherit_from->ciphersuites); + safe_strdup(tlsoptions->groups, inherit_from->groups); + safe_strdup(tlsoptions->signature_algorithms, inherit_from->signature_algorithms); + safe_strdup(tlsoptions->outdated_protocols, inherit_from->outdated_protocols); + safe_strdup(tlsoptions->outdated_ciphers, inherit_from->outdated_ciphers); + tlsoptions->options = inherit_from->options; + tlsoptions->renegotiate_bytes = inherit_from->renegotiate_bytes; + tlsoptions->renegotiate_timeout = inherit_from->renegotiate_timeout; + tlsoptions->sts_port = inherit_from->sts_port; + tlsoptions->sts_duration = inherit_from->sts_duration; + tlsoptions->sts_preload = inherit_from->sts_preload; + tlsoptions->certificate_expiry_notification = inherit_from->certificate_expiry_notification; } /* Now process the options */ @@ -7769,12 +7804,12 @@ void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions) else if (!strcmp(cepp->name, "certificate")) { convert_to_absolute_path(&cepp->value, CONFDIR); - add_name_list(tlsoptions->certificate_files, cepp->value); + append_name_list(tlsoptions->certificate_files, cepp->value); } else if (!strcmp(cepp->name, "key")) { convert_to_absolute_path(&cepp->value, CONFDIR); - add_name_list(tlsoptions->key_files, cepp->value); + append_name_list(tlsoptions->key_files, cepp->value); } else if (!strcmp(cepp->name, "trusted-ca-file")) { @@ -7835,12 +7870,12 @@ void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions) * additional certs/keys due to the nature of it being a name list. * So we simply only add these here at the end if they were not set. */ - if (tlsoptions != tempiConf.tls_options) + if (tlsoptions != inherit_from) { if (!tlsoptions->certificate_files) - tlsoptions->certificate_files = duplicate_name_list(tempiConf.tls_options->certificate_files); + tlsoptions->certificate_files = duplicate_name_list(inherit_from->certificate_files); if (!tlsoptions->key_files) - tlsoptions->key_files = duplicate_name_list(tempiConf.tls_options->key_files); + tlsoptions->key_files = duplicate_name_list(inherit_from->key_files); } } @@ -8347,7 +8382,7 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce) } else if (!strcmp(cep->name, "ssl") || !strcmp(cep->name, "tls")) { /* no need to alloc tempiConf.tls_options since config_defaults() already ensures it exists */ - conf_tlsblock(conf, cep, tempiConf.tls_options); + conf_tlsblock(conf, cep, tempiConf.tls_options, tempiConf.tls_options); } else if (!strcmp(cep->name, "plaintext-policy")) { @@ -9188,8 +9223,9 @@ int _test_set(ConfigFile *conf, ConfigEntry *ce) } else if (!strcmp(ceppp->name, "away-count")) { - int temp = atol(ceppp->value); + int temp; CheckNull(ceppp); + temp = atol(ceppp->value); if (temp < 1 || temp > 255) { config_error("%s:%i: set::anti-flood::away-count must be between 1 and 255", @@ -10623,7 +10659,7 @@ int _test_alias(ConfigFile *conf, ConfigEntry *ce) { continue; } if (!strcmp(cep->name, "format")) { - char *err = NULL; + const char *err = NULL; Match *expr; char has_type = 0, has_target = 0, has_parameters = 0; @@ -10873,12 +10909,22 @@ int _test_deny(ConfigFile *conf, ConfigEntry *ce) char has_mask = 0, has_match = 0; for (cep = ce->items; cep; cep = cep->next) { - if (config_is_blankorempty(cep, "deny channel")) + 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 if (config_is_blankorempty(cep, "deny channel")) { errors++; continue; } - if (!strcmp(cep->name, "channel")) + else if (!strcmp(cep->name, "channel")) { if (has_channel) { @@ -10928,16 +10974,6 @@ 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 { config_error_unknown(cep->file->filename, @@ -11889,29 +11925,14 @@ int reloadable_perm_module_unloaded(void) return ret; } -const char *link_generator_spkifp(TLSOptions *tlsoptions) -{ - SSL_CTX *ctx; - SSL *ssl; - X509 *cert; - - ctx = init_ctx(tlsoptions, 1); - if (!ctx) - exit(1); - ssl = SSL_new(ctx); - if (!ssl) - exit(1); - cert = SSL_get_certificate(ssl); - return spki_fingerprint_ex(cert); -} - void link_generator(void) { ConfigItem_listen *lstn; - TLSOptions *tlsopt = iConf.tls_options; /* never null */ + TLSOptions *tlsopt = iConf.server_linking_tls_options ? iConf.server_linking_tls_options : iConf.tls_options; /* set::server-linking::tls-options and otherwise set::tls */ int port = 0; char *ip = NULL; - const char *spkifp; + SSL_CTX *ctx; + NameList *fp; for (lstn = conf_listen; lstn; lstn = lstn->next) { @@ -11936,8 +11957,9 @@ void link_generator(void) exit(1); } - spkifp = link_generator_spkifp(tlsopt); - if (!spkifp) + /* init_ctx() will compute the spkifp(s) */ + ctx = init_ctx(tlsopt, 1); + if (!ctx || !tlsopt->spkifp) { printf("Could not calculate spkifp. Maybe you have uncommon TLS options set? Odd...\n"); exit(1); @@ -11955,14 +11977,18 @@ void link_generator(void) " hostname %s;\n" " port %d;\n" " options { tls; autoconnect; }\n" - " }\n" - " password \"%s\" { spkifp; }\n" - " class servers;\n" - "}\n", + " }\n", conf_me->name, ip ? ip : conf_me->name, - port, - spkifp); + port); + /* The simple case is a single password ".." { spkifp; } line, but we also + * have to deal with the case of multiple certificate/keys with multiple + * password..spkifp lines. Like for ECC+RSA or ECC+ML-DSA. + */ + for (fp = tlsopt->spkifp; fp; fp = fp->next) + printf(" password \"%s\" { spkifp; }\n", fp->name); + printf(" class servers;\n" + "}\n"); printf("################################################################################\n"); exit(0); } @@ -12309,6 +12335,10 @@ void central_spamfilter_download_complete(OutgoingWebRequest *request, OutgoingW /* And load the new ones... */ num_rules = config_run_blocks_generic(cfptr, 0); + /* Restore hit counters onto the freshly re-added central spamfilters (matched + * by key), from the snapshot taken in remove_config_tkls() just above. + */ + config_tkl_hits_restore(); active_rules = count_central_spamfilter_rules(); if (iConf.central_spamfilter_verbose > 2) diff --git a/src/ircd.c b/src/ircd.c index 9dd5510ee5..1475403586 100644 --- a/src/ircd.c +++ b/src/ircd.c @@ -547,6 +547,7 @@ int InitUnrealIRCd(int argc, char *argv[]) init_hash(); log_throttle_init(); + init_match(); SetupEvents(); diff --git a/src/json.c b/src/json.c index 6a1c5830e5..257c8f2e8b 100644 --- a/src/json.c +++ b/src/json.c @@ -206,6 +206,7 @@ void json_expand_client_security_groups(json_t *parent, Client *client) * detail=2: everything, except 'channels' * detail=3: everything, with 'channels' being a max 384 character string (meant for JSON logging only) * detail=4: everything, with 'channels' object (full). + * detail=5: this also adds "flood" counters */ void json_expand_client(json_t *j, const char *key, Client *client, int detail) { @@ -300,6 +301,12 @@ void json_expand_client(json_t *j, const char *key, Client *client, int detail) if (client->local && client->local->idle_since) json_object_set_new(child, "idle_since", json_timestamp(client->local->idle_since)); + /* Flood counters: only at level 5+, so not in JSON-RPC default user.get (level 4) + * or in JSON logs (level 3). + */ + if (detail >= 5) + json_expand_flood_counts(child, "flood", client); + if (client->user) { char buf[512]; @@ -569,6 +576,8 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail) json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline' json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line' json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by)); + if (tkl->id[0]) + json_object_set_new(j, "id", json_string_unreal(tkl->id)); json_object_set_new(j, "set_at", json_timestamp(tkl->set_at)); json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at)); *buf = '\0'; @@ -596,11 +605,17 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail) else json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0))); json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason)); + if (tkl->spamfilter_id[0]) + json_object_set_new(j, "spamfilter_id", json_string_unreal(tkl->spamfilter_id)); + json_object_set_new(j, "hits", json_integer(tkl->hits)); + json_object_set_new(j, "last_hit_at", json_timestamp(tkl->lasthit)); } else if (TKLIsNameBan(tkl)) { json_object_set_new(j, "name", json_string_unreal(tkl->ptr.nameban->name)); json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.nameban->reason)); + json_object_set_new(j, "hits", json_integer(tkl->hits)); + json_object_set_new(j, "last_hit_at", json_timestamp(tkl->lasthit)); } else if (TKLIsBanException(tkl)) { @@ -627,8 +642,10 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail) json_object_set_new(j, "ban_duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->ptr.spamfilter->tkl_duration))); json_object_set_new(j, "spamfilter_targets", json_string_unreal(spamfilter_target_inttostring(tkl->ptr.spamfilter->target))); json_object_set_new(j, "reason", json_string_unreal(unreal_decodespace(tkl->ptr.spamfilter->tkl_reason))); - json_object_set_new(j, "hits", json_integer(tkl->ptr.spamfilter->hits)); + json_object_set_new(j, "hits", json_integer(tkl->hits)); + json_object_set_new(j, "last_hit_at", json_timestamp(tkl->lasthit)); json_object_set_new(j, "hits_except", json_integer(tkl->ptr.spamfilter->hits_except)); + json_object_set_new(j, "last_hit_except_at", json_timestamp(tkl->ptr.spamfilter->lasthit_except)); } } @@ -661,6 +678,50 @@ void json_expand_textanalysis(json_t *root, const char *key, TextAnalysis *ta, i } } +/** Add a "flood" object to JSON: how often this client hit flood limits this session. + * Under it are two parts: + * "server" server flood limits (set::anti-flood): nick, away, join, etc. + * "channel" channel flood protection (+f/+F), totals over the channels the user is in. + * If the client hit no flood limits, no flood object is added. + * + * @param root The parent JSON object + * @param key Key to put the flood object in, or NULL to write under root directly. + * @param client The client (we only produce output for local clients) + */ +void json_expand_flood_counts(json_t *root, const char *key, Client *client) +{ + json_t *flood, *server; + int i; + + if (!client->local) + return; /* counters are per-connection, local clients only */ + + flood = key ? json_object() : root; + + /* server-level flood limits (set::anti-flood) */ + server = json_object(); + for (i = 0; i < MAXFLOODOPTIONS; i++) + { + if (floodoption_shortnames[i] && (client->local->flood[i].blocked > 0)) + json_object_set_new(server, floodoption_shortnames[i], json_integer(client->local->flood[i].blocked)); + } + if (json_object_size(server) > 0) + json_object_set_new(flood, "server", server); + else + json_decref(server); + + /* channel-level flood protection (+f/+F), via floodprot; adds a "channel" object if nonzero */ + channel_flood_expand_json(flood, client); + + if (key) + { + if (json_object_size(flood) > 0) + json_object_set_new(root, key, flood); + else + json_decref(flood); + } +} + /** Expand a ConfigItem_mask list to a JSON array. * @param parent The parent JSON object * @param key The key name for the array diff --git a/src/list.c b/src/list.c index 60a03ea557..5e735a11cb 100644 --- a/src/list.c +++ b/src/list.c @@ -563,6 +563,13 @@ void _add_name_list(NameList **list, const char *name) AddListItem(e, *list); } +void _append_name_list(NameList **list, const char *name) +{ + NameList *e = safe_alloc(sizeof(NameList)+strlen(name)+1); + strcpy(e->name, name); /* safe, allocated above */ + AppendListItem(e, *list); +} + void _free_entire_name_list(NameList *n) { NameList *n_next; @@ -576,13 +583,20 @@ void _free_entire_name_list(NameList *n) NameList *duplicate_name_list(NameList *e) { - NameList *ret = NULL; - - if (e == NULL) - return NULL; + NameList *ret = NULL, *tail = NULL, *n; + /* We do manual pointer tracking here to speed things up */ for (; e; e = e->next) - add_name_list(ret, e->name); + { + n = safe_alloc(sizeof(NameList)+strlen(e->name)+1); + strcpy(n->name, e->name); /* safe, allocated above */ + if (tail) + tail->next = n; + else + ret = n; + n->prev = tail; + tail = n; + } return ret; } diff --git a/src/log.c b/src/log.c index e286add197..481aaf1bf6 100644 --- a/src/log.c +++ b/src/log.c @@ -587,6 +587,39 @@ LogData *log_data_string(const char *key, const char *str) return d; } +/** Like log_data_string(), the JSON is identical, but in buildlogstring() + * if the value is empty the string will be empty plus one preceding space + * character is "eaten". This way you can write things like + * "blablabla. $optionalstring" and both the space and the $optionalstring + * will not appear in the output. + */ +LogData *log_data_optional_string(const char *key, const char *value) +{ + LogData *d = safe_alloc(sizeof(LogData)); + + d->type = LOG_FIELD_OPTIONAL_STRING; + safe_strdup(d->key, key); + safe_strdup(d->value.string, value); + return d; +} + +/** Optional "[label: value]" field. In the JSON the value is stored raw, exactly + * like log_data_string() (so consumers get a clean value, not display text); in + * the text buildlogstring() shows it as "[label: value]". If the value is empty + * the whole thing is left out of the text and one preceding space is consumed, so + * "blablabla. $field" collapses cleanly when $field is empty. + */ +LogData *log_data_optional_name_value(const char *key, const char *label, const char *value) +{ + LogData *d = safe_alloc(sizeof(LogData)); + + d->type = LOG_FIELD_OPTIONAL_NAME_VALUE; + safe_strdup(d->key, key); + safe_strdup(d->label, label); + safe_strdup(d->value.string, value); + return d; +} + LogData *log_data_char(const char *key, const char c) { LogData *d = safe_alloc(sizeof(LogData)); @@ -783,11 +816,13 @@ LogData *log_data_textanalysis(const char *key, TextAnalysis *ta) void log_data_free(LogData *d) { - if (d->type == LOG_FIELD_STRING) + if ((d->type == LOG_FIELD_STRING) || (d->type == LOG_FIELD_OPTIONAL_NAME_VALUE) || + (d->type == LOG_FIELD_OPTIONAL_STRING)) safe_free(d->value.string); else if (((d->type == LOG_FIELD_OBJECT) || (d->type == LOG_FIELD_OBJECT_NOFREE)) && d->value.object) json_decref(d->value.object); + safe_free(d->label); safe_free(d->key); safe_free(d); } @@ -899,14 +934,26 @@ int valid_subsystem(const char *s) * @param name Array of variables names * @param value Array of variable values */ -void buildlogstring(const char *inbuf, char *outbuf, size_t len, json_t *details) +/* Eat one space immediately before the current output position, if any. + * Used when an optional log field outputs nothing. */ +static void buildlogstring_eat_space(char **o, char *outbuf, int *left) +{ + if ((*o > outbuf) && ((*o)[-1] == ' ')) + { + (*o)--; + (*left)++; + } +} + +void buildlogstring(const char *inbuf, char *outbuf, size_t len, json_t *details, json_t *optional_keys) { const char *i, *p; char *o; int left = len - 1; int cnt, found; char varname[256], *varp, *varpp; - json_t *t; + char buf[BUFSIZE]; + json_t *t, *opt; #ifdef DEBUGMODE if (len <= 0) @@ -973,13 +1020,30 @@ void buildlogstring(const char *inbuf, char *outbuf, size_t len, json_t *details { output = json_get_value(t); } - if (output) + /* optional_keys maps each optional field's key to its label (a + * json string) for log_data_optional_name_value, or to json null + * for log_data_optional_string. A non-NULL lookup means "optional". + */ + opt = json_object_get(optional_keys, varname); + if (output && *output) { + if (opt && json_is_string(opt)) + { + /* name:value field: show it as "[label: value]" (the JSON keeps the raw value) */ + ircsnprintf(buf, sizeof(buf), "[%s: %s]", json_string_value(opt), output); + output = buf; + } strlcpy(o, output, left); left -= strlen(output); /* may become <0 */ if (left <= 0) return; /* return - don't write \0 to 'o'. ensured by strlcpy already */ o += strlen(output); /* value entirely written */ + } else + if (opt) + { + /* Optional field that is empty: show nothing and eat a + * preceding space. */ + buildlogstring_eat_space(&o, outbuf, &left); } } else { @@ -1801,6 +1865,7 @@ void do_unreal_log_internal(LogLevel loglevel, const char *subsystem, const char const char *str; json_t *j = NULL; json_t *j_details = NULL; + json_t *optional_keys = NULL; json_t *t; char msgbuf[MAXLOGLENGTH]; const char *loglevel_string = log_level_valtostring(loglevel); @@ -1842,6 +1907,7 @@ void do_unreal_log_internal(LogLevel loglevel, const char *subsystem, const char j = json_object(); j_details = json_object(); + optional_keys = json_object(); json_object_set_new(j, "timestamp", json_string_unreal(timestamp_iso8601_now())); json_object_set_new(j, "level", json_string_unreal(loglevel_string)); @@ -1869,6 +1935,27 @@ void do_unreal_log_internal(LogLevel loglevel, const char *subsystem, const char else json_object_set_new(j_details, d->key, json_null()); break; + case LOG_FIELD_OPTIONAL_NAME_VALUE: + /* Optional: raw value in the JSON (or null), exactly like + * LOG_FIELD_STRING; buildlogstring() shows it as "[label: value]" + * in the text and eats a preceding space when empty. The label is + * stored as this key's value in optional_keys. */ + json_object_set_new(optional_keys, d->key, json_string_unreal(d->label)); + if (d->value.string) + json_object_set_new(j_details, d->key, json_string_unreal(d->value.string)); + else + json_object_set_new(j_details, d->key, json_null()); + break; + case LOG_FIELD_OPTIONAL_STRING: + /* Same JSON as LOG_FIELD_STRING (value as-is, null only when + * NULL); the optional_keys entry (value null = no label) is what + * makes buildlogstring() eat a preceding space when empty. */ + json_object_set_new(optional_keys, d->key, json_null()); + if (d->value.string) + json_object_set_new(j_details, d->key, json_string_unreal(d->value.string)); + else + json_object_set_new(j_details, d->key, json_null()); + break; case LOG_FIELD_CLIENT: json_expand_client(j_details, d->key, d->value.client, 3); break; @@ -1897,9 +1984,10 @@ void do_unreal_log_internal(LogLevel loglevel, const char *subsystem, const char } if (expand_msg) - buildlogstring(msg, msgbuf, sizeof(msgbuf), j_details); + buildlogstring(msg, msgbuf, sizeof(msgbuf), j_details, optional_keys); else strlcpy(msgbuf, msg, sizeof(msgbuf)); + json_decref(optional_keys); json_object_set_new(j, "msg", json_string_unreal(msgbuf)); diff --git a/src/match.c b/src/match.c index 7b6e164405..2c27e58f9f 100644 --- a/src/match.c +++ b/src/match.c @@ -35,6 +35,8 @@ u_char touppertab[], tolowertab[]; #define tolowertab2 tolowertab #define lc(x) tolowertab2[x] +pcre2_match_context *unreal_pcre2_match_ctx = NULL; + /* Match routine for special cases where escaping is needed in a normal fashion. * Checks a string ('name') against a globbing(+more) pattern ('mask'). * Original by Douglas A Lewis (dalewis@acsu.buffalo.edu). @@ -369,6 +371,17 @@ u_char char_atribs[] = { /* f0-ff */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +/* Set up global match state. Called once at startup. */ +void init_match(void) +{ + unreal_pcre2_match_ctx = pcre2_match_context_create(NULL); + if (unreal_pcre2_match_ctx) + { + pcre2_set_match_limit(unreal_pcre2_match_ctx, UNREAL_PCRE2_MATCH_LIMIT); + pcre2_set_depth_limit(unreal_pcre2_match_ctx, UNREAL_PCRE2_DEPTH_LIMIT); + } +} + /** Free up all resources of an Match entry (including the struct itself). * NOTE: this function may (also) be called for Match structs that have only been * setup half-way, so use special care when accessing members (NULL checks!) @@ -384,7 +397,7 @@ void unreal_delete_match(Match *m) safe_free(m); } -Match *unreal_create_match(MatchType type, const char *str, char **error) +Match *unreal_create_match(MatchType type, const char *str, const char **error) { Match *m = safe_alloc(sizeof(Match)); static char errorbuf[512]; @@ -441,28 +454,46 @@ Match *unreal_create_match(MatchType type, const char *str, char **error) } /** Try to match an Match entry ('m') against a string ('str'). + * @param error If non-NULL, set to an error string when the regex could not + * complete (eg. a resource limit was hit), or to NULL otherwise. + * Points to a static buffer, valid until the next unreal_match(). * @returns 1 if matched, 0 if not. * @note These (more logical) return values are opposite to the match_simple() function. */ -int unreal_match(Match *m, const char *str) +int unreal_match(Match *m, const char *str, const char **error) { + static char errbuf[256]; + + if (error) + *error = NULL; + if (m->type == MATCH_SIMPLE) { if (match_simple(m->str, str)) return 1; return 0; } - + if (m->type == MATCH_PCRE_REGEX) { pcre2_match_data *md = pcre2_match_data_create(9, NULL); int ret; - - ret = pcre2_match(m->ext.pcre2_expr, str, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); /* run the regex */ + + ret = pcre2_match(m->ext.pcre2_expr, str, PCRE2_ZERO_TERMINATED, 0, 0, md, unreal_pcre2_match_ctx); /* run the regex */ pcre2_match_data_free(md); /* yeah, we never use it. unfortunately argument must be non-NULL for pcre2_match() */ - + if (ret > 0) - return 1; /* MATCH */ + return 1; /* MATCH */ + + if (error && (ret < 0) && (ret != PCRE2_ERROR_NOMATCH) && (ret != PCRE2_ERROR_PARTIAL)) + { + /* Regex did not finish (eg. hit the match, depth or JIT stack limit). + * Report it so the caller can warn. We still return no-match. + */ + *errbuf = '\0'; + pcre2_get_error_message(ret, errbuf, sizeof(errbuf)); + *error = errbuf; + } return 0; /* NO MATCH */ } @@ -681,7 +712,7 @@ const char *stripbadwords(const char *str, ConfigItem_badword *start_bw, int *bl pcre2_match_data *md = pcre2_match_data_create(9, NULL); int ret; - ret = pcre2_match(this_word->pcre2_expr, cleanstr, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); /* run the regex */ + ret = pcre2_match(this_word->pcre2_expr, cleanstr, PCRE2_ZERO_TERMINATED, 0, 0, md, unreal_pcre2_match_ctx); /* run the regex */ pcre2_match_data_free(md); /* yeah, we never use it. unfortunately argument must be non-NULL for pcre2_match() */ if (ret > 0) { @@ -702,7 +733,7 @@ const char *stripbadwords(const char *str, ConfigItem_badword *start_bw, int *bl /* ^^ we need to free 'md' in ALL circumstances. * remember this if you break or continue in this loop! */ - ret = pcre2_match(this_word->pcre2_expr, ptr, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); /* run the regex */ + ret = pcre2_match(this_word->pcre2_expr, ptr, PCRE2_ZERO_TERMINATED, 0, 0, md, unreal_pcre2_match_ctx); /* run the regex */ if (ret > 0) { dd = pcre2_get_ovector_pointer(md); diff --git a/src/misc.c b/src/misc.c index a6c93585fa..8481a58453 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1361,7 +1361,7 @@ MessageTag *duplicate_mtags(MessageTag *mtags) /** Duplicate a message tag list, excluding tags with MTAG_HANDLER_FLAGS_FIRST_ONLY. * Used to build the tag set for subsequent lines (lines 2..N), - * where tags like msgid and +draft/reply should not be repeated. + * where tags like msgid, +reply and +draft/reply should not be repeated. */ MessageTag *duplicate_mtags_for_subsequent_lines(MessageTag *mtags) { @@ -1514,6 +1514,15 @@ int floodprot_check_multiline_batch_default_handler(Channel *channel, Client *cl return 0; } +int channel_flood_blocked_count_default_handler(Client *client, const char *type) +{ + return 0; +} + +void channel_flood_expand_json_default_handler(json_t *root, Client *client) +{ +} + 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, const char *autojoin_channels) @@ -2967,7 +2976,7 @@ const char *command_issued_by_rpc(MessageTag *mtags) /** Is 's' a valid spamfilter id? A-Z, 0-9 and _ and with a max length. */ int valid_spamfilter_id(const char *s) { - if (strlen(s) > MAXSPAMFILTERIDLEN) + if (strlen(s) >= TKLIDLEN) return 0; return 1; } diff --git a/src/modules/cap.c b/src/modules/cap.c index dbde4314cf..66f75c8b29 100644 --- a/src/modules/cap.c +++ b/src/modules/cap.c @@ -165,6 +165,9 @@ static void clicap_generate(Client *client, const char *subcmd, int flags) if (cap->visible && !cap->visible(client)) continue; /* hidden */ + if (cap->minimum_cap_version && (client->local->cap_protocol < cap->minimum_cap_version)) + continue; /* skip: doesn't meet minimum CAP version */ + if (flags) { if (!cap->cap || !(client->local->caps & cap->cap)) diff --git a/src/modules/central-blocklist.c b/src/modules/central-blocklist.c index 54c0a79ae3..3cf12b84e2 100644 --- a/src/modules/central-blocklist.c +++ b/src/modules/central-blocklist.c @@ -1131,7 +1131,7 @@ CMD_OVERRIDE_FUNC(cbl_override_spamreport_gather) int _central_spamreport(Client *client, Client *by, const char *url) { - json_t *j, *requests, *data, *cmds, *item; + json_t *j, *requests, *data, *cmds, *item, *clientobj; OutgoingWebRequest *w; NameValuePrioList *headers = NULL; int num; @@ -1166,6 +1166,13 @@ int _central_spamreport(Client *client, Client *by, const char *url) data = json_deep_copy(CBL(client)->handshake); /* .. deep copy. */ json_object_set_new(requests, client->id, data); /* ..and steal reference */ + + /* We add the flood counters here explicitly, because when "client" was created + * earlier there were no or limited flood counts yet (during registration phase). + */ + clientobj = json_object_get(data, "client"); + json_expand_flood_counts(clientobj ? clientobj : data, "flood", client); + cmds = json_object(); json_object_set_new(data, "commands", cmds); start = CBL(client)->last_cmds_slot; diff --git a/src/modules/chanmodes/floodprot.c b/src/modules/chanmodes/floodprot.c index 4ff96b6154..75e960354c 100644 --- a/src/modules/chanmodes/floodprot.c +++ b/src/modules/chanmodes/floodprot.c @@ -125,6 +125,24 @@ struct ChannelFloodProfile { /* Global variables */ ModDataInfo *mdflood = NULL; + +/* Per-local-client tally of how many times the user was blocked by channel flood + * protection (+f/+F), per flood type. Read via the channel_flood_blocked_count efunc + * (eg the total_channel_flood_count() crule). Local-only, freed on disconnect. + */ +typedef struct ChannelFloodBlocks { int blocked[NUMFLD]; } ChannelFloodBlocks; +ModDataInfo *md_channelflood_blocked = NULL; +/* Friendly type names for the efunc/crule, indexed by enum Flood. KEEP IN SYNC. */ +static const char *channelfloodtype_names[NUMFLD] = { + "ctcp", /* CHFLD_CTCP */ + "join", /* CHFLD_JOIN */ + "knock", /* CHFLD_KNOCK */ + "msg", /* CHFLD_MSG */ + "nick", /* CHFLD_NICK */ + "text", /* CHFLD_TEXT */ + "repeat", /* CHFLD_REPEAT */ + "paste" /* CHFLD_PASTE */ +}; Cmode_t EXTMODE_FLOODLIMIT = 0L; Cmode_t EXTMODE_FLOOD_PROFILE = 0L; 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. */ @@ -185,6 +203,10 @@ void inherit_settings(ChannelFloodProtection *from, ChannelFloodProtection *to); void reapply_profiles(void); int _get_floodprot_channel_max_lines(Channel *channel); int _floodprot_check_multiline_batch(Channel *channel, Client *client, int line_count); +void channelfloodblocks_free(ModData *m); +static void channel_flood_blocked_increment(Client *client, int what); +int _channel_flood_blocked_count(Client *client, const char *type); +void _channel_flood_expand_json(json_t *root, Client *client); MOD_TEST() { @@ -193,6 +215,8 @@ MOD_TEST() HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, floodprot_config_test_antiflood_block); EfunctionAdd(modinfo->handle, EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, _get_floodprot_channel_max_lines); EfunctionAdd(modinfo->handle, EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, _floodprot_check_multiline_batch); + EfunctionAdd(modinfo->handle, EFUNC_CHANNEL_FLOOD_BLOCKED_COUNT, _channel_flood_blocked_count); + EfunctionAddVoid(modinfo->handle, EFUNC_CHANNEL_FLOOD_EXPAND_JSON, _channel_flood_expand_json); return MOD_SUCCESS; } @@ -244,6 +268,15 @@ MOD_INIT() mdflood = ModDataAdd(modinfo->handle, mreq); if (!mdflood) abort(); + + memset(&mreq, 0, sizeof(mreq)); + mreq.name = "channelfloodblocks"; + mreq.type = MODDATATYPE_LOCAL_CLIENT; + mreq.free = channelfloodblocks_free; + md_channelflood_blocked = ModDataAdd(modinfo->handle, mreq); + if (!md_channelflood_blocked) + abort(); + if (!floodprot_msghash_key) { floodprot_msghash_key = safe_alloc(16); @@ -1457,6 +1490,8 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership * flood_type = CHFLD_TEXT; } + channel_flood_blocked_increment(client, flood_type); + if (fld->action[flood_type] == 'd') { /* Drop the message */ @@ -1788,7 +1823,10 @@ int do_floodprot(Channel *channel, Client *client, int what) (TStime() - fld->timer[what] < fld->per)) { if (MyUser(client)) + { do_floodprot_action(channel, what); + channel_flood_blocked_increment(client, what); + } return 1; /* flood detected! */ } } @@ -1998,6 +2036,87 @@ void memberflood_free(ModData *md) safe_free(md->ptr); } +void channelfloodblocks_free(ModData *m) +{ + safe_free(m->ptr); +} + +/* Count a channel-flood-block (+f/+F) for this local user, by flood type (enum Flood). + * bump_tag_serial() lets rule-only spamfilters re-check at the next safe boundary + * (eg total_channel_flood_count('nick') > 5), see parse_client_queued(). + */ +static void channel_flood_blocked_increment(Client *client, int what) +{ + ChannelFloodBlocks *b; + + if (!MyConnect(client) || (what < 0) || (what >= NUMFLD)) + return; + + if (moddata_local_client(client, md_channelflood_blocked).ptr == NULL) + moddata_local_client(client, md_channelflood_blocked).ptr = safe_alloc(sizeof(ChannelFloodBlocks)); + + b = (ChannelFloodBlocks *)moddata_local_client(client, md_channelflood_blocked).ptr; + if (b->blocked[what] < 65535) + b->blocked[what]++; + + bump_tag_serial(client); +} + +/* efunc: how many times this local user was channel-flood-blocked of type 'type' + * (a name from channelfloodtype_names[], or "all" for the grand total). Unknown -> 0. + */ +int _channel_flood_blocked_count(Client *client, const char *type) +{ + ChannelFloodBlocks *b; + int i, total = 0; + + if (!client || !MyConnect(client)) + return 0; + if (moddata_local_client(client, md_channelflood_blocked).ptr == NULL) + return 0; + b = (ChannelFloodBlocks *)moddata_local_client(client, md_channelflood_blocked).ptr; + + if (!strcasecmp(type, "all")) + { + for (i = 0; i < NUMFLD; i++) + total += b->blocked[i]; + return total; + } + + for (i = 0; i < NUMFLD; i++) + if (!strcasecmp(channelfloodtype_names[i], type)) + return b->blocked[i]; + + return 0; +} + +/* efunc: add a "channel" object (type -> count) to 'root' for this local user, listing only + * the channel-flood-block (+f/+F) types with a nonzero count this session. Adds nothing when + * there is no data. Used for: spamreport and JSON-RPC (via json_expand_flood_counts()). + */ +void _channel_flood_expand_json(json_t *root, Client *client) +{ + ChannelFloodBlocks *b; + json_t *channel; + int i; + + if (!client || !MyConnect(client)) + return; + if (moddata_local_client(client, md_channelflood_blocked).ptr == NULL) + return; + b = (ChannelFloodBlocks *)moddata_local_client(client, md_channelflood_blocked).ptr; + + channel = json_object(); + for (i = 0; i < NUMFLD; i++) + if (b->blocked[i] > 0) + json_object_set_new(channel, channelfloodtype_names[i], json_integer(b->blocked[i])); + + if (json_object_size(channel) > 0) + json_object_set_new(root, "channel", channel); + else + json_decref(channel); +} + int floodprot_stats(Client *client, const char *flag) { if (*flag != 'S') diff --git a/src/modules/chanmodes/history.c b/src/modules/chanmodes/history.c index 43127abfe9..30d4331cf7 100644 --- a/src/modules/chanmodes/history.c +++ b/src/modules/chanmodes/history.c @@ -781,7 +781,7 @@ int history_join(Client *client, Channel *channel, MessageTag *mtags) } l = next; } - history_send_result(client, r); + history_send_result(client, r, 0); free_history_result(r); } } diff --git a/src/modules/chathistory.c b/src/modules/chathistory.c index 94907a3e7c..4d869d0520 100644 --- a/src/modules/chathistory.c +++ b/src/modules/chathistory.c @@ -27,6 +27,7 @@ struct ChatHistoryTarget { /* Forward declarations */ CMD_FUNC(cmd_chathistory); static int chathistory_account_is_member(Client *client, Channel *channel); +int chathistory_end_mtag_is_ok(Client *client, const char *name, const char *value); /* Global variables */ long CAP_CHATHISTORY = 0L; @@ -36,13 +37,22 @@ long CAP_CHATHISTORY = 0L; MOD_INIT() { ClientCapabilityInfo c; + ClientCapability *cap; + MessageTagHandlerInfo mtag; MARK_AS_OFFICIAL_MODULE(modinfo); CommandAdd(modinfo->handle, "CHATHISTORY", cmd_chathistory, MAXPARA, CMD_USER); memset(&c, 0, sizeof(c)); c.name = "draft/chathistory"; - ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY); + cap = ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY); + + memset(&mtag, 0, sizeof(mtag)); + mtag.name = "draft/chathistory-end"; + mtag.is_ok = chathistory_end_mtag_is_ok; + mtag.clicap_handler = cap; + MessageTagHandlerAdd(modinfo->handle, &mtag); + return MOD_SUCCESS; } @@ -58,6 +68,14 @@ MOD_UNLOAD() return MOD_SUCCESS; } +/** The draft/chathistory-end tag is server-generated only; reject it from clients */ +int chathistory_end_mtag_is_ok(Client *client, const char *name, const char *value) +{ + if (IsServer(client)) + return 1; + return 0; +} + int chathistory_token(const char *str, char *token, char **store) { char request[BUFSIZE]; @@ -189,18 +207,34 @@ void chathistory_targets(Client *client, HistoryFilter *filter, int limit) /* 2. Now send it to the client */ + /* Count total matching targets to determine end-of-pagination */ + { + ChatHistoryTarget *t; + for (t = targets; t; t = t->next) + sent++; + } + batch[0] = '\0'; if (HasCapability(client, "batch")) { /* Start a new batch */ + MessageTag *batch_open_mtags = NULL; generate_batch_id(batch); - sendto_one(client, NULL, ":%s BATCH +%s draft/chathistory-targets", me.name, batch); + if (sent <= limit) + { + batch_open_mtags = safe_alloc(sizeof(MessageTag)); + safe_strdup(batch_open_mtags->name, "draft/chathistory-end"); + } + sendto_one(client, batch_open_mtags, ":%s BATCH +%s draft/chathistory-targets", me.name, batch); + if (batch_open_mtags) + free_message_tags(batch_open_mtags); } + sent = 0; for (; targets; targets = targets_next) { targets_next = targets->next; - if (++sent < limit) + if (sent++ < limit) chathistory_targets_send_line(client, targets, batch); safe_free(targets->datetime); safe_free(targets->object); @@ -451,7 +485,7 @@ CMD_FUNC(cmd_chathistory) if (fakelag_ms > 5000) fakelag_ms = 5000; add_fake_lag(client, fakelag_ms); - history_send_result(client, r); + history_send_result(client, r, r->reached_end); } } diff --git a/src/modules/chgname.c b/src/modules/chgname.c index bac4fcd1d9..b4b628f9b7 100644 --- a/src/modules/chgname.c +++ b/src/modules/chgname.c @@ -124,7 +124,7 @@ CMD_FUNC(cmd_chgname) if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",target,NULL,NULL,NULL) && ((bconf = find_ban(NULL, target->info, CONF_BAN_REALNAME)))) { - banned_client(target, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(target, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return; } } diff --git a/src/modules/crule.c b/src/modules/crule.c index c7d5c5219d..fea0f29945 100644 --- a/src/modules/crule.c +++ b/src/modules/crule.c @@ -125,6 +125,8 @@ static int crule__not(crule_context *, int, void **); static int crule_online_time(crule_context *, int, void **); static int crule_reputation(crule_context *, int, void **); static int crule_tag(crule_context *, int, void **); +static int crule_server_flood_count(crule_context *, int, void **); +static int crule_total_channel_flood_count(crule_context *, int, void **); static int crule_in_channel(crule_context *, int, void **); static int crule_destination(crule_context *, int, void **); static int crule_cap_version(crule_context *, int, void **); @@ -260,10 +262,12 @@ struct crule_funclistent crule_funclist[] = { {"non_ascii_percentage", 0, crule_non_ascii_percentage}, {"online_time", 0, crule_online_time}, {"reputation", 0, crule_reputation}, + {"server_flood_count", 1, crule_server_flood_count}, {"server_port", 0, crule_server_port}, {"tag", 1, crule_tag}, {"text_byte_count", 0, crule_text_byte_count}, {"text_character_count", 0, crule_text_character_count}, + {"total_channel_flood_count", 1, crule_total_channel_flood_count}, {"unicode_block_count", 0, crule_unicode_block_count}, {"unicode_count", 1, crule_unicode_count}, {"uppercase_percentage", 0, crule_uppercase_percentage}, @@ -449,6 +453,43 @@ static int crule_reputation(crule_context *context, int numargs, void *crulearg[ return 0; } +/* server_flood_count('away') etc: how many times the client hit that server flood limit + * this session. 'all' gives the grand total across types. Unknown type returns 0. + */ +static int crule_server_flood_count(crule_context *context, int numargs, void *crulearg[]) +{ + const char *type = (char *)crulearg[0]; + int i, total = 0; + + if (!context || !context->client || !MyConnect(context->client)) + return 0; + + if (!strcasecmp(type, "all")) + { + for (i = 0; i < MAXFLOODOPTIONS; i++) + total += context->client->local->flood[i].blocked; + return total; + } + + for (i = 0; i < MAXFLOODOPTIONS; i++) + if (floodoption_shortnames[i] && !strcasecmp(floodoption_shortnames[i], type)) + return context->client->local->flood[i].blocked; + + return 0; /* unknown flood type */ +} + +/* total_channel_flood_count('nick') etc: how many times the client was blocked by channel + * flood protection (+f/+F) of that type this session, summed across all their channels. + * 'all' is the grand total. Backed by the floodprot module via the channel_flood_blocked_count + * efunc (returns 0 if floodprot is not loaded, or for an unknown type). + */ +static int crule_total_channel_flood_count(crule_context *context, int numargs, void *crulearg[]) +{ + if (!context || !context->client) + return 0; + return channel_flood_blocked_count(context->client, (char *)crulearg[0]); +} + static int crule_tag(crule_context *context, int numargs, void *crulearg[]) { Tag *tag; diff --git a/src/modules/history.c b/src/modules/history.c index d99823ee66..5d6f6f0123 100644 --- a/src/modules/history.c +++ b/src/modules/history.c @@ -130,7 +130,7 @@ CMD_FUNC(cmd_history) if ((r = history_request(channel->name, &filter))) { - history_send_result(client, r); + history_send_result(client, r, 0); free_history_result(r); } } diff --git a/src/modules/history_backend_mem.c b/src/modules/history_backend_mem.c index 392765bb16..6cbbe1c3c3 100644 --- a/src/modules/history_backend_mem.c +++ b/src/modules/history_backend_mem.c @@ -428,6 +428,7 @@ static void init_history_storage(ModuleInfo *modinfo) cap.name = "unrealircd.org/history-storage"; cap.flags = CLICAP_FLAGS_ADVERTISE_ONLY; cap.parameter = history_storage_capability_parameter; + cap.minimum_cap_version = 302; ClientCapabilityAdd(modinfo->handle, &cap, NULL); } @@ -893,6 +894,7 @@ static int hbm_return_after(HistoryResult *r, HistoryLogObject *h, HistoryFilter HistoryLogLine *l, *n; int written = 0; int started = 0; + int reached_end = 1; MessageTag *m; for (l = h->head; l; l = l->next) @@ -922,14 +924,20 @@ static int hbm_return_after(HistoryResult *r, HistoryLogObject *h, HistoryFilter break; } + /* Limit reached but there are more lines available */ + if (written >= filter->limit) + { + reached_end = 0; + break; + } /* Add line to the return buffer */ n = duplicate_log_line(l); hbm_result_append_line(r, n); - if (++written >= filter->limit) - break; + written++; } } + r->reached_end = reached_end; return written; } @@ -946,6 +954,7 @@ static int hbm_return_before(HistoryResult *r, HistoryLogObject *h, HistoryFilte HistoryLogLine *l, *n; int written = 0; int started = 0; + int reached_end = 1; MessageTag *m; for (l = h->tail; l; l = l->prev) @@ -975,14 +984,20 @@ static int hbm_return_before(HistoryResult *r, HistoryLogObject *h, HistoryFilte break; } + /* Limit reached but there are more lines available */ + if (written >= filter->limit) + { + reached_end = 0; + break; + } /* Add line to the return buffer */ n = duplicate_log_line(l); hbm_result_prepend_line(r, n); - if (++written >= filter->limit) - break; + written++; } } + r->reached_end = reached_end; return written; } @@ -997,6 +1012,7 @@ static int hbm_return_latest(HistoryResult *r, HistoryLogObject *h, HistoryFilte { HistoryLogLine *l, *n; int written = 0; + int reached_end = 1; MessageTag *m; for (l = h->tail; l; l = l->prev) @@ -1007,12 +1023,18 @@ static int hbm_return_latest(HistoryResult *r, HistoryLogObject *h, HistoryFilte if (filter->msgid_a && !strcmp(l->msgid, filter->msgid_a)) break; /* Stop now */ + /* Limit reached but there are more lines available */ + if (written >= filter->limit) + { + reached_end = 0; + break; + } n = duplicate_log_line(l); hbm_result_prepend_line(r, n); - if (++written >= filter->limit) - break; + written++; } + r->reached_end = reached_end; return written; } @@ -1268,8 +1290,9 @@ static int hbm_return_between(HistoryResult *r, HistoryLogObject *h, HistoryFilt f.msgid_b = filter->msgid_a; return hbm_return_after(r, h, &f); } - /* else direction is -1 which means not found / invalid */ + /* else direction is -1 which means not found / invalid */ + r->reached_end = 1; return 0; } diff --git a/src/modules/join.c b/src/modules/join.c index 6a81c6af57..e410887220 100644 --- a/src/modules/join.c +++ b/src/modules/join.c @@ -271,7 +271,7 @@ void _join_channel(Channel *channel, Client *client, MessageTag *recv_mtags, con parv[0] = NULL; parv[1] = channel->name; parv[2] = NULL; - if (!HasCapability(client,"draft/no-implicit-names") /* && !HasCapability(client, "no-implicit-names") */) + if (!HasCapability(client,"draft/no-implicit-names") && !HasCapability(client, "no-implicit-names")) do_cmd(client, NULL, "NAMES", 2, parv);; unreal_log(ULOG_INFO, "join", "LOCAL_CLIENT_JOIN", client, @@ -487,6 +487,7 @@ void _do_join(Client *client, int parc, const char *parv[]) } if (!ValidatePermissionsForPath("immune:server-ban:deny-channel",client,NULL,NULL,NULL) && (tklban = find_qline(client, name, &ishold))) { + tkl_hit(client, tklban); sendnumeric(client, ERR_FORBIDDENCHANNEL, name, tklban->ptr.nameban->reason); continue; } diff --git a/src/modules/jointhrottle.c b/src/modules/jointhrottle.c index 58a24049fc..0f8044a095 100644 --- a/src/modules/jointhrottle.c +++ b/src/modules/jointhrottle.c @@ -153,6 +153,7 @@ int jointhrottle_can_join(Client *client, Channel *channel, const char *key, cha { if (!ValidatePermissionsForPath("immune:join-flood",client,NULL,channel,NULL) && isjthrottled(client, channel)) { + flood_blocked_increment(client, FLD_JOIN); *errmsg = STR_ERR_TOOMANYJOINS; return ERR_TOOMANYJOINS; } diff --git a/src/modules/json-log-tag.c b/src/modules/json-log-tag.c index 89d86106bc..92e758a015 100644 --- a/src/modules/json-log-tag.c +++ b/src/modules/json-log-tag.c @@ -54,6 +54,7 @@ MOD_INIT() memset(&cap, 0, sizeof(cap)); cap.name = "unrealircd.org/json-log"; + cap.minimum_cap_version = 302; c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_JSON_LOG); memset(&mtag, 0, sizeof(mtag)); diff --git a/src/modules/link-security.c b/src/modules/link-security.c index 76e8bed110..30fac0fba7 100644 --- a/src/modules/link-security.c +++ b/src/modules/link-security.c @@ -80,6 +80,7 @@ MOD_LOAD() cap.name = "unrealircd.org/link-security"; cap.flags = CLICAP_FLAGS_ADVERTISE_ONLY; cap.parameter = link_security_capability_parameter; + cap.minimum_cap_version = 302; ClientCapabilityAdd(modinfo->handle, &cap, NULL); EventAdd(modinfo->handle, "checklinksec", checklinksec, NULL, 2000, 0); diff --git a/src/modules/message-tags.c b/src/modules/message-tags.c index 109a893813..78b4342191 100644 --- a/src/modules/message-tags.c +++ b/src/modules/message-tags.c @@ -112,45 +112,42 @@ void message_tag_unescape(char *in, char *out) } /** Escape a message tag (name or value). - * @param in The input string - * @param out The output string for writing - * @note No size checking, so ensure that the output buffer - * is at least twice as long as the input buffer + 1. + * @param in The input string + * @param out The output buffer + * @param outsize Size of the output buffer + * @note Escaping can double the length of the input, so the output is + * truncated if it does not fit. We always NUL-terminate and never + * write a half-finished escape sequence. */ -void message_tag_escape(char *in, char *out) +void message_tag_escape(char *in, char *out, size_t outsize) { + char *o = out; + char *end = out + outsize; + for (; *in; in++) { - if (*in == ';') - { - *out++ = '\\'; - *out++ = ':'; - } else - if (*in == ' ') - { - *out++ = '\\'; - *out++ = 's'; - } else - if (*in == '\\') + if (*in == ';' || *in == ' ' || *in == '\\' || *in == '\r' || *in == '\n') { - *out++ = '\\'; - *out++ = '\\'; - } else - if (*in == '\r') - { - *out++ = '\\'; - *out++ = 'r'; - } else - if (*in == '\n') - { - *out++ = '\\'; - *out++ = 'n'; - } else - { - *out++ = *in; + if (end - o < 3) /* 2-byte escape + NUL */ + break; + *o++ = '\\'; + if (*in == ';') + *o++ = ':'; + else if (*in == ' ') + *o++ = 's'; + else if (*in == '\\') + *o++ = '\\'; + else if (*in == '\r') + *o++ = 'r'; + else + *o++ = 'n'; + } else { + if (end - o < 2) /* 1 byte + NUL */ + break; + *o++ = *in; } } - *out = '\0'; + *o = '\0'; } /** Incoming filter for message tags */ @@ -307,11 +304,11 @@ const char *_mtags_to_string(MessageTag *m, Client *client) continue; if (m->value) { - message_tag_escape(m->name, name); - message_tag_escape(m->value, value); + message_tag_escape(m->name, name, sizeof(name)); + message_tag_escape(m->value, value, sizeof(value)); snprintf(tbuf, sizeof(tbuf), "%s=%s;", name, value); } else { - message_tag_escape(m->name, name); + message_tag_escape(m->name, name, sizeof(name)); snprintf(tbuf, sizeof(tbuf), "%s;", name); } strlcat(buf, tbuf, sizeof(buf)); diff --git a/src/modules/multiline.c b/src/modules/multiline.c index 2e547b4b20..d9584b0618 100644 --- a/src/modules/multiline.c +++ b/src/modules/multiline.c @@ -57,7 +57,6 @@ struct MultilineBatch { /* Buffered lines */ MLine *lines; MLine *lines_tail; - MLine *fallback_lines; /**< Cached fallback lines (built once, reused for all non-multiline clients) */ }; /** State for an S2S multiline batch being received from a remote server */ @@ -453,7 +452,6 @@ static void multiline_free_batch(MultilineBatch *batch) safe_free(batch->fail_message); free_message_tags(batch->client_mtags); multiline_free_lines(batch->lines); - multiline_free_lines(batch->fallback_lines); safe_free(batch); } @@ -592,51 +590,6 @@ static char *multiline_concat_text(MultilineBatch *batch) return buf; } -/** Build fallback text: merge concat lines into their predecessors. - * Returns a list of "logical lines" for fallback delivery. - * Caller must free the returned list with multiline_free_lines(). - */ -static MLine *multiline_build_fallback_lines(MultilineBatch *batch) -{ - MLine *out = NULL, *out_tail = NULL; - MLine *l; - - for (l = batch->lines; l; l = l->next) - { - if (l->concat && out_tail) - { - /* Append to current logical line without separator */ - const char *append = l->text ? l->text : ""; - int newlen = strlen(out_tail->text) + strlen(append) + 1; - char *newtext = safe_alloc(newlen); - strlcpy(newtext, out_tail->text, newlen); - strlcat(newtext, append, newlen); - safe_free(out_tail->text); - out_tail->text = newtext; - } else { - /* Start a new logical line */ - MLine *newline = safe_alloc(sizeof(MLine)); - safe_strdup(newline->text, l->text ? l->text : ""); - newline->concat = 0; - newline->next = NULL; - if (out_tail) - out_tail->next = newline; - else - out = newline; - out_tail = newline; - } - } - return out; -} - -/** Get cached fallback lines, building them on first call */ -static MLine *multiline_get_fallback_lines(MultilineBatch *batch) -{ - if (!batch->fallback_lines) - batch->fallback_lines = multiline_build_fallback_lines(batch); - return batch->fallback_lines; -} - /** Check if a batch has at least one non-blank line */ static int multiline_batch_has_content(MultilineBatch *batch) { @@ -1414,19 +1367,20 @@ static void multiline_send_batch_to_client(Client *to, Client *from, MultilineBa static void multiline_send_fallback_to_client(Client *to, Client *from, MultilineBatch *batch, MessageTag *base_mtags, const char *targetstr, const char *cmd) { - MLine *fallback_lines, *l; + MLine *l; MessageTag *rest_mtags; int first = 1; - fallback_lines = multiline_get_fallback_lines(batch); - /* Build rest_mtags: base_mtags minus msgid. * Spec: msgid MUST only be on the first line, but other tags * (time, account, etc.) MAY be on subsequent lines. */ rest_mtags = duplicate_mtags_for_subsequent_lines(base_mtags); - for (l = fallback_lines; l; l = l->next) + /* This client has no multiline support, so just send each line as a + * normal separate message. + */ + for (l = batch->lines; l; l = l->next) { /* Spec: "Servers MUST NOT send blank lines to clients * that have not negotiated the multiline capability." @@ -1465,7 +1419,6 @@ static void multiline_deliver_to_local_members(Channel *channel, Client *from, { LocalMember *lm; MLine *l; - MLine *fallback_lines; // merged lines for non-multiline clients MessageTag *m; // temporary for building tag lists MessageTag *rest_mtags; // base_mtags minus first-only tags (msgid, +draft/reply) MessageTag *mtags_line; // rest_mtags + batch=, for multiline content lines @@ -1514,9 +1467,8 @@ static void multiline_deliver_to_local_members(Channel *channel, Client *from, for (i = 0; i < batch->line_count; i++) cache_lines[i] = linecache_init(); - /* Fallback path: one cache per non-blank fallback line */ - fallback_lines = multiline_get_fallback_lines(batch); - for (l = fallback_lines; l; l = l->next) + /* Fallback path: one cache per non-blank line */ + for (l = batch->lines; l; l = l->next) if (l->text && l->text[0]) fallback_count++; if (fallback_count > 0) @@ -1574,7 +1526,7 @@ static void multiline_deliver_to_local_members(Channel *channel, Client *from, first = 1; i = 0; - for (l = fallback_lines; l; l = l->next) + for (l = batch->lines; l; l = l->next) { /* Spec: "Servers MUST NOT send blank lines to clients * that have not negotiated the multiline capability." diff --git a/src/modules/nick.c b/src/modules/nick.c index 204281ce40..59b4437fb5 100644 --- a/src/modules/nick.c +++ b/src/modules/nick.c @@ -314,6 +314,7 @@ CMD_FUNC(cmd_nick_local) } if (!ValidatePermissionsForPath("immune:server-ban:ban-nick",client,NULL,NULL,nick)) { + tkl_hit(client, tklban); add_fake_lag(client, 4000); /* lag them up */ sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, tklban->ptr.nameban->reason); unreal_log(ULOG_INFO, "nick", "QLINE_NICK_LOCAL_ATTEMPT", client, @@ -1096,7 +1097,7 @@ int _register_user(Client *client) if ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME))) { ircstats.is_ref++; - banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return 0; } /* Check G/Z lines before shuns -- kill before quite -- codemastr */ diff --git a/src/modules/no-implicit-names.c b/src/modules/no-implicit-names.c index c0175617c9..213ec0e712 100644 --- a/src/modules/no-implicit-names.c +++ b/src/modules/no-implicit-names.c @@ -40,24 +40,15 @@ MOD_INIT() MARK_AS_OFFICIAL_MODULE(modinfo); - /** We only add the draft/ version for now */ memset(&cap, 0, sizeof(cap)); cap.name = NO_IMPLICIT_NAMES_CAP_DRAFT; if (!ClientCapabilityAdd(modinfo->handle, &cap, &CAP_NO_IMPLICIT_NAMES_DRAFT)) - { return MOD_FAILED; - } - /** This is for the future :D - * If you add this, then also update - * _join_channel in src/modules/join.c to check for it, - * as it is currently commented out there as well. - */ - /** memset(&cap, 0, sizeof(cap)); cap.name = NO_IMPLICIT_NAMES_CAP; - ClientCapabilityAdd(modinfo->handle, &cap, &CAP_NO_IMPLICIT_NAMES); - */ + if (!ClientCapabilityAdd(modinfo->handle, &cap, &CAP_NO_IMPLICIT_NAMES)) + return MOD_FAILED; return MOD_SUCCESS; } diff --git a/src/modules/plaintext-policy.c b/src/modules/plaintext-policy.c index d5159d682f..dd82b655c8 100644 --- a/src/modules/plaintext-policy.c +++ b/src/modules/plaintext-policy.c @@ -71,5 +71,6 @@ void init_plaintext_policy(ModuleInfo *modinfo) cap.name = "unrealircd.org/plaintext-policy"; cap.flags = CLICAP_FLAGS_ADVERTISE_ONLY; cap.parameter = plaintext_policy_capability_parameter; + cap.minimum_cap_version = 302; ClientCapabilityAdd(modinfo->handle, &cap, NULL); } diff --git a/src/modules/quit.c b/src/modules/quit.c index 54e62232bd..7f949d06ab 100644 --- a/src/modules/quit.c +++ b/src/modules/quit.c @@ -38,7 +38,7 @@ CMD_FUNC(cmd_quit); void _exit_client(Client *client, MessageTag *recv_mtags, const char *comment); void _exit_client_fmt(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); void _exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); -void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit); +void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit); static void remove_dependents(Client *client, Client *from, MessageTag *mtags, const char *comment, const char *splitstr); static void exit_one_client(Client *, MessageTag *mtags_i, const char *); static int should_hide_ban_reason(Client *client, const char *reason); @@ -491,11 +491,13 @@ static void exit_one_client(Client *client, MessageTag *mtags_i, const char *com * * @note This function will call exit_client() appropriately. */ -void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit) +void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit) { char buf[512]; + char idbuf[64]; + const char *banid; char *fmt = global ? iConf.reject_message_gline : iConf.reject_message_kline; - const char *vars[6], *values[6]; + const char *vars[7], *values[7]; MessageTag *mtags = NULL; if (!MyConnect(client)) @@ -503,6 +505,19 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int RunHook(HOOKTYPE_BANNED_CLIENT, client, bantype, reason, global); + /* Create the tklid. We actually need two buffers: + * 1) 'idbuf' is used in snprintf() in the quit reason and is " [ID: %s]" (or "") + * 2) 'banid' is used by buildvarstring() and is just "[ID: %s]" (or "") + */ + if (!BadPtr(tklid)) + { + snprintf(idbuf, sizeof(idbuf), " [ID: %s]", tklid); + banid = idbuf + 1; + } else { + idbuf[0] = '\0'; + banid = idbuf; + } + /* This was: "You are not welcome on this %s. %s: %s. %s" but is now dynamic: */ vars[0] = "bantype"; values[0] = bantype; @@ -514,8 +529,10 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int values[3] = GLINE_ADDRESS ? GLINE_ADDRESS : KLINE_ADDRESS; /* fallback to klineaddr */ vars[4] = "ip"; values[4] = GetIP(client); - vars[5] = NULL; - values[5] = NULL; + vars[5] = "banid"; + values[5] = banid; + vars[6] = NULL; + values[6] = NULL; buildvarstring(fmt, buf, sizeof(buf), vars, values); /* This is a bit extensive but we will send both a YOUAREBANNEDCREEP @@ -537,13 +554,13 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int /* Hide the ban reason, but put the real reason in unrealircd.org/real-quit-reason */ MessageTag *m = safe_alloc(sizeof(MessageTag)); safe_strdup(m->name, "unrealircd.org/real-quit-reason"); - snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); + snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf); safe_strdup(m->value, buf); AddListItem(m, mtags); /* And the quit reason for anyone else, goes here.. */ - snprintf(buf, sizeof(buf), "Banned (%s)", bantype); + snprintf(buf, sizeof(buf), "Banned (%s)%s", bantype, idbuf); } else { - snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); + snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf); } if (noexit != NO_EXIT_CLIENT) diff --git a/src/modules/reply-tag.c b/src/modules/reply-tag.c index 0ec51d0075..14b521f05d 100644 --- a/src/modules/reply-tag.c +++ b/src/modules/reply-tag.c @@ -42,13 +42,11 @@ MOD_INIT() MARK_AS_OFFICIAL_MODULE(modinfo); -#if 0 memset(&mtag, 0, sizeof(mtag)); mtag.name = "+reply"; mtag.is_ok = replytag_mtag_is_ok; - mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED; + mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED | MTAG_HANDLER_FLAGS_FIRST_ONLY; MessageTagHandlerAdd(modinfo->handle, &mtag); -#endif memset(&mtag, 0, sizeof(mtag)); mtag.name = "+draft/reply"; @@ -98,14 +96,12 @@ void mtag_add_replytag(Client *client, MessageTag *recv_mtags, MessageTag **mtag if (IsUser(client)) { -#if 0 m = find_mtag(recv_mtags, "+reply"); if (m) { m = duplicate_mtag(m); AddListItem(m, *mtag_list); } -#endif m = find_mtag(recv_mtags, "+draft/reply"); if (m) { diff --git a/src/modules/rpc/rpc.c b/src/modules/rpc/rpc.c index d8c463f154..fec689d857 100644 --- a/src/modules/rpc/rpc.c +++ b/src/modules/rpc/rpc.c @@ -9,7 +9,7 @@ ModuleHeader MOD_HEADER = { "rpc/rpc", - "1.0.4", + "1.0.5", "RPC module for remote management", "UnrealIRCd Team", "unrealircd-6", @@ -925,8 +925,9 @@ void _rpc_error(Client *client, json_t *request, JsonRpcError error_code, const json_object_set_new(error, "message", json_string_unreal(error_message)); unreal_log(ULOG_INFO, "rpc", "RPC_CALL_ERROR", client, - "[rpc] Client $client: RPC call $method", - log_data_string("method", method ? method : "")); + "[rpc] Client $client: RPC call $method: $error_message", + log_data_string("method", method ? method : ""), + log_data_string("error_message", error_message)); json_serialized = json_dumps(j, 0); @@ -1884,6 +1885,7 @@ void rpc_call_remote(RRPC *r) return; } client = make_client(server->direction, server); + SetRPC(client); strlcpy(client->id, r->source, sizeof(client->id)); client->rpc = safe_alloc(sizeof(RPCClient)); strlcpy(client->name, "RPC:remote", sizeof(client->name)); @@ -2028,7 +2030,7 @@ void rpc_send_generic_to_remote(Client *source, Client *target, const char *requ safe_free(json_serialized); } -int _rrpc_supported_simple(Client *target, char **problem_server) +int rrpc_supported_simple_helper(Client *target, char **problem_server) { if (!moddata_client_get(target, "rrpc")) { @@ -2036,20 +2038,51 @@ int _rrpc_supported_simple(Client *target, char **problem_server) *problem_server = target->name; return 0; } - if ((target != target->direction) && !rrpc_supported_simple(target->direction, problem_server)) + if ((target != target->direction) && !rrpc_supported_simple_helper(target->direction, problem_server)) return 0; return 1; } +int _rrpc_supported_simple(Client *target, char **problem_server) +{ + /* We require the RPC module and at least module version 1.0.5. + * This is because there is a bug in 1.0.4 where remote RPC did + * not work due to an auth check that was only meant locally. + */ + return rrpc_supported(target, "rpc", "1.0.5", problem_server); +} + int _rrpc_supported(Client *target, const char *module, const char *minimum_version, char **problem_server) { + const char *have_version; + if (!moddata_client_get(target, "rrpc")) { if (problem_server) *problem_server = target->name; return 0; } - if ((target != target->direction) && !rrpc_supported_simple(target->direction, problem_server)) + + /* If a specific rpc module (and optionally a minimum version) is requested, check that + * 'target' actually advertises it. The module list (name -> version) travels via the + * synced "rrpc" moddata, so we can check this for any server on the network. + */ + if (module) + { + have_version = get_nvplist(RRPCMODULES(target), module); + if (!have_version || + (minimum_version && (strnatcmp(have_version, minimum_version) < 0))) + { + if (problem_server) + *problem_server = target->name; + return 0; + } + } + + /* And the entire path towards 'target' must support remote RPC (transport only; + * intermediate servers just relay, so they don't need the module itself). + */ + if ((target != target->direction) && !rrpc_supported_simple_helper(target->direction, problem_server)) return 0; return 1; } @@ -2189,6 +2222,13 @@ RPC_CALL_FUNC(rpc_rpc_add_timer) RPCHandler *handler; RPCTimer *timer; + /* Remote timers would be weird, complex and are not supported */ + if (!MyConnect(client)) + { + rpc_error(client, request, JSON_RPC_ERROR_INVALID_REQUEST, "rpc.add_timer is not available for remote RPC requests"); + return; + } + REQUIRE_PARAM_INTEGER("every_msec", every_msec); REQUIRE_PARAM_STRING("timer_id", timer_id); diff --git a/src/modules/rpc/spamfilter.c b/src/modules/rpc/spamfilter.c index a10bf73abe..bae9531013 100644 --- a/src/modules/rpc/spamfilter.c +++ b/src/modules/rpc/spamfilter.c @@ -210,7 +210,7 @@ RPC_CALL_FUNC(rpc_spamfilter_add) char targetbuf[64]; char actionbuf[2]; char reasonbuf[512]; - char *err = NULL; + const char *err = NULL; if (!spamfilter_select_criteria(client, request, params, &name, &match_type, &targets, targetbuf, sizeof(targetbuf), &action, actionbuf)) return; /* Error already communicated to client */ diff --git a/src/modules/rpc/stats.c b/src/modules/rpc/stats.c index a21c262605..d95ab57b19 100644 --- a/src/modules/rpc/stats.c +++ b/src/modules/rpc/stats.c @@ -955,7 +955,7 @@ static void collect_tkl_stats(json_t *parent, int detail) { spamfilter++; if (tkl->ptr.spamfilter) - total_spamfilter_hits += tkl->ptr.spamfilter->hits; + total_spamfilter_hits += tkl->hits; } else if (TKLIsBanException(tkl)) except++; @@ -985,7 +985,7 @@ static void collect_tkl_stats(json_t *parent, int detail) { spamfilter++; if (tkl->ptr.spamfilter) - total_spamfilter_hits += tkl->ptr.spamfilter->hits; + total_spamfilter_hits += tkl->hits; } else if (TKLIsBanException(tkl)) except++; diff --git a/src/modules/rpc/user.c b/src/modules/rpc/user.c index 41e53f7e32..3d03d9c371 100644 --- a/src/modules/rpc/user.c +++ b/src/modules/rpc/user.c @@ -188,10 +188,11 @@ RPC_CALL_FUNC(rpc_user_list) RPC_CALL_FUNC(rpc_user_get) { - json_t *result, *list, *item; + json_t *result; const char *nick; Client *acptr; int details; + int remote_fetch; REQUIRE_PARAM_STRING("nick", nick); @@ -201,6 +202,19 @@ RPC_CALL_FUNC(rpc_user_get) rpc_error(client, request, JSON_RPC_ERROR_INVALID_PARAMS, "Using an 'object_detail_level' of 3 is not allowed in user.* calls, use 0, 1, 2 or 4."); return; } + /* Some fields (flood counters, idle time, snomasks, ..) only exist + * on the server where the user is on. If 'object_remote_fetch' is + * set to true, then we forward the request to the corresponding + * server. This defaults to false, except when details >= 5, then + * the default is true. Yeah a bit complicated, but that way with + * level 5+ we actually include the fields you want :D. + * Note that default user.get without object_detail_level defaults + * to detail level 4, so default is off. Which is intended, because + * all of this does come at a cost of network latency (server may + * even be multiple hops away) versus locally responding which is + * much faster. + */ + OPTIONAL_PARAM_BOOLEAN("object_remote_fetch", remote_fetch, details >= 5); if (!(acptr = find_user(nick, NULL))) { @@ -208,6 +222,16 @@ RPC_CALL_FUNC(rpc_user_get) return; } + /* Forward to the user's own server if requested and RRPC is supported + * along the entire routing path. Otherwise, just fall back. + */ + if (remote_fetch && !MyConnect(acptr) && acptr->uplink && + rrpc_supported_simple(acptr->uplink, NULL)) + { + rpc_send_request_to_remote(client, acptr->uplink, request); + return; + } + result = json_object(); json_expand_client(result, "client", acptr, details); rpc_response(client, request, result); @@ -254,6 +278,7 @@ RPC_CALL_FUNC(rpc_user_set_nick) /* Check other restrictions */ Client *check = find_user(newnick, NULL); int ishold = 0; + TKL *tklban; /* Check if in use by someone else (do allow case-changing) */ if (check && (acptr != check)) @@ -265,8 +290,9 @@ RPC_CALL_FUNC(rpc_user_set_nick) // Can't really check for spamfilter here, since it assumes user is local // But we can check q-lines... - if (find_qline(acptr, newnick, &ishold)) + if ((tklban = find_qline(acptr, newnick, &ishold))) { + tkl_hit(acptr, tklban); rpc_error(client, request, JSON_RPC_ERROR_INVALID_NAME, "New nickname is forbidden by q-line"); return; } diff --git a/src/modules/server.c b/src/modules/server.c index c4ae56814b..1794de21b0 100644 --- a/src/modules/server.c +++ b/src/modules/server.c @@ -73,6 +73,7 @@ const char *_check_deny_link(ConfigItem_link *link, int auto_connect); int server_stats_denylink_all(Client *client, const char *para); int server_stats_denylink_auto(Client *client, const char *para); int server_quit_reset_autoconnect_time(Client *client, MessageTag *mtags); +int check_server_linking_cert_consistency(void); /* Global variables */ static cfgstruct cfg; @@ -114,6 +115,7 @@ MOD_INIT() HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, server_stats_denylink_all); HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, server_stats_denylink_auto); HookAdd(modinfo->handle, HOOKTYPE_SERVER_QUIT, 0, server_quit_reset_autoconnect_time); + HookAdd(modinfo->handle, HOOKTYPE_POSTCONF, 0, check_server_linking_cert_consistency); CommandAdd(modinfo->handle, "SERVER", cmd_server, MAXPARA, CMD_UNREGISTERED|CMD_SERVER); CommandAdd(modinfo->handle, "SID", cmd_sid, MAXPARA, CMD_SERVER); @@ -199,6 +201,11 @@ int server_config_test_set_server_linking(ConfigFile *cf, ConfigEntry *ce, int t for (cep = ce->items; cep; cep = cep->next) { + if (!strcmp(cep->name, "ssl-options") || !strcmp(cep->name, "tls-options")) + { + test_tlsblock(cf, cep, &errors); + continue; + } if (!cep->value) { config_error("%s:%i: blank set::server-linking::%s without value", @@ -239,6 +246,14 @@ int server_config_test_set_server_linking(ConfigFile *cf, ConfigEntry *ce, int t continue; } } else + if (!strcmp(cep->name, "mixed-certificates")) + { + /* yes/no, applied at config run */ + } else + if (!strcmp(cep->name, "allow-ca-certificate")) + { + /* yes/no, applied at config run */ + } else { config_error("%s:%i: unknown directive set::server-linking::%s", cep->file->filename, cep->line_number, cep->name); @@ -257,6 +272,13 @@ int server_config_run_set_server_linking(ConfigFile *cf, ConfigEntry *ce, int ty for (cep = ce->items; cep; cep = cep->next) { + if (!strcmp(cep->name, "ssl-options") || !strcmp(cep->name, "tls-options")) + { + /* Processed late in core config_run_blocks(), after set::tls is + * finalized, so it inherits the same way listen/link do. + */ + set_server_linking_tlsoptions_ce(cep); + } else if (!strcmp(cep->name, "autoconnect-strategy")) { cfg.autoconnect_strategy = autoconnect_strategy_strtoval(cep->value); @@ -268,12 +290,322 @@ int server_config_run_set_server_linking(ConfigFile *cf, ConfigEntry *ce, int ty if (!strcmp(cep->name, "handshake-timeout")) { cfg.handshake_timeout = config_checkval(cep->value, CFG_TIME); + } else + if (!strcmp(cep->name, "mixed-certificates")) + { + tempiConf.server_linking_mixed_certificates = config_checkval(cep->value, CFG_YESNO); + } else + if (!strcmp(cep->name, "allow-ca-certificate")) + { + tempiConf.server_linking_allow_ca_certificate = config_checkval(cep->value, CFG_YESNO); } } return 1; } +/* Helper for heck_server_linking_cert_consistency(). We build the suggested + * config file block here. + * When 'opts' has a certificate, use those cert/key paths (which are + * for a self-signed cert already in use somewhere). + * When 'opts' is NULL, there is no self signed certificate in use for linking + * (which usually means the user has overriden the default certs with CA certs), + * so we simply recommend to run ./unrealircd mkcert to create a new one. + */ +static void check_server_linking_cert_consistency_suggest_fix(TLSOptions *opts, char *buf, size_t buflen) +{ + char inner[512]; + NameList *c, *k; + + inner[0] = '\0'; + if (opts && opts->certificate_files && opts->key_files) + { + for (c = opts->certificate_files, k = opts->key_files; c && k; c = c->next, k = k->next) + { + char line[384]; + snprintf(line, sizeof(line), + " certificate \"%s\";\n" + " key \"%s\";\n", + display_path(c->name, CONFDIR), display_path(k->name, CONFDIR)); + strlcat(inner, line, sizeof(inner)); + } + } + if (inner[0]) + { + snprintf(buf, buflen, + "set {\n" + " server-linking {\n" + " tls-options {\n" + "%s" + " }\n" + " }\n" + "}", inner); + } + else + { + /* Nothing self-signed is in use, so do not name an existing file (it + * may be the CA certificate). Recommend generating a dedicated one. + * The mkcert command uses an absolute path (mkcert follows normal path + * rules, no config-dir magic) so the files land in the config dir no + * matter the working directory; the config block stays config-relative. + */ + snprintf(buf, buflen, + "Generate one with: ./unrealircd mkcert %s/tls/server-linking\n" + "Then set:\n" + "set {\n" + " server-linking {\n" + " tls-options {\n" + " certificate \"tls/server-linking.cert.pem\";\n" + " key \"tls/server-linking.key.pem\";\n" + " }\n" + " }\n" + "}", CONFDIR); + } +} + +/* Return 1 if two spkifp lists hold the same set of fingerprints (order- + * independent). Used to compare the certificate sets of two server-to-server paths. + */ +static int spkifp_sets_equal(NameList *a, NameList *b) +{ + NameList *e; + + for (e = a; e; e = e->next) + if (!find_name_list(b, e->name)) + return 0; + for (e = b; e; e = e->next) + if (!find_name_list(a, e->name)) + return 0; + return 1; +} + +/** HOOKTYPE_POSTCONF: advise on the certificate(s) used for server linking. + * A peer authenticates us by our SPKI fingerprint(s), so all our server-to-server + * paths (serversonly listeners and outgoing TLS links) must present the same + * certificate(s). We also require it to be a long-lived self-signed ceritifcate as + * a publicly-trusted CA cert is short(er)-lived and typically rekeys on renewal, + * which changes the spkifp and breaks linking). Because this runs in postconf, + * all the TLS context are fully available (this runs on both boot & rehash). + */ +int check_server_linking_cert_consistency(void) +{ + struct { + char desc[128]; + int trusted; + int has_own_tls_options; /* explicit per-block tls-options, vs the global default */ + TLSOptions *opts; + } s2s[128]; + int n = 0, i, capped = 0, mismatch = 0, diff = -1, rec = -1, ca = -1; + int n_inbound = 0, n_outbound = 0; + ConfigItem_listen *listen; + ConfigItem_link *link; + SSL_CTX *ctx; + char fix[768]; + + /* Gather every server-to-server path that presents a certificate. */ + for (listen = conf_listen; listen; listen = listen->next) + { + TLSOptions *opts; + + if (listen->flag.temporary) + continue; /* block pending removal on rehash, holds stale tls_options */ + /* Only serversonly listeners. A port presents one certificate to + * everyone on it, so a listener that also serves clients can't carry the + * linking cert without handing that cert to clients too. The dual-cert + * setup therefore needs serversonly linking ports; this scoping is + * deliberate, not an oversight (widening to all server-capable listeners + * would false-warn on the common client port that isn't marked + * clientsonly). + */ + if (!(listen->options & LISTENER_SERVERSONLY)) + continue; + if (n >= (int)(sizeof(s2s)/sizeof(s2s[0]))) { capped = 1; break; } + /* A serversonly listener may also be reached plaintext and upgraded via + * STARTTLS, which still presents listener->ssl_ctx, so no LISTENER_TLS check. + */ + opts = tls_options_for_listener(listen); + if (!opts || !opts->spkifp) + continue; /* no certificate loaded, nothing to compare */ + ctx = tls_ctx_for_listener(listen); + snprintf(s2s[n].desc, sizeof(s2s[n].desc), "listen %s:%d", + listen->ip ? listen->ip : "*", listen->port); + s2s[n].trusted = ctx ? is_trusted_cert(ctx) : 0; + s2s[n].opts = opts; + s2s[n].has_own_tls_options = (listen->tls_options != NULL); + n++; + n_inbound++; + } + for (link = conf_link; link && !capped; link = link->next) + { + TLSOptions *opts; + + if (link->flag.temporary) + continue; + /* Only outgoing links that present a certificate and may be spkifp/cert- + * authenticated by the peer. Skip incoming-only, 'insecure' (plaintext) and + * password-authenticated links (our cert is not authenticated by either side). + */ + if (!link->outgoing.hostname && !link->outgoing.file) + continue; + if (link->outgoing.options & CONNECT_OUTGOING_INSECURE) + continue; + if (link->auth && link->auth->type == AUTHTYPE_PLAINTEXT) + continue; + if (n >= (int)(sizeof(s2s)/sizeof(s2s[0]))) { capped = 1; break; } + opts = tls_options_for_outgoing_link(link); + if (!opts || !opts->spkifp) + continue; /* no certificate loaded, nothing to compare */ + ctx = tls_ctx_for_outgoing_link(link); + snprintf(s2s[n].desc, sizeof(s2s[n].desc), "link %s", link->servername); + s2s[n].trusted = ctx ? is_trusted_cert(ctx) : 0; + s2s[n].opts = opts; + s2s[n].has_own_tls_options = (link->tls_options != NULL); + n++; + n_outbound++; + } + + if (capped) + unreal_log(ULOG_WARNING, "config", "SERVER_LINKING_CERT_CHECK_CAPPED", NULL, + "Too many server-to-server TLS paths; only the first $num were checked " + "for certificate consistency.", + log_data_integer("num", n)); + + if (n == 0) + return 0; + + /* Mismatch = not all certificate sets equal. Recommended cert = first non-trusted one. */ + for (i = 1; i < n; i++) + { + if (!spkifp_sets_equal(s2s[0].opts->spkifp, s2s[i].opts->spkifp)) + { + mismatch = 1; + if (diff < 0) + diff = i; + } + } + for (i = 0; i < n; i++) + { + if (!s2s[i].trusted) + { + rec = i; + break; + } + } + check_server_linking_cert_consistency_suggest_fix(rec >= 0 ? s2s[rec].opts : NULL, fix, sizeof(fix)); + + /* A mismatch only breaks linking when the same peer could be served two + * different certificates, which needs both an inbound path (a serversonly + * listener) and an outbound path (an outgoing link). If every path is the + * same direction (for example outgoing links only), each peer pins exactly + * the one certificate it sees, so differing certificates are harmless and + * we stay silent. + */ + if (mismatch && n_inbound && n_outbound) + { + if (iConf.server_linking_mixed_certificates) + return 0; /* admin acknowledged intentionally-different s2s certs */ + + if (iConf.server_linking_tls_options) + { + /* set::server-linking is configured, so the mismatch is caused by + * blocks that override it with their own tls-options. Name those + * (SPKI differs from the server-linking cert) so the admin knows + * exactly what to remove. + */ + char offenders[512]; + + offenders[0] = '\0'; + for (i = 0; i < n; i++) + { + if (iConf.server_linking_tls_options->spkifp && + !spkifp_sets_equal(s2s[i].opts->spkifp, iConf.server_linking_tls_options->spkifp)) + { + if (offenders[0]) + strlcat(offenders, ", ", sizeof(offenders)); + strlcat(offenders, s2s[i].desc, sizeof(offenders)); + strlcat(offenders, " { }", sizeof(offenders)); + } + } + unreal_log(ULOG_WARNING, "config", "SERVER_LINKING_CERT_MISMATCH", NULL, + "This server uses a different TLS certificate for incoming server " + "connections than for outgoing links ($desc1 vs $desc2). This can break " + "server linking.\nRemove tls-options from the following blocks, which " + "override set::server-linking::tls-options: $offenders", + log_data_string("desc1", s2s[0].desc), + log_data_string("desc2", s2s[diff >= 0 ? diff : 0].desc), + log_data_string("offenders", offenders[0] ? offenders : "the relevant link/listen blocks")); + } + else + { + /* set::server-linking is not configured. Adding it fixes blocks that + * use the global/default certificate, but any block with its OWN + * tls-options whose certificate differs would still override it, so + * name those so the admin removes them too. + */ + char overrides[512]; + + overrides[0] = '\0'; + for (i = 0; i < n; i++) + { + if (s2s[i].has_own_tls_options && (rec < 0 || !spkifp_sets_equal(s2s[i].opts->spkifp, s2s[rec].opts->spkifp))) + { + if (overrides[0]) + strlcat(overrides, ", ", sizeof(overrides)); + strlcat(overrides, s2s[i].desc, sizeof(overrides)); + strlcat(overrides, " { }", sizeof(overrides)); + } + } + if (overrides[0]) + unreal_log(ULOG_WARNING, "config", "SERVER_LINKING_CERT_MISMATCH", NULL, + "This server uses a different TLS certificate for incoming server " + "connections than for outgoing links ($desc1 vs $desc2). This can break " + "server linking.\nFirst, remove tls-options from the following blocks: $overrides\n" + "After that, use the same certificate(s) for all server linking:\n$fix", + log_data_string("desc1", s2s[0].desc), + log_data_string("desc2", s2s[diff >= 0 ? diff : 0].desc), + log_data_string("fix", fix), + log_data_string("overrides", overrides)); + else + unreal_log(ULOG_WARNING, "config", "SERVER_LINKING_CERT_MISMATCH", NULL, + "This server uses a different TLS certificate for incoming server " + "connections than for outgoing links ($desc1 vs $desc2). This can break " + "server linking. Use the same certificate(s) for all server linking:\n$fix", + log_data_string("desc1", s2s[0].desc), + log_data_string("desc2", s2s[diff >= 0 ? diff : 0].desc), + log_data_string("fix", fix)); + } + return 0; + } + + /* Reached when all paths use the same certificate, or when a difference was + * found but cannot break linking (all paths the same direction, see above). + * Either way, advise if any path uses a public CA cert, since those rotate + * on renewal and change the spkifp. Scan all paths, not just s2s[0]: after a + * suppressed single-direction mismatch the CA cert may be on a later path. + */ + for (i = 0; i < n; i++) + { + if (s2s[i].trusted) + { + ca = i; + break; + } + } + if (ca >= 0 && !iConf.server_linking_allow_ca_certificate) + { + unreal_log(ULOG_ADVICE, "config", "SERVER_LINKING_CA_CERTIFICATE", NULL, + "Server linking is using a publicly-trusted (CA) certificate ($desc). " + "When such a certificate is renewed it usually gets a new key, which changes " + "the spkifp and breaks linking. For server linking, use a long-lived " + "self-signed certificate instead:\n$fix\n" + "(If you reuse the same key for certificate renewals, then set " + "set::server-linking::allow-ca-certificate yes; to silence this advice.)", + log_data_string("desc", s2s[ca].desc), + log_data_string("fix", fix)); + } + return 0; +} + int server_config_test_deny_link(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) { int errors = 0; diff --git a/src/modules/setname.c b/src/modules/setname.c index 6fa190e82f..426ad6832a 100644 --- a/src/modules/setname.c +++ b/src/modules/setname.c @@ -145,7 +145,7 @@ CMD_FUNC(cmd_setname) if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",client,NULL,NULL,NULL) && ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME)))) { - banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); + banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0); return; } } else { diff --git a/src/modules/spamreport.c b/src/modules/spamreport.c index 04f4a9b88e..1903dc6b91 100644 --- a/src/modules/spamreport.c +++ b/src/modules/spamreport.c @@ -467,7 +467,7 @@ int _spamreport(Client *client, const char *ip, NameValuePrioList *details, cons NameValuePrioList *list = NULL; list = duplicate_nvplist(details); add_nvplist(&list, -1, "ip", ip); - buildvarstring_nvp(s->url, urlbuf, sizeof(urlbuf), list, BUILDVARSTRING_URLENCODE|BUILDVARSTRING_UNKNOWN_VAR_IS_EMPTY); + buildvarstring_nvp(s->url, urlbuf, sizeof(urlbuf), list, BUILDVARSTRING_URLENCODE|BUILDVARSTRING_UNKNOWN_VAR_IS_EMPTY|BUILDVARSTRING_KEEP_SPACE_FOR_EMPTY_VAR); url = urlbuf; safe_free_nvplist(list); if (s->http_method == HTTP_METHOD_POST) @@ -492,7 +492,7 @@ int _spamreport(Client *client, const char *ip, NameValuePrioList *details, cons " \n" "\n", find_nvplist(s->parameters, "staging") ? " staging='1'" : ""); - buildvarstring_nvp(fmtstring, bodybuf, sizeof(bodybuf), list, BUILDVARSTRING_XML|BUILDVARSTRING_UNKNOWN_VAR_IS_EMPTY); + buildvarstring_nvp(fmtstring, bodybuf, sizeof(bodybuf), list, BUILDVARSTRING_XML|BUILDVARSTRING_UNKNOWN_VAR_IS_EMPTY|BUILDVARSTRING_KEEP_SPACE_FOR_EMPTY_VAR); body = bodybuf; safe_free_nvplist(list); // frees all the duplicated lists add_nvplist(&headers, 0, "Content-Type", "text/xml"); diff --git a/src/modules/starttls.c b/src/modules/starttls.c index bdaee2f423..7bcfc68565 100644 --- a/src/modules/starttls.c +++ b/src/modules/starttls.c @@ -68,8 +68,8 @@ CMD_FUNC(cmd_starttls) if (!MyConnect(client) || !IsUnknown(client)) return; - ctx = client->local->listener->ssl_ctx ? client->local->listener->ssl_ctx : ctx_server; - tls_options = client->local->listener->tls_options ? client->local->listener->tls_options->options : iConf.tls_options->options; + ctx = tls_ctx_for_listener(client->local->listener); + tls_options = tls_options_for_listener(client->local->listener)->options; /* This should never happen? */ if (!ctx) diff --git a/src/modules/stats.c b/src/modules/stats.c index 63a47d6fd2..485067c541 100644 --- a/src/modules/stats.c +++ b/src/modules/stats.c @@ -565,15 +565,11 @@ int stats_port(Client *client, const char *para) } if (listener->options & LISTENER_TLS) { + TLSOptions *o = tls_options_for_listener(listener); + const char *kind = listener->tls_options ? "" : (o == iConf.server_linking_tls_options) ? "server-linking " : "default "; NameList *n, *n2; - if (listener->tls_options) - { - for (n = listener->tls_options->certificate_files, n2 = listener->tls_options->key_files; n && n2; n = n->next, n2 = n2->next) - sendtxtnumeric(client, "- using tls certificate %s + key %s", n->name, n2->name); - } else { - for (n = iConf.tls_options->certificate_files, n2 = iConf.tls_options->key_files; n && n2; n = n->next, n2 = n2->next) - sendtxtnumeric(client, "- using default tls certificate %s + key %s", n->name, n2->name); - } + for (n = o->certificate_files, n2 = o->key_files; n && n2; n = n->next, n2 = n2->next) + sendtxtnumeric(client, "- using %stls certificate %s + key %s", kind, n->name, n2->name); } } return 0; diff --git a/src/modules/tkl.c b/src/modules/tkl.c index bfcca9cfa1..57222dba4d 100644 --- a/src/modules/tkl.c +++ b/src/modules/tkl.c @@ -55,6 +55,7 @@ CMD_FUNC(cmd_eline); CMD_FUNC(cmd_spaminfo); void cmd_tkl_line(Client *client, int parc, const char *parv[], char *type); int _tkl_hash(unsigned int c); +void _tkl_hit(Client *client, TKL *tkl); char _tkl_typetochar(int type); int _tkl_chartotype(char c); char _tkl_configtypetochar(const char *name); @@ -97,7 +98,13 @@ CMD_FUNC(_cmd_tkl); int _take_action(Client *client, BanAction *action, const char *reason, long duration, int take_action_flags, int *stopped); int _match_spamfilter(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, ClientContext *clictx, TKL **rettk); int _match_spamfilter_mtags(Client *client, MessageTag *mtags, const char *cmd); +void _run_deferred_rule_only_spamfilters(Client *client); int check_special_spamfilters_present(void); +char *tkl_hits_key(TKL *tkl, char *buf, size_t len); +void config_tkl_hits_free(ModData *m); +void config_tkl_hits_snapshot(TKL *tkl); +void _config_tkl_hits_restore(void); +void _remove_config_tkls(int flag); int _join_viruschan(Client *client, TKL *tk, int type); void _spamfilter_build_user_string(char *buf, const char *nick, Client *client); int _match_user(const char *rmask, Client *client, int options); @@ -131,6 +138,7 @@ struct TKLTypeTable unsigned tkltype:1; /**< Is a type available in cmd_tkl() and friends */ unsigned exceptiontype:1; /**< Is a type available for exceptions */ unsigned needip:1; /**< When using this exempt option, only IP addresses are permitted (processed before DNS/ident lookups etc) */ + char *id_prefix; /**< Prefix for generated TKL ids, eg "G", "K", "SPAM". NULL for exempt-only options. Note that shun has "H" here while its type.letter is 's'. */ }; /** This table which defines all TKL types and TKL exception types. @@ -143,26 +151,26 @@ struct TKLTypeTable * - more? */ TKLTypeTable tkl_types[] = { - /* */ - { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1, 0 }, - { "kline", 'k', TKL_KILL, "K-Line", 1, 1, 0 }, - { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1, 1 }, - { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1, 1 }, - { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1, 0 }, - { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1, 0 }, - { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0, 0 }, - { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1, 0 }, - { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0, 0 }, - { "local-exception", 'e', TKL_EXCEPTION, "Local Exception", 1, 0, 0 }, - { "local-spamfilter", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0, 0 }, - { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1, 1 }, - { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1, 0 }, - { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1, 0 }, - { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1, 1 }, - { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1, 0 }, - { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1, 0 }, - { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1, 0 }, - { NULL, '\0', 0, NULL, 0, 0, 0 }, + /* */ + { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1, 0, "G" }, + { "kline", 'k', TKL_KILL, "K-Line", 1, 1, 0, "K" }, + { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1, 1, "Z" }, + { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1, 1, "Z" }, + { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1, 0, "SPAM" }, + { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1, 0, "Q" }, + { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0, 0, "E" }, + { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1, 0, "H" }, + { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0, 0, "Q" }, + { "local-exception", 'e', TKL_EXCEPTION, "Local Exception", 1, 0, 0, "E" }, + { "local-spamfilter", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0, 0, "SPAM" }, + { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1, 1, NULL }, + { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1, 0, NULL }, + { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1, 0, NULL }, + { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1, 1, NULL }, + { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1, 0, NULL }, + { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1, 0, NULL }, + { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1, 0, NULL }, + { NULL, '\0', 0, NULL, 0, 0, 0, NULL }, }; #define ALL_VALID_EXCEPTION_TYPES "kline, gline, zline, gzline, spamfilter, shun, qline, blacklist, connect-flood, handshake-data-flood, antirandom, antimixedutf8, ban-version" @@ -175,6 +183,46 @@ int confusables_spamfilters_present = 0; /**< Are any spamfilters with input-con long previous_spamfilter_utf8 = 0; static int firstboot = 0; +/* Config-file (and central) TKLs are freed and re-created on every /REHASH (and + * central spamfilters on every feed refresh) since they live in the config, not + * in the tkldb, which would otherwise reset their hit counters. To keep the + * counters we stash them here right before the old entry is freed, keyed by a + * stable per-TKL identity (see tkl_hits_key), and copy them back onto the freshly + * re-added entry with the same key. The list is carried across the rehash + * module-unload by Save/LoadPersistentPointer (same trick connthrottle uses). + * This is rehash/refresh-only; on a full restart the counters still reset. + * Covers config server bans, name bans (qlines) and spamfilters; not exceptions. + */ +typedef struct ConfigTKLHits ConfigTKLHits; +struct ConfigTKLHits { + ConfigTKLHits *prev, *next; + char key[256]; + long long hits; + time_t lasthit; + long long hits_except; /* spamfilter only */ + time_t lasthit_except; /* spamfilter only */ +}; +ConfigTKLHits *config_tkl_hits = NULL; + +/* s2s-tkl/: per-TKL fields carried over S2S as @s2s-tkl/= on the + * TKL command (modeled on s2s-md/). Old servers ignore unknown tags. To add a field, + * add a row here plus its serialize/unserialize pair (defined further down). + */ +typedef struct { + const char *name; + const char *(*serialize)(TKL *tkl); /**< value to send, or NULL to omit the tag */ + void (*unserialize)(TKL *tkl, const char *value); +} TKLS2SField; +static const char *tkl_s2s_get_id(TKL *tkl); +static void tkl_s2s_set_id(TKL *tkl, const char *value); +static const char *tkl_s2s_get_spamfilter_id(TKL *tkl); +static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value); +static const TKLS2SField tkl_s2s_fields[] = { + { "id", tkl_s2s_get_id, tkl_s2s_set_id }, + { "spamfilter_id", tkl_s2s_get_spamfilter_id, tkl_s2s_set_spamfilter_id }, + { NULL, NULL, NULL }, +}; + MOD_TEST() { MARK_AS_OFFICIAL_MODULE(modinfo); @@ -203,6 +251,9 @@ MOD_TEST() EfunctionAddVoid(modinfo->handle, EFUNC_FREE_TKL, _free_tkl); EfunctionAddVoid(modinfo->handle, EFUNC_TKL_CHECK_LOCAL_REMOVE_SHUN, _tkl_check_local_remove_shun); EfunctionAdd(modinfo->handle, EFUNC_FIND_TKLINE_MATCH, _find_tkline_match); + EfunctionAddVoid(modinfo->handle, EFUNC_TKL_HIT, _tkl_hit); + EfunctionAddVoid(modinfo->handle, EFUNC_REMOVE_CONFIG_TKLS, _remove_config_tkls); + EfunctionAddVoid(modinfo->handle, EFUNC_CONFIG_TKL_HITS_RESTORE, _config_tkl_hits_restore); EfunctionAdd(modinfo->handle, EFUNC_FIND_SHUN, _find_shun); EfunctionAdd(modinfo->handle, EFUNC_FIND_SPAMFILTER_USER, _find_spamfilter_user); EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_QLINE, TO_PVOIDFUNC(_find_qline)); @@ -217,6 +268,7 @@ MOD_TEST() EfunctionAdd(modinfo->handle, EFUNC_TAKE_ACTION, _take_action); EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER, _match_spamfilter); EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER_MTAGS, _match_spamfilter_mtags); + EfunctionAddVoid(modinfo->handle, EFUNC_RUN_DEFERRED_RULE_ONLY_SPAMFILTERS, _run_deferred_rule_only_spamfilters); EfunctionAdd(modinfo->handle, EFUNC_JOIN_VIRUSCHAN, _join_viruschan); EfunctionAddVoid(modinfo->handle, EFUNC_SPAMFILTER_BUILD_USER_STRING, _spamfilter_build_user_string); EfunctionAdd(modinfo->handle, EFUNC_MATCH_USER, _match_user); @@ -239,6 +291,7 @@ MOD_INIT() if (loop.booted == 0) firstboot = 1; LoadPersistentLong(modinfo, previous_spamfilter_utf8); + LoadPersistentPointer(modinfo, config_tkl_hits, config_tkl_hits_free); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_spamfilter); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_ban); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_except); @@ -264,13 +317,21 @@ MOD_LOAD() { check_special_spamfilters_present(); check_set_spamfilter_utf8_setting_changed(); + _config_tkl_hits_restore(); EventAdd(modinfo->handle, "tklexpire", tkl_check_expire, NULL, 5000, 0); return MOD_SUCCESS; } MOD_UNLOAD() { + /* Free our config TKLs here (rather than from config_rehash) so the same pass + * can snapshot the spamfilter hit counters. Call the local impl: the snapshot + * store is a per-instance global, and we must write to ours (saved just below), + * not the new instance the efunc pointer now points to. + */ + _remove_config_tkls(TKL_FLAG_CONFIG); SavePersistentLong(modinfo, previous_spamfilter_utf8); + SavePersistentPointer(modinfo, config_tkl_hits); return MOD_SUCCESS; } @@ -320,7 +381,7 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e { config_error("%s:%i: spamfilter::id invalid: maximum size (%d chars) exceeded " "or forbidden characters encountered: only A-Z, 0-9 and _ are permitted.", - cep->file->filename, cep->line_number, MAXSPAMFILTERIDLEN); + cep->file->filename, cep->line_number, TKLIDLEN-1); errors++; } } else @@ -413,6 +474,10 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e has_action = 1; errors += test_ban_action_config(cep); } + 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, @@ -517,10 +582,6 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e errors++; } } - else if (!strcmp(cep->name, "except")) - { - test_match_block(cf, cep, &errors); - } else { config_error_unknown(cep->file->filename, cep->line_number, @@ -533,7 +594,7 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e if (match && match_type) { Match *m; - char *err; + const char *err; m = unreal_create_match(match_type, match, &err); if (!m) @@ -733,7 +794,7 @@ int tkl_config_run_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type) if (match) { - char *err; + const char *err; m = unreal_create_match(match_type, match, &err); if (!m) { @@ -1227,7 +1288,7 @@ void recompile_spamfilters(void) { TKL *tkl; Match *m; - char *err; + const char *err; int converted = 0; int index; @@ -1272,37 +1333,262 @@ void check_set_spamfilter_utf8_setting_changed(void) previous_spamfilter_utf8 = iConf.spamfilter_utf8; } -/** Return unique spamfilter id for TKL */ -char *spamfilter_id(TKL *tk) +/* === TKL unique IDs === + * Every TKL typically has a unique id (tkl->id), such as "G7K2MP9WQX3" for a gline. + * See the comment below about TKL_GENERATED_ID_LEN for more info. + * These TKLIDs are communicated in S2S via @s2s-tkl/id mtag, and they + * also persist via tkldb. + */ + +/* The generated TKLID (not to be confused with the maximum allowed length of TKLID): + * This consists of the prefix + the random/hashed base32 chars. Obviously, the idea + * is that this will never clash, so we have to look at birthday attack collision rates: + * - Spamfilter: "SPAM" prefix + 7 chars = 35 bits = ~0.0015% for 1000 spamfilters. + * - All the rest: 1 letter prefix + 10 chars = 50 bits = ~0.0018% for 200k entries + * And obviously both the 1000 spamfilters and 200k GLINE/whatever are a rather + * absurd number of entries, but possible in odd/attack scenarios. + */ +#define TKL_GENERATED_ID_LEN 11 + +/** The TKL ID prefix string. Eg. "G" for a gline or "SPAM" for a spamfilter. */ +static const char *tkl_id_prefix(int type) { - static char buf[128]; + int i; + + for (i = 0; tkl_types[i].config_name; i++) + if ((tkl_types[i].type == type) && tkl_types[i].id_prefix) + return tkl_types[i].id_prefix; +#ifdef DEBUGMODE + abort(); /* this should never happen */ +#endif + return "?"; +} + +/** Find a TKL by its exact id (case-insensitive). Returns NULL on no match or empty id. */ +static TKL *find_tkl_by_id(const char *id) +{ + TKL *tkl; + int index, index2; + + if (BadPtr(id)) + return NULL; + + for (index = 0; index < TKLIPHASHLEN1; index++) + for (index2 = 0; index2 < TKLIPHASHLEN2; index2++) + for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next) + if (!strcasecmp(tkl->id, id)) + return tkl; + + for (index = 0; index < TKLISTLEN; index++) + for (tkl = tklines[index]; tkl; tkl = tkl->next) + if (!strcasecmp(tkl->id, id)) + return tkl; + + return NULL; +} + +/** Assign a fresh, locally-unique random id to 'tkl' (overwrites tkl->id). + * Called when this server is the origin of the entry. + */ +static void tkl_generate_id(TKL *tkl) +{ + /* Crockford base32 (no I/L/O/U) so the id survives being read aloud */ + static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + const char *prefix = tkl_id_prefix(tkl->type); + int prefixlen = strlen(prefix); + int nrandom = TKL_GENERATED_ID_LEN - prefixlen; + TKL *other; + int attempt, i; + + for (attempt = 0; attempt < 16; attempt++) + { + strlcpy(tkl->id, prefix, sizeof(tkl->id)); + for (i = 0; i < nrandom; i++) + tkl->id[prefixlen + i] = b32[getrandom8() % 32]; + tkl->id[prefixlen + nrandom] = '\0'; + other = find_tkl_by_id(tkl->id); + if (!other || (other == tkl)) + return; /* unique (or only matches ourselves, if already linked) */ + } + /* 16 collisions in a row is astronomically unlikely; keep the last candidate. */ +} + +/* s2s-tkl/xxx message-tag field handlers */ + +static const char *tkl_s2s_get_id(TKL *tkl) +{ + return tkl->id[0] ? tkl->id : NULL; +} + +static void tkl_s2s_set_id(TKL *tkl, const char *value) +{ + if (!BadPtr(value) && (strlen(value) < sizeof(tkl->id))) + strlcpy(tkl->id, value, sizeof(tkl->id)); +} + +static const char *tkl_s2s_get_spamfilter_id(TKL *tkl) +{ + return tkl->spamfilter_id[0] ? tkl->spamfilter_id : NULL; +} + +static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value) +{ + if (!BadPtr(value) && (strlen(value) < sizeof(tkl->spamfilter_id))) + strlcpy(tkl->spamfilter_id, value, sizeof(tkl->spamfilter_id)); +} + +/** Append s2s-tkl/ message tags describing 'tkl' onto '*mtags' (for sending over S2S). */ +static void tkl_add_s2s_mtags(TKL *tkl, MessageTag **mtags) +{ + const TKLS2SField *f; + const char *value; + MessageTag *m; + char name[64]; + + for (f = tkl_s2s_fields; f->name; f++) + { + value = f->serialize(tkl); + if (!value) + continue; + snprintf(name, sizeof(name), "s2s-tkl/%s", f->name); + m = safe_alloc(sizeof(MessageTag)); + safe_strdup(m->name, name); + safe_strdup(m->value, value); + AddListItem(m, *mtags); + } +} + +/** Build a one-element s2s-tkl/spamfilter_id message tag from 'spamfilter_id', or NULL if it is empty. + * Lets a locally-created server ban carry its spamfilter id through the same path as a remote one. + */ +static MessageTag *tkl_spamfilter_id_mtag(const char *spamfilter_id) +{ + MessageTag *m; - snprintf(buf, sizeof(buf), "%p", (void *)tk); + if (BadPtr(spamfilter_id)) + return NULL; + m = safe_alloc(sizeof(MessageTag)); + safe_strdup(m->name, "s2s-tkl/spamfilter_id"); + safe_strdup(m->value, spamfilter_id); + return m; +} + +/** Read s2s-tkl/ message tags from 'mtags' into 'tkl' (on receiving an S2S TKL command). */ +static void tkl_extract_s2s_mtags(TKL *tkl, MessageTag *mtags) +{ + MessageTag *m; + const TKLS2SField *f; + + for (m = mtags; m; m = m->next) + { + if (strncmp(m->name, "s2s-tkl/", 8)) + continue; + for (f = tkl_s2s_fields; f->name; f++) + { + if (!strcmp(f->name, m->name + 8)) + { + f->unserialize(tkl, m->value); + break; + } + } + } +} + +/** Return the value of the s2s-tkl/ message tag in 'mtags', or NULL if absent. + * Used by the merge logic to peek at an incoming field without overwriting the existing entry. + */ +static const char *tkl_s2s_mtag_value(MessageTag *mtags, const char *field) +{ + MessageTag *m; + char name[64]; + + snprintf(name, sizeof(name), "s2s-tkl/%s", field); + for (m = mtags; m; m = m->next) + if (!strcmp(m->name, name)) + return m->value; + return NULL; +} + +/** A fallback spamfilter id (not the normal randomly generated ID). + * This ID is a hash based on (target, action, match method+string). So when those + * properties are the same, it will create the same ID. Used for: + * - Config spamfilter or Central Spamfilter without an explicit id. + * For these it behaves very much like a regular tkl->id, is shown in STATS etc. + * These entries never propagate servers, as they are local, but it is still nice + * for the ID to be unique on each server if you use the exact same spamfilter + * (think remote includes or central spamfilter). + * - Global spamfilter with a blank id (eg from an older server). + * In this case it is ONLY used for '/SPAMFILTER del' and not in regular STATS + * or used as a true id. + * And to be clear: for both cases they never appear in S2S and not on disk either + * (it is computed, each time). + */ +static const char *spamfilter_fallback_id(TKL *tkl) +{ + static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + /* Fixed, non-secret key: the fallback id is opers-only and not security-sensitive, the + * key just makes the hash deterministic and identical across servers. */ + static const char key[16] = "UnrealIRCd rocks"; + static char buf[16]; + const char *prefix; + int prefixlen; + char content[8192]; + char actbuf[2]; + uint64_t h; + int i; + + actbuf[0] = banact_valtochar(tkl->ptr.spamfilter->action->action); + actbuf[1] = '\0'; + snprintf(content, sizeof(content), "%s\t%s\t%s\t%s", + spamfilter_target_inttostring(tkl->ptr.spamfilter->target), + actbuf, + unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type), + tkl->ptr.spamfilter->match->str); + + h = siphash(content, key); + + prefix = tkl_id_prefix(tkl->type); + prefixlen = strlen(prefix); + strlcpy(buf, prefix, sizeof(buf)); + for (i = 0; i < TKL_GENERATED_ID_LEN - prefixlen; i++) + { + buf[prefixlen + i] = b32[h & 31]; + h >>= 5; + } + buf[prefixlen + i] = '\0'; return buf; } -/** Record that this TKL fired locally: bump hits and stamp lasthit. - * /STATS gline and the upstream test rely on these being non-zero - * after a rejection. Counters are local-only -- not synced over s2s. */ -static inline void tkl_record_hit(TKL *tkl) +/** Called when a TKL is enforced against a client: bump its hit counter and + * remember when. Call this while 'client' is still valid (before the client is + * exited), so it stays safe if we later add a hook here. + */ +void _tkl_hit(Client *client, TKL *tkl) { - if (!tkl) - return; tkl->hits++; tkl->lasthit = TStime(); } -/** Build " [ID: ]" into buf, or just "" if the TKL has no id. - * The id is appended to the reject-message so the client can quote it back to - * an oper when reporting "why am I banned" -- matches upstream and the - * unrealircd-tests opercommands/tkl assertions. +/* Warn opers when a spamfilter regex could not finish (eg. it hit the + * PCRE2 match or depth limit). The match is treated as no-match, so we + * only warn and do not remove the spamfilter. */ -static const char *tkl_reason_with_id(TKL *tkl, char *buf, size_t buflen, const char *reason) +static void spamfilter_regex_error(TKL *tkl, const char *regex_error) { - if (!tkl || !tkl->id[0]) - return reason; - snprintf(buf, buflen, "%s [ID: %s]", reason ? reason : "", tkl->id); - return buf; + if (tkl->type & TKL_GLOBAL) + { + unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, + "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? " + "To delete, use: /SPAMFILTER del $tkl.id", + log_data_string("regex_error", regex_error), + log_data_tkl("tkl", tkl)); + } else { + unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, + "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? " + "To remove it, edit your config file", + log_data_string("regex_error", regex_error), + log_data_tkl("tkl", tkl)); + } } int tkl_ip_change(Client *client, const char *oldip) @@ -1310,11 +1596,8 @@ int tkl_ip_change(Client *client, const char *oldip) TKL *tkl; if ((tkl = find_tkline_match_zap(client))) { - char rbuf[512]; - tkl_record_hit(tkl); - - const char *r = tkl_reason_with_id(tkl, rbuf, sizeof(rbuf), tkl->ptr.serverban->reason); - banned_client(client, "Z-Lined", r, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); + tkl_hit(client, tkl); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); } return 0; } @@ -1324,11 +1607,8 @@ int tkl_accept(Client *client) TKL *tkl; if ((tkl = find_tkline_match_zap(client))) { - char rbuf[512]; - tkl_record_hit(tkl); - - const char *r = tkl_reason_with_id(tkl, rbuf, sizeof(rbuf), tkl->ptr.serverban->reason); - banned_client(client, "Z-Lined", r, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); + tkl_hit(client, tkl); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); return 2; // TODO: HOOK_DENY_ALWAYS; } return 0; @@ -2442,7 +2722,8 @@ void spamfilter_del_by_id(Client *client, const char *id) { int index; TKL *tk; - int found = 0; + int matches = 0; + TKL *match = NULL; char mo[32], mo2[32]; const char *tkllayer[13] = { me.name, /* 0 server.name */ @@ -2464,21 +2745,30 @@ void spamfilter_del_by_id(Client *client, const char *id) { for (tk = tklines[index]; tk; tk = tk->next) { - if (((tk->type & (TKL_GLOBAL|TKL_SPAMF)) == (TKL_GLOBAL|TKL_SPAMF)) && !strcmp(spamfilter_id(tk), id)) + if ((tk->type & (TKL_GLOBAL|TKL_SPAMF)) != (TKL_GLOBAL|TKL_SPAMF)) + continue; + /* Match the real id, or for an id-less spamfilter (eg from an old server) + * its locally-computed fallback handle. + */ + if (tk->id[0] ? !strcmp(tk->id, id) : !strcmp(spamfilter_fallback_id(tk), id)) { - found = 1; - break; + match = tk; + matches++; } } - if (found) - break; /* break outer loop */ } - if (!tk) + if (matches == 0) { sendnotice(client, "Sorry, no spamfilter found with that ID. Did you run '/spamfilter del' to get the appropriate id?"); return; } + if (matches > 1) + { + sendnotice(client, "That ID is ambiguous (it matches multiple spamfilters). Please remove the spamfilter by its full definition instead."); + return; + } + tk = match; /* Spamfilter found. Now fill the tkllayer */ tkllayer[1] = "-"; @@ -2528,7 +2818,7 @@ CMD_FUNC(cmd_spamfilter) int n; Match *m; int match_type = 0; - char *err = NULL; + const char *err = NULL; if (IsServer(client)) return; @@ -2714,106 +3004,6 @@ int _tkl_hash(unsigned int c) #endif } -/** Generate (and store) a deterministic id on this TKL. - * - * Layout: one uppercase letter from tkl_typetochar() + 10 Crockford - * base32 chars (no I/L/O/U so people don't misread them when reading - * the id aloud). The base32 body is the first 50 bits of SHA1 over - * stable, network-identical TKL attributes (set_by, set_at, and the - * union's discriminating fields), which means every server on the - * network independently computes the same id for the same TKL -- - * no s2s message-tag plumbing required. - * - * The id is exposed in /STATS and in the rejected client's reject - * message so a user can quote it back to an oper. - */ -static void tkl_generate_id(TKL *tkl) -{ - static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; - unsigned char sum[EVP_MAX_MD_SIZE]; - unsigned int sumlen = 0; - char buf[1024]; - char prefix; - int i; - - if (!tkl) - return; - prefix = _tkl_typetochar(tkl->type); - if (prefix == 0) - prefix = 'X'; - - /* Hash stable, network-identical attributes only. The union - * payload differs by ban kind so we walk it discriminately; - * pointer addresses or local hash buckets must never appear. - */ - if (TKLIsServerBan(tkl) && tkl->ptr.serverban) - { - snprintf(buf, sizeof(buf), "S\x1e%s\x1e%lld\x1e%s\x1e%s\x1e%s\x1e%u", - tkl->set_by ? tkl->set_by : "", - (long long)tkl->set_at, - tkl->ptr.serverban->usermask ? tkl->ptr.serverban->usermask : "", - tkl->ptr.serverban->hostmask ? tkl->ptr.serverban->hostmask : "", - tkl->ptr.serverban->reason ? tkl->ptr.serverban->reason : "", - tkl->type); - } - else if (TKLIsSpamfilter(tkl) && tkl->ptr.spamfilter && tkl->ptr.spamfilter->match) - { - snprintf(buf, sizeof(buf), "F\x1e%s\x1e%lld\x1e%u\x1e%s\x1e%s", - tkl->set_by ? tkl->set_by : "", - (long long)tkl->set_at, - tkl->ptr.spamfilter->target, - ban_actions_to_string(tkl->ptr.spamfilter->action), - tkl->ptr.spamfilter->match->str ? tkl->ptr.spamfilter->match->str : ""); - } - else if (TKLIsBanException(tkl) && tkl->ptr.banexception) - { - snprintf(buf, sizeof(buf), "E\x1e%s\x1e%lld\x1e%s\x1e%s\x1e%s", - tkl->set_by ? tkl->set_by : "", - (long long)tkl->set_at, - tkl->ptr.banexception->usermask ? tkl->ptr.banexception->usermask : "", - tkl->ptr.banexception->hostmask ? tkl->ptr.banexception->hostmask : "", - tkl->ptr.banexception->bantypes ? tkl->ptr.banexception->bantypes : ""); - } - else if (TKLIsNameBan(tkl) && tkl->ptr.nameban) - { - snprintf(buf, sizeof(buf), "N\x1e%s\x1e%lld\x1e%s", - tkl->set_by ? tkl->set_by : "", - (long long)tkl->set_at, - tkl->ptr.nameban->name ? tkl->ptr.nameban->name : ""); - } - else - { - /* Unknown discriminator: fall back to set_by + set_at; still - * deterministic, just lower entropy. */ - snprintf(buf, sizeof(buf), "?\x1e%s\x1e%lld", - tkl->set_by ? tkl->set_by : "", - (long long)tkl->set_at); - } - - if (!EVP_Digest(buf, strlen(buf), sum, &sumlen, EVP_sha1(), NULL) || sumlen < 7) - { - /* Fall back to random if hashing somehow fails. */ - tkl->id[0] = (char)toupper((unsigned char)prefix); - for (i = 1; i <= 10; i++) - tkl->id[i] = b32[getrandom8() % 32]; - tkl->id[i] = '\0'; - return; - } - - tkl->id[0] = (char)toupper((unsigned char)prefix); - /* Pack the first 50 bits of the hash into 10 base32 chars. */ - for (i = 0; i < 10; i++) - { - int bit = i * 5; - int byte = bit / 8; - int shift = 11 - (bit % 8) - 5; /* bits left in the next 2 bytes after `bit` */ - unsigned int v = (sum[byte] << 8) | sum[byte + 1]; - v = (v >> shift) & 0x1f; - tkl->id[1 + i] = b32[v]; - } - tkl->id[11] = '\0'; -} - /** tkl type to tkl character. * NOTE: type is assumed to be valid. */ @@ -3086,7 +3276,6 @@ TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAct tkl->set_at = set_at; safe_strdup(tkl->set_by, set_by); tkl->expire_at = expire_at; - tkl_generate_id(tkl); tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter)); if (rule) { @@ -3112,6 +3301,19 @@ TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAct safe_strdup(tkl->ptr.spamfilter->id, id); tkl->ptr.spamfilter->show_message_content_on_hit = show_message_content_on_hit; + /* Config (and central) spamfilters are local per server, so they never receive + * a random network id. Give them a deterministic id instead, identical on every + * server: the config/central id label if set, otherwise a content hash. This is + * what lets a central spamfilter be referenced by the same id on every server. + */ + if (flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER)) + { + if (!BadPtr(id)) + strlcpy(tkl->id, id, sizeof(tkl->id)); + else + strlcpy(tkl->id, spamfilter_fallback_id(tkl), sizeof(tkl->id)); + } + if (tkl->ptr.spamfilter->target & SPAMF_USER) loop.do_bancheck_spamf_user = 1; if (tkl->ptr.spamfilter->target & SPAMF_AWAY) @@ -3167,7 +3369,6 @@ TKL *_tkl_add_serverban(int type, const char *usermask, const char *hostmask, Se tkl->set_at = set_at; safe_strdup(tkl->set_by, set_by); tkl->expire_at = expire_at; - tkl_generate_id(tkl); /* Now the server ban fields */ tkl->ptr.serverban = safe_alloc(sizeof(ServerBan)); if (soft) @@ -3236,7 +3437,6 @@ TKL *_tkl_add_banexception(int type, const char *usermask, const char *hostmask, tkl->set_at = set_at; safe_strdup(tkl->set_by, set_by); tkl->expire_at = expire_at; - tkl_generate_id(tkl); /* Now the ban except fields */ tkl->ptr.banexception = safe_alloc(sizeof(BanException)); safe_strdup(tkl->ptr.banexception->usermask, usermask); @@ -3298,7 +3498,6 @@ TKL *_tkl_add_nameban(int type, const char *name, int hold, const char *reason, tkl->set_at = set_at; safe_strdup(tkl->set_by, set_by); tkl->expire_at = expire_at; - tkl_generate_id(tkl); /* Now the name ban fields */ tkl->ptr.nameban = safe_alloc(sizeof(ServerBan)); safe_strdup(tkl->ptr.nameban->name, name); @@ -3363,6 +3562,176 @@ void _free_tkl(TKL *tkl) safe_free(tkl); } +/** Build a stable match key for a hit-bearing config/central TKL (server ban, + * name ban or spamfilter), used to pair an old entry with its rebuilt self across + * a rehash / feed refresh. Returns buf, or NULL for types we don't preserve + * (exceptions) or with no usable key. Format: " ", joined + * by a single space (these fields never contain spaces). + */ +char *tkl_hits_key(TKL *tkl, char *buf, size_t len) +{ + char tmp[BUFSIZE]; + char t = tkl_typetochar(tkl->type); + + if (TKLIsSpamfilter(tkl) && tkl->id[0]) + snprintf(buf, len, "%c %s", t, tkl->id); + else if (TKLIsServerBan(tkl)) + snprintf(buf, len, "%c %s", t, tkl_uhost(tkl, tmp, sizeof(tmp), 0)); + else if (TKLIsNameBan(tkl)) + snprintf(buf, len, "%c %s", t, tkl->ptr.nameban->name); + else + return NULL; + return buf; +} + +/** Stash a config/central TKL's hit counters before it is freed (from the removal + * loop in _remove_config_tkls), so they can be copied back onto the same entry + * after a rehash / feed refresh re-adds it. Covers server bans, name bans and + * spamfilters (whatever tkl_hits_key recognises); skips entries that never hit. + */ +void config_tkl_hits_snapshot(TKL *tkl) +{ + ConfigTKLHits *e; + char key[256]; + + if (!(tkl->flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER))) + return; + if (!tkl_hits_key(tkl, key, sizeof(key))) + return; /* not a hit-bearing config type */ + if (!tkl->hits && !tkl->lasthit && + (!TKLIsSpamfilter(tkl) || + (!tkl->ptr.spamfilter->hits_except && !tkl->ptr.spamfilter->lasthit_except))) + return; /* never hit, nothing to keep */ + + e = safe_alloc(sizeof(ConfigTKLHits)); + strlcpy(e->key, key, sizeof(e->key)); + e->hits = tkl->hits; + e->lasthit = tkl->lasthit; + if (TKLIsSpamfilter(tkl)) + { + e->hits_except = tkl->ptr.spamfilter->hits_except; + e->lasthit_except = tkl->ptr.spamfilter->lasthit_except; + } + AddListItem(e, config_tkl_hits); +} + +/** Free the whole snapshot list and forget it. */ +void config_tkl_hits_free_all(void) +{ + ConfigTKLHits *e, *e_next; + + for (e = config_tkl_hits; e; e = e_next) + { + e_next = e->next; + safe_free(e); + } + config_tkl_hits = NULL; +} + +/** PersistentPointer free callback: used only if the snapshot is never reloaded + * (e.g. final module unload), otherwise MOD_LOAD consumes and frees it. + */ +void config_tkl_hits_free(ModData *m) +{ + config_tkl_hits_free_all(); +} + +/** Copy stashed counters back onto one re-added TKL, matched by key. */ +static void config_tkl_hits_restore_one(TKL *tkl) +{ + ConfigTKLHits *e; + char key[256]; + + if (!(tkl->flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER))) + return; + if (!tkl_hits_key(tkl, key, sizeof(key))) + return; + for (e = config_tkl_hits; e; e = e->next) + { + if (!strcmp(e->key, key)) + { + tkl->hits = e->hits; + tkl->lasthit = e->lasthit; + if (TKLIsSpamfilter(tkl)) + { + tkl->ptr.spamfilter->hits_except = e->hits_except; + tkl->ptr.spamfilter->lasthit_except = e->lasthit_except; + } + return; + } + } +} + +/** After config/central TKLs have been re-added, copy the stashed hit counters + * back onto the ones with a matching key, then drop the whole stash. Called from + * MOD_LOAD (config rehash) and from the central feed refresh; a no-op on a normal + * boot (the stash is empty). + */ +void _config_tkl_hits_restore(void) +{ + TKL *tkl; + int index, index2; + + if (!config_tkl_hits) + return; + + /* IP-hashed list (zlines, klines, glines) */ + for (index = 0; index < TKLIPHASHLEN1; index++) + for (index2 = 0; index2 < TKLIPHASHLEN2; index2++) + for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next) + config_tkl_hits_restore_one(tkl); + + /* Generic list (name bans, spamfilters, non-ip-hashed server bans) */ + for (index = 0; index < TKLISTLEN; index++) + for (tkl = tklines[index]; tkl; tkl = tkl->next) + config_tkl_hits_restore_one(tkl); + + /* Matched entries are applied; entries no longer in the config are discarded. */ + config_tkl_hits_free_all(); +} + +/** Remove all TKLs with the given flag (TKL_FLAG_CONFIG or + * TKL_FLAG_CENTRAL_SPAMFILTER). For config/central spamfilters the hit counters + * are snapshotted first, so a later re-add can restore them. Lives here (rather + * than in conf.c) so the snapshot sits right next to the removal it rides on. + */ +void _remove_config_tkls(int flag) +{ + TKL *tk, *tk_next; + int index, index2; + + /* IP hashed TKL list */ + for (index = 0; index < TKLIPHASHLEN1; index++) + { + for (index2 = 0; index2 < TKLIPHASHLEN2; index2++) + { + for (tk = tklines_ip_hash[index][index2]; tk; tk = tk_next) + { + tk_next = tk->next; + if (tk->flags & flag) + { + config_tkl_hits_snapshot(tk); + tkl_del_line(tk); + } + } + } + } + + /* Generic TKL list */ + for (index = 0; index < TKLISTLEN; index++) + { + for (tk = tklines[index]; tk; tk = tk_next) + { + tk_next = tk->next; + if (tk->flags & flag) + { + config_tkl_hits_snapshot(tk); + tkl_del_line(tk); + } + } + } +} + /** Delete a TKL entry from the list and free it. * @param tkl The TKL entry. */ @@ -3549,23 +3918,27 @@ void tkl_expire_entry(TKL *tkl) if (TKLIsServerBan(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL, - "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id $spamfilter_id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id)); } else if (TKLIsNameBan(tkl)) { if (!tkl->ptr.nameban->hold) { unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL, - "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id)); } } else if (TKLIsBanException(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL, - "Expiring $tkl.type_string '$tkl' [type: $tkl.exception_types] [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "Expiring $tkl.type_string '$tkl' [type: $tkl.exception_types] [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id)); } // FIXME: so.. this isn't logged? or what? @@ -3795,25 +4168,19 @@ int _find_tkline_match(Client *client, int skip_soft) if (tkl->type & TKL_KILL) { - char rbuf[512]; - tkl_record_hit(tkl); - - const char *r = tkl_reason_with_id(tkl, rbuf, sizeof(rbuf), tkl->ptr.serverban->reason); ircstats.is_ref++; + tkl_hit(client, tkl); if (tkl->type & TKL_GLOBAL) - banned_client(client, "G-Lined", r, 1, 0); + banned_client(client, "G-Lined", tkl->ptr.serverban->reason, tkl->id, 1, 0); else - banned_client(client, "K-Lined", r, 0, 0); + banned_client(client, "K-Lined", tkl->ptr.serverban->reason, tkl->id, 0, 0); return 1; /* killed */ } else if (tkl->type & TKL_ZAP) { - char rbuf[512]; - tkl_record_hit(tkl); - - const char *r = tkl_reason_with_id(tkl, rbuf, sizeof(rbuf), tkl->ptr.serverban->reason); ircstats.is_ref++; - banned_client(client, "Z-Lined", r, (tkl->type & TKL_GLOBAL)?1:0, 0); + tkl_hit(client, tkl); + banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, 0); return 1; /* killed */ } @@ -3855,6 +4222,7 @@ int _find_shun(Client *client) /* Found match. Now check for exception... */ if (find_tkl_exception(TKL_SHUN, client)) return 0; + tkl_hit(client, tkl); SetShunned(client); return 1; } @@ -3923,16 +4291,23 @@ int spamfilter_check_users(TKL *tkl) { if (MyUser(client)) { + const char *regex_error = NULL; + spamfilter_build_user_string(spamfilter_user, client->name, client); - if (!unreal_match(tkl->ptr.spamfilter->match, spamfilter_user)) + if (!unreal_match(tkl->ptr.spamfilter->match, spamfilter_user, ®ex_error)) + { + if (regex_error) + spamfilter_regex_error(tkl, regex_error); continue; /* No match */ + } /* matched! */ unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client, - "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]", + "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id", log_data_tkl("tkl", tkl), log_data_string("command", "USER"), - log_data_string("str", spamfilter_user)); + log_data_string("str", spamfilter_user), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id)); RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, spamfilter_user, spamfilter_user, SPAMF_USER, NULL, tkl); matches++; @@ -4049,12 +4424,15 @@ TKL *_find_tkline_match_zap(Client *client) #define NOT_BY_REASON 0x8 #define BY_SETBY 0x10 #define NOT_BY_SETBY 0x20 +#define BY_ID 0x40 +#define NOT_BY_ID 0x80 typedef struct { int flags; const char *mask; const char *reason; const char *set_by; + const char *id; } TKLFlag; /** Parse STATS tkl parameters. @@ -4109,6 +4487,15 @@ static void parse_stats_params(const char *para, TKLFlag *flag) flag->flags |= NOT_BY_SETBY; flag->set_by = tmp; break; + case 'i': + if (flag->id || !(tmp = strtok(NULL, " "))) + continue; + if (what == '+') + flag->flags |= BY_ID; + else + flag->flags |= NOT_BY_ID; + flag->id = tmp; + break; } } } @@ -4118,6 +4505,9 @@ static void parse_stats_params(const char *para, TKLFlag *flag) */ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklflags, TKL *tkl) { + const char *id_str = tkl->id[0] ? tkl->id : "-"; + const char *spamfilter_id_str = tkl->spamfilter_id[0] ? tkl->spamfilter_id : "-"; + /***** First, handle the selection ******/ if (!BadPtr(para)) @@ -4128,6 +4518,12 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl if (tklflags->flags & NOT_BY_SETBY) if (match_simple(tklflags->set_by, tkl->set_by)) return 0; + if (tklflags->flags & BY_ID) + if (!match_simple(tklflags->id, tkl->id)) + return 0; + if (tklflags->flags & NOT_BY_ID) + if (match_simple(tklflags->id, tkl->id)) + return 0; if (TKLIsServerBan(tkl)) { if (tklflags->flags & BY_MASK) @@ -4198,9 +4594,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl { sendnumeric(client, RPL_STATSGLINE, 'K', namevalue_nospaces(m), (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } } else { @@ -4210,41 +4604,31 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl { sendnumeric(client, RPL_STATSGLINE, 'G', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_ZAP | TKL_GLOBAL)) { sendnumeric(client, RPL_STATSGLINE, 'Z', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_SHUN | TKL_GLOBAL)) { sendnumeric(client, RPL_STATSGLINE, 's', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_KILL)) { sendnumeric(client, RPL_STATSGLINE, 'K', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } else if (tkl->type == (TKL_ZAP)) { sendnumeric(client, RPL_STATSGLINE, 'z', uhost, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, - (long long)(TStime() - tkl->set_at), tkl->set_by, - tkl->hits, (long long)tkl->lasthit, "*", tkl->id[0] ? tkl->id : "*", - tkl->ptr.serverban->reason); + (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason); } } } else @@ -4260,28 +4644,19 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl (long long)tkl->ptr.spamfilter->tkl_duration, tkl->ptr.spamfilter->tkl_reason, tkl->set_by, - tkl->ptr.spamfilter->hits, + tkl->hits, tkl->ptr.spamfilter->hits_except, - /* lasthit / lasthit_except: upstream added these to TKL + - * Spamfilter to expose when a rule last fired. We don't - * track them yet; emit 0 so the field count matches the - * upstream protocol and unrealircd-tests' /STATS regex. - * id_str is the deletion handle, computed on demand. */ - 0LL, - 0LL, - spamfilter_id(tkl), + (long long)tkl->lasthit, + (long long)tkl->ptr.spamfilter->lasthit_except, + id_str, tkl->ptr.spamfilter->match->str); if (para && !strcasecmp(para, "del")) { - char *hash = spamfilter_id(tkl); - if (tkl->type & TKL_GLOBAL) - { - sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", hash); - sendtxtnumeric(client, "-"); - } else { + if (!(tkl->type & TKL_GLOBAL)) sendtxtnumeric(client, "This spamfilter is stored in the configuration file and cannot be removed with /SPAMFILTER del"); - sendtxtnumeric(client, "-"); - } + else + sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", tkl->id[0] ? tkl->id : spamfilter_fallback_id(tkl)); + sendtxtnumeric(client, "-"); } } else if (TKLIsNameBan(tkl)) @@ -4292,6 +4667,9 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (long long)(TStime() - tkl->set_at), tkl->set_by, + tkl->hits, + (long long)tkl->lasthit, + id_str, tkl->ptr.nameban->reason); } else if (TKLIsBanException(tkl)) @@ -4305,7 +4683,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl 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); + (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason); } } else { /* IRC-added: uses simple user/host mask */ @@ -4314,7 +4692,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl 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); + (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason); } } else { @@ -4395,6 +4773,7 @@ void _tkl_stats(Client *client, int type, const char *para, int *cnt) */ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) { + MessageTag *mtags = NULL; char typ; if (!(tkl->type & TKL_GLOBAL)) @@ -4402,9 +4781,13 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) typ = tkl_typetochar(tkl->type); + /* Carry the s2s-tkl/ fields (id, spamfilter_id) on add; del matches by mask so needs no tags. */ + if (add) + tkl_add_s2s_mtags(tkl, &mtags); + if (TKLIsServerBan(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name, add ? '+' : '-', typ, (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", @@ -4415,7 +4798,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsNameBan(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name, add ? '+' : '-', typ, tkl->ptr.nameban->hold ? 'H' : '*', @@ -4426,7 +4809,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsSpamfilter(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name, add ? '+' : '-', typ, spamfilter_target_inttostring(tkl->ptr.spamfilter->target), @@ -4439,7 +4822,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) } else if (TKLIsBanException(tkl)) { - sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name, + sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name, add ? '+' : '-', typ, (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", @@ -4456,6 +4839,8 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) log_data_integer("tkl_type_int", typ)); abort(); } + + safe_free_message_tags(mtags); } /** Broadcast a TKL entry. @@ -4613,27 +4998,32 @@ void _sendnotice_tkl_add(TKL *tkl) if (TKLIsServerBan(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL, - "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id $spamfilter_id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id)); } else if (TKLIsNameBan(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL, - "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id)); } else if (TKLIsSpamfilter(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL, "Spamfilter added: '$tkl' [type: $tkl.match_type] [targets: $tkl.spamfilter_targets] " - "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $tkl.set_by]", - log_data_tkl("tkl", tkl)); + "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $tkl.set_by] $id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id)); } else if (TKLIsBanException(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL, - "$tkl.type_string added: '$tkl' [types: $tkl.exception_types] [by: $tkl.set_by] [duration: $tkl.duration_string]", - log_data_tkl("tkl", tkl)); + "$tkl.type_string added: '$tkl' [types: $tkl.exception_types] [by: $tkl.set_by] [duration: $tkl.duration_string] $id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("id", "id", tkl->id)); } else { unreal_log(ULOG_ERROR, "tkl", "BUG_UNKNOWN_TKL", NULL, @@ -4651,31 +5041,36 @@ void _sendnotice_tkl_del(const char *removed_by, TKL *tkl) if (TKLIsServerBan(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL, - "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string]", + "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id $spamfilter_id", log_data_tkl("tkl", tkl), - log_data_string("removed_by", removed_by)); + log_data_string("removed_by", removed_by), + log_data_optional_name_value("id", "id", tkl->id), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id)); } else if (TKLIsNameBan(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL, - "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string]", + "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id", log_data_tkl("tkl", tkl), - log_data_string("removed_by", removed_by)); + log_data_string("removed_by", removed_by), + log_data_optional_name_value("id", "id", tkl->id)); } else if (TKLIsSpamfilter(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL, "Spamfilter removed: '$tkl' [type: $tkl.match_type] [targets: $tkl.spamfilter_targets] " - "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string]", + "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id", log_data_tkl("tkl", tkl), - log_data_string("removed_by", removed_by)); + log_data_string("removed_by", removed_by), + log_data_optional_name_value("id", "id", tkl->id)); } else if (TKLIsBanException(tkl)) { unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL, - "$tkl.type_string removed: '$tkl' [types: $tkl.exception_types] [by: $removed_by] [set at: $tkl.set_at_string]", + "$tkl.type_string removed: '$tkl' [types: $tkl.exception_types] [by: $removed_by] [set at: $tkl.set_at_string] $id", log_data_tkl("tkl", tkl), - log_data_string("removed_by", removed_by)); + log_data_string("removed_by", removed_by), + log_data_optional_name_value("id", "id", tkl->id)); } else { unreal_log(ULOG_ERROR, "tkl", "BUG_UNKNOWN_TKL", NULL, @@ -4881,7 +5276,7 @@ CMD_FUNC(cmd_tkl_add) BanActionValue action; unsigned short target; /* helper variables */ - char *err; + const char *err; if (parc < 12) { @@ -4962,6 +5357,30 @@ CMD_FUNC(cmd_tkl_add) * Note that we only update common fields, * which is acceptable to me. -- Syzop */ + const char *incoming_id = tkl_s2s_mtag_value(recv_mtags, "id"); + const char *incoming_spamfilter_id = NULL; + int changed = 0; + + /* id: an assigned id beats a blank one; two assigned ids tie-break by older + * set_at, then lexicographically. The "spamfilter_id" travels with the winning "id". + * This is done before set_at is lowered below, so the tie-break sees the original set_at. + */ + if (!BadPtr(incoming_id) && (strlen(incoming_id) < sizeof(tkl->id)) && strcmp(tkl->id, incoming_id)) + { + if (!tkl->id[0] || + (set_at < tkl->set_at) || + ((set_at == tkl->set_at) && (strcmp(incoming_id, tkl->id) < 0))) + { + strlcpy(tkl->id, incoming_id, sizeof(tkl->id)); + incoming_spamfilter_id = tkl_s2s_mtag_value(recv_mtags, "spamfilter_id"); + if (!BadPtr(incoming_spamfilter_id) && (strlen(incoming_spamfilter_id) < sizeof(tkl->spamfilter_id))) + strlcpy(tkl->spamfilter_id, incoming_spamfilter_id, sizeof(tkl->spamfilter_id)); + else + tkl->spamfilter_id[0] = '\0'; + changed = 1; + } + } + if ((set_at < tkl->set_at) || (expire_at != tkl->expire_at) || strcmp(tkl->set_by, parv[5])) { /* here's how it goes: @@ -4984,12 +5403,22 @@ CMD_FUNC(cmd_tkl_add) if (strcmp(tkl->set_by, parv[5]) < 0) safe_strdup(tkl->set_by, parv[5]); - if (type & TKL_GLOBAL) - tkl_broadcast_entry(1, client, client, tkl); + changed = 1; } + + if (changed && (type & TKL_GLOBAL)) + tkl_broadcast_entry(1, client, client, tkl); return; } + /* New entry: assign its unique id. Honor a supplied s2s-tkl/id (and spamfilter_id) from a + * remote server or services; if none and we are the originating server, generate a random one; + * otherwise leave it blank (eg the tag was dropped by an old server in the path). + */ + tkl_extract_s2s_mtags(tkl, recv_mtags); + if (!tkl->id[0] && IsMe(client)) + tkl_generate_id(tkl); + tkl_added(client, tkl); } @@ -5309,7 +5738,18 @@ void ban_action_run_all_sets_and_stops(Client *client, BanAction *action, int *s * @note Be sure to check IsDead(client) if return value is 1 and you are * considering to continue processing. */ +static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id); + int _take_action(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped) +{ + return take_action_ex(client, actions, reason, duration, take_action_flags, stopped, NULL); +} + +/** Internal variant of take_action() that records the spamfilter id on any + * *LINE/SHUN it creates, for the gline->spamfilter trace. Kept internal so the public + * take_action() efunc stays free of this spamfilter-specific concept. + */ +static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id) { BanAction *action; int previous_highest = 0; @@ -5384,7 +5824,11 @@ int _take_action(Client *client, BanAction *actions, const char *reason, long du tkllayer[6] = mo; tkllayer[7] = mo2; tkllayer[8] = reason; - cmd_tkl(NULL, &me, NULL, 9, tkllayer); + { + MessageTag *m = tkl_spamfilter_id_mtag(spamfilter_id); /* NULL unless set by a spamfilter */ + cmd_tkl(NULL, &me, m, 9, tkllayer); + safe_free_message_tags(m); + } RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration); if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN)) { @@ -5583,33 +6027,44 @@ static void match_spamfilter_hit(Client *client, const char *str_in, const char if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central)) { tkl->ptr.spamfilter->hits_except++; + tkl->ptr.spamfilter->lasthit_except = TStime(); } else { - tkl->ptr.spamfilter->hits++; + tkl_hit(client, tkl); highest_action = highest_ban_action(tkl->ptr.spamfilter->action); if (highest_action > BAN_ACT_SET) { + if (!cmd) + { + /* On-tag-change has no context, so log without the [cmd: ...] part */ + unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client, + "[Spamfilter] $client.details matches filter '$tkl': [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id", + log_data_tkl("tkl", tkl), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id)); + } else if (hide_content || (target == SPAMF_RAW)) { unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client, - "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination] [reason: $tkl.reason] [action: $tkl.ban_action]", + "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id", log_data_tkl("tkl", tkl), log_data_string("command", cmd), log_data_string("_space", destination ? " " : ""), - log_data_string("destination", destination ? destination : "")); + log_data_string("destination", destination ? destination : ""), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id)); } else { // Yeah we are re-running the text analysis. TextAnalysis textanalysis; memset(&textanalysis, 0, sizeof(textanalysis)); RunHook(HOOKTYPE_ANALYZE_TEXT, client, str, &textanalysis); unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client, - "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]", + "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id", log_data_tkl("tkl", tkl), log_data_string("command", cmd), log_data_string("_space", destination ? " " : ""), log_data_string("destination", destination ? destination : ""), log_data_string("str", str), - log_data_textanalysis("text_analysis", &textanalysis)); + log_data_textanalysis("text_analysis", &textanalysis), + log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id)); *content_revealed = 1; } @@ -5643,6 +6098,128 @@ static void match_spamfilter_hit(Client *client, const char *str_in, const char } +/** Run rule-only spamfilters: ones with no ::match/::target, only a ::rule. + * These run when a tag changes. The most severe hit (highest action) wins and is set in *winner_tkl. + * The caller then takes that action. This function is used both by: + * 1) match_spamfilter(): when a tag changed during a message + * 2) parse_client_queued(): after a command ran and there was a tag change + * or similar, like server_flood_count('..') changed value. + */ +static void run_rule_only_spamfilter_loop(Client *client, const char *str_in, const char *str, + int target, const char *cmd, const char *destination, + int flags, ClientContext *clictx, TKL **winner_tkl, + char user_is_exempt_general, char user_is_exempt_central, + int *stop_processing_general_spamfilters, + int *stop_processing_central_spamfilters, + int *content_revealed) +{ + TKL *tkl; + crule_context context; + + *stop_processing_general_spamfilters = *stop_processing_central_spamfilters = 0; /* reset */ + for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next) + { + if (tkl->ptr.spamfilter->target || + (tkl->ptr.spamfilter->match->type != MATCH_NONE) || + !tkl->ptr.spamfilter->rule) + { + continue; + } + + /* Skip spamfilters due to a 'stop' action from an earlier spamfilter + * or set::spamfilter::stop-on-first-match. + * We treat such stops as separate for central & general spamfilters + * so they don't affect each other. + */ + if (IsCentralSpamfilter(tkl)) + { + if (*stop_processing_central_spamfilters) + continue; + } else { + if (*stop_processing_general_spamfilters) + continue; + } + + if ((flags & SPAMFLAG_NOWARN) && only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_WARN)) + continue; + + /* If the action is 'soft' (for non-logged in users only) then + * don't bother running the spamfilter if the user is logged in. + */ + if (IsLoggedIn(client) && only_soft_actions(tkl->ptr.spamfilter->action)) + continue; + + memset(&context, 0, sizeof(context)); + context.client = client; + context.text = str_in; + context.destination = destination; + context.clictx = clictx; + if (!crule_eval(&context, tkl->ptr.spamfilter->rule)) + continue; + + match_spamfilter_hit(client, str_in, str, target, cmd, destination, + tkl, winner_tkl, + user_is_exempt_general, user_is_exempt_central, + stop_processing_general_spamfilters, stop_processing_central_spamfilters, + content_revealed, + 1); + /* and continue (yes, always, no stopping on first match) */ + } +} + +/** Run rule-only spamfilters, after a tag change happened. This gets called from + * parse_client_queued(), so it is safe to kill a client without having to deal + * with complex conditions. Since there is no "message" here, context is empty. + */ +void _run_deferred_rule_only_spamfilters(Client *client) +{ + TKL *winner_tkl = NULL; + char user_is_exempt_general = 0; + char user_is_exempt_central = 0; + int stop_processing_general_spamfilters = 0; + int stop_processing_central_spamfilters = 0; + int content_revealed = 0; + crule_context context; + + if (!client->local) + return; + + /* Mark all tag changes up to now as handled. Done up-front so the early-returns below + * (exempt users etc) don't make us re-run on every following command. + */ + client->local->spamfilter_run_tags_serial = client->local->tags_serial; + + if (!client->user || ValidatePermissionsForPath("immune:server-ban:spamfilter",client,NULL,NULL,NULL) || IsULine(client)) + return; + + memset(&context, 0, sizeof(context)); + context.client = client; + context.text = ""; + context.destination = NULL; + context.clictx = NULL; + + /* Client exempt from spamfilter checking? */ + if (find_tkl_exception(TKL_SPAMF, client) || + (iConf.spamfilter_except && user_allowed_by_security_group_context(client, iConf.spamfilter_except, &context))) + { + user_is_exempt_general = 1; + } + if (user_allowed_by_security_group_context(client, iConf.central_spamfilter_except, &context)) + user_is_exempt_central = 1; + + run_rule_only_spamfilter_loop(client, "", "", 0, NULL, NULL, 0, NULL, + &winner_tkl, user_is_exempt_general, user_is_exempt_central, + &stop_processing_general_spamfilters, &stop_processing_central_spamfilters, + &content_revealed); + + if (winner_tkl && !match_spamfilter_exempt(winner_tkl, user_is_exempt_general, user_is_exempt_central)) + { + char *reason = unreal_decodespace(winner_tkl->ptr.spamfilter->tkl_reason); + take_action_ex(client, winner_tkl->ptr.spamfilter->action, reason, + winner_tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, winner_tkl->id); + } +} + /** match_spamfilter: executes the spamfilter on the input string. * @param str The text (eg msg text, notice text, part text, quit text, etc * @param target The spamfilter target (SPAMF_*) @@ -5666,7 +6243,6 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char struct rusage rnow, rprev; long ms_past; #endif - int tags_serial = client->local ? client->local->tags_serial : 0; char user_is_exempt_general = 0; char user_is_exempt_central = 0; int stop_processing_general_spamfilters = 0; @@ -5758,6 +6334,8 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char if (tkl->ptr.spamfilter->match && (tkl->ptr.spamfilter->match->type != MATCH_NONE)) { + const char *regex_error = NULL; + #ifdef SPAMFILTER_DETECTSLOW if (tkl->ptr.spamfilter->match->type == MATCH_PCRE_REGEX) { @@ -5769,11 +6347,11 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char #endif if (tkl->ptr.spamfilter->input_conversion == INPUT_CONVERSION_STRIP_CONTROL_CODES) - ret = unreal_match(tkl->ptr.spamfilter->match, str); /* StripControlCodes() */ + ret = unreal_match(tkl->ptr.spamfilter->match, str, ®ex_error); /* StripControlCodes() */ else if (tkl->ptr.spamfilter->input_conversion == INPUT_CONVERSION_CONFUSABLES) - ret = unreal_match(tkl->ptr.spamfilter->match, str_deconfused ? str_deconfused : str); /* utf8_convert_confusables(), with fallback */ + ret = unreal_match(tkl->ptr.spamfilter->match, str_deconfused ? str_deconfused : str, ®ex_error); /* utf8_convert_confusables(), with fallback */ else - ret = unreal_match(tkl->ptr.spamfilter->match, str_in); /* raw */ + ret = unreal_match(tkl->ptr.spamfilter->match, str_in, ®ex_error); /* raw */ #ifdef SPAMFILTER_DETECTSLOW if (tkl->ptr.spamfilter->match->type == MATCH_PCRE_REGEX) @@ -5802,6 +6380,9 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char } } #endif + + if (regex_error) + spamfilter_regex_error(tkl, regex_error); } else { /* There is no ::match but there was a ::rule, and that is enough for a match.. */ if (tkl->ptr.spamfilter->rule) @@ -5819,65 +6400,16 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char } } - if (client->local && (client->local->tags_serial != tags_serial)) + if (client->local && (client->local->tags_serial != client->local->spamfilter_run_tags_serial)) { - /* A tag has been changed: - * Run spamfilters that have no 'match' and no 'targets', - * these are special in the sense that they only run - * whenever a tag is changed. + /* A tag changed (eg a spamfilter 'set' action during this message): run the + * rule-only spamfilters, which only run when a tag has changed. */ - stop_processing_general_spamfilters = stop_processing_central_spamfilters = 0; /* reset this */ - for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next) - { - crule_context context; - - if (tkl->ptr.spamfilter->target || - (tkl->ptr.spamfilter->match->type != MATCH_NONE) || - !tkl->ptr.spamfilter->rule) - { - continue; - } - - /* Skip spamfilters due to a 'stop' action from an earlier spamfilter - * or set::spamfilter::stop-on-first-match. - * We treat such stops as separate for central & general spamfilters - * so they don't affect each other. - */ - if (IsCentralSpamfilter(tkl)) - { - if (stop_processing_central_spamfilters) - continue; - } else { - if (stop_processing_general_spamfilters) - continue; - } - - - if ((flags & SPAMFLAG_NOWARN) && only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_WARN)) - continue; - - /* If the action is 'soft' (for non-logged in users only) then - * don't bother running the spamfilter if the user is logged in. - */ - if (IsLoggedIn(client) && only_soft_actions(tkl->ptr.spamfilter->action)) - continue; - - memset(&context, 0, sizeof(context)); - context.client = client; - context.text = str_in; - context.destination = destination; - context.clictx = clictx; - if (!crule_eval(&context, tkl->ptr.spamfilter->rule)) - continue; - - match_spamfilter_hit(client, str_in, str, target, cmd, destination, - tkl, &winner_tkl, - user_is_exempt_general, user_is_exempt_central, - &stop_processing_general_spamfilters, &stop_processing_central_spamfilters, - &content_revealed, - 1); - /* and continue (yes, always, no stopping on first match) */ - } + run_rule_only_spamfilter_loop(client, str_in, str, target, cmd, destination, flags, clictx, + &winner_tkl, user_is_exempt_general, user_is_exempt_central, + &stop_processing_general_spamfilters, &stop_processing_central_spamfilters, + &content_revealed); + client->local->spamfilter_run_tags_serial = client->local->tags_serial; } tkl = winner_tkl; @@ -5889,7 +6421,7 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char /* Spamfilter matched */ reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason); - ret = take_action(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL); + ret = take_action_ex(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, tkl->id); if (!IsDead(client)) { if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK)) diff --git a/src/modules/tkldb.c b/src/modules/tkldb.c index f18efe4d5d..aadfda947e 100644 --- a/src/modules/tkldb.c +++ b/src/modules/tkldb.c @@ -29,7 +29,7 @@ ModuleHeader MOD_HEADER = { #define TKLDB_MAGIC 0x10101010 /* Database version */ -#define TKLDB_VERSION 4999 +#define TKLDB_VERSION 6260 /* Save tkls to file every seconds */ #define TKLDB_SAVE_EVERY 300 /* The very first save after boot, apply this delta, this @@ -401,6 +401,12 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl) W_SAFE(unrealdb_write_str(db, tkl->set_by)); W_SAFE(unrealdb_write_int64(db, tkl->set_at)); W_SAFE(unrealdb_write_int64(db, tkl->expire_at)); + W_SAFE(unrealdb_write_str(db, tkl->id)); /* since TKLDB_VERSION 6260 */ + W_SAFE(unrealdb_write_str(db, tkl->spamfilter_id)); /* since TKLDB_VERSION 6260 */ + + /* Hit-stat fields for all TKL types (since TKLDB_VERSION 6260): hits, lasthit. */ + W_SAFE(unrealdb_write_int64(db, tkl->hits)); + W_SAFE(unrealdb_write_int64(db, tkl->lasthit)); if (TKLIsServerBan(tkl)) { @@ -446,6 +452,9 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl) W_SAFE(unrealdb_write_char(db, action)); W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason)); W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration)); + /* Spamfilter-only hit-stat fields (since TKLDB_VERSION 6260): hits_except, lasthit_except. */ + W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->hits_except)); + W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->lasthit_except)); } return 1; @@ -530,6 +539,7 @@ int read_tkldb(void) for (cnt = 0; cnt < tklcount; cnt++) { int do_not_add = 0; + TKL *added = NULL; tkl = safe_alloc(sizeof(TKL)); @@ -556,6 +566,22 @@ int read_tkldb(void) R_SAFE(unrealdb_read_int64(db, &v)); tkl->expire_at = v; + /* id and spamfilter_id were added in TKLDB_VERSION 6260 */ + if (version >= 6260) + { + R_SAFE(unrealdb_read_str(db, &str)); + strlcpy(tkl->id, str, sizeof(tkl->id)); + safe_free(str); + R_SAFE(unrealdb_read_str(db, &str)); + strlcpy(tkl->spamfilter_id, str, sizeof(tkl->spamfilter_id)); + safe_free(str); + /* Hit-stat fields for all TKL types (hits, lasthit). */ + R_SAFE(unrealdb_read_int64(db, &v)); + tkl->hits = v; + R_SAFE(unrealdb_read_int64(db, &v)); + tkl->lasthit = v; + } + /* Save some CPU... if it's already expired then don't bother adding */ if (tkl->expire_at != 0 && tkl->expire_at <= TStime()) do_not_add = 1; @@ -599,7 +625,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask, + added = tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask, NULL, tkl->ptr.serverban->reason, @@ -639,7 +665,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask, + added = tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask, NULL, tkl->ptr.banexception->reason, @@ -668,7 +694,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_nameban(tkl->type, tkl->ptr.nameban->name, + added = tkl_add_nameban(tkl->type, tkl->ptr.nameban->name, tkl->ptr.nameban->hold, tkl->ptr.nameban->reason, tkl->set_by, tkl->expire_at, @@ -678,7 +704,7 @@ int read_tkldb(void) if (TKLIsSpamfilter(tkl)) { int match_method; - char *err = NULL; + const char *err = NULL; tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter)); @@ -726,6 +752,14 @@ int read_tkldb(void) R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason)); R_SAFE(unrealdb_read_int64(db, &v)); tkl->ptr.spamfilter->tkl_duration = v; + /* Spamfilter-only hit-stat fields (hits_except, lasthit_except). */ + if (version >= 6260) + { + R_SAFE(unrealdb_read_int64(db, &v)); + tkl->ptr.spamfilter->hits_except = v; + R_SAFE(unrealdb_read_int64(db, &v)); + tkl->ptr.spamfilter->lasthit_except = v; + } if (!do_not_add && find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str, @@ -744,7 +778,7 @@ int read_tkldb(void) if (!do_not_add) { - tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target, + added = tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target, tkl->ptr.spamfilter->action, tkl->ptr.spamfilter->match, NULL, @@ -771,6 +805,19 @@ int read_tkldb(void) break; /* we MUST stop reading */ } + if (added) + { + strlcpy(added->id, tkl->id, sizeof(added->id)); + strlcpy(added->spamfilter_id, tkl->spamfilter_id, sizeof(added->spamfilter_id)); + added->hits = tkl->hits; + added->lasthit = tkl->lasthit; + if (TKLIsSpamfilter(added)) + { + added->ptr.spamfilter->hits_except = tkl->ptr.spamfilter->hits_except; + added->ptr.spamfilter->lasthit_except = tkl->ptr.spamfilter->lasthit_except; + } + } + if (!do_not_add) added_cnt++; diff --git a/src/parse.c b/src/parse.c index 32fb40081a..979062780f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -143,6 +143,16 @@ void parse_client_queued(Client *client) return; dopacket(client, buf, dolen); + + /* Tags and similar may change outside of match_spamfilter(), such as a flood + * counter or a central spamfilter tag. We run rule-only spamfilters here. + * This is outside the command handler so we can safely kill the client here. + */ + if (!IsDead(client) && client->local && + (client->local->tags_serial != client->local->spamfilter_run_tags_serial)) + { + run_deferred_rule_only_spamfilters(client); + } if (IsDead(client)) return; @@ -676,8 +686,12 @@ long parse_addlag(Client *client, int command_bytes, int mtags_bytes) */ void add_fake_lag(Client *client, long msec) { - if (!MyConnect(client)) + if (!MyConnect(client) || IsNoFakeLag(client)) + return; +#ifdef FAKELAG_CONFIGURABLE + if (client->local->class && (client->local->class->options & CLASS_OPT_NOFAKELAG)) return; +#endif client->local->fake_lag_msec += msec; client->local->fake_lag += (client->local->fake_lag_msec / 1000); diff --git a/src/socket.c b/src/socket.c index ec7045423b..1c0c1bcda9 100644 --- a/src/socket.c +++ b/src/socket.c @@ -939,7 +939,7 @@ Client *add_connection(ConfigItem_listen *listener, int fd) if ((listener->options & LISTENER_TLS) && ctx_server) { - SSL_CTX *ctx = listener->ssl_ctx ? listener->ssl_ctx : ctx_server; + SSL_CTX *ctx = tls_ctx_for_listener(listener); if (ctx) { diff --git a/src/support.c b/src/support.c index 292d5c3b2b..c4dc1c491f 100644 --- a/src/support.c +++ b/src/support.c @@ -1477,11 +1477,25 @@ void buildvarstring_nvp(const char *inbuf, char *outbuf, size_t len, NameValuePr output = urlencode(output, outputbuf, sizeof(outputbuf)); if (flags & BUILDVARSTRING_XML) output = xmlescape(output, outputbuf, sizeof(outputbuf)); - strlcpy(o, output, left); - left -= strlen(output); /* may become <0 */ - if (left <= 0) - return; /* return - don't write \0 to 'o'. ensured by strlcpy already */ - o += strlen(output); /* value entirely written */ + if (!*output && !(flags & BUILDVARSTRING_KEEP_SPACE_FOR_EMPTY_VAR)) + { + /* Empty value: eat one preceding space so "Something. $var" + * becomes "Something." Opt out with KEEP_SPACE_FOR_EMPTY_VAR + * (eg URL/XML). Only present-but-empty values are affected; + * NULL values and unknown vars are left alone. + */ + if ((o > outbuf) && (o[-1] == ' ')) + { + o--; + left++; + } + } else { + strlcpy(o, output, left); + left -= strlen(output); /* may become <0 */ + if (left <= 0) + return; /* return - don't write \0 to 'o'. ensured by strlcpy already */ + o += strlen(output); /* value entirely written */ + } } } else { diff --git a/src/tls.c b/src/tls.c index a6812900dd..772914a39b 100644 --- a/src/tls.c +++ b/src/tls.c @@ -48,8 +48,10 @@ int cipher_check(SSL_CTX *ctx, char **errstr); int certificate_quality_check(SSL_CTX *ctx, char **errstr); /* The TLS structures */ -SSL_CTX *ctx_server; -SSL_CTX *ctx_client; +SSL_CTX *ctx_server; /**< Default TLS context for incoming connections (set::tls). */ +SSL_CTX *ctx_client; /**< Default TLS context for outgoing connections (set::tls). */ +static SSL_CTX *ctx_link_server = NULL; /**< Default TLS context for incoming server links (set::server-linking::tls-options). NULL when not set, then use ctx_server instead. */ +static SSL_CTX *ctx_link_client = NULL; /**< Default TLS context for outgoing server links (set::server-linking::tls-options). NULL when not set, then use ctx_client instead. */ char *TLSKeyPasswd; @@ -358,7 +360,7 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server) { unreal_log(ULOG_ERROR, "config", "TLS_LOAD_FAILED_DEFAULT_CERT", NULL, "It seems the default certificate is missing. " - "Run 'make pem && make install' in the UnrealIRCd source directory " + "Run './unrealircd mkcert' " "to generate a self-signed cert."); } goto fail; @@ -381,6 +383,16 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server) // TODO: verify same amount of certificate_files vs key_files :D + /* (Re)compute the cached spkifp list for these options. init_ctx() can be + * called more than once on the same TLSOptions (eg ctx_server + ctx_client, + * and again on rehash), so clear it first to avoid duplicate entries. + * + * Note: rebuilding spkifp is a side effect, so throwaway callers (the expiry + * check, test_tlsblock) also rebuild the live list. Harmless for now. + * TODO: add an init_ctx() flag so those callers can skip it. + */ + safe_free_name_list(tlsoptions->spkifp); + for (n = tlsoptions->certificate_files, n2 = tlsoptions->key_files; n && n2; n = n->next, n2 = n2->next) { if (SSL_CTX_use_certificate_chain_file(ctx, n->name) <= 0) @@ -422,6 +434,24 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server) } goto fail; } + + /* Cache the spkifp of this just-loaded leaf certificate, so consumers + * (genlinkblock and the server-linking advisor) see all of a multi-cert + * server's fingerprints without re-reading files. SSL_new() + + * SSL_get_certificate() works on all OpenSSL/LibreSSL versions and + * returns the context's current (= just-loaded) certificate. + */ + { + SSL *probe = SSL_new(ctx); + if (probe) + { + X509 *leafcert = SSL_get_certificate(probe); + const char *fp = leafcert ? spki_fingerprint_ex(leafcert) : NULL; + if (fp) + append_name_list(tlsoptions->spkifp, fp); + SSL_free(probe); + } + } } if (SSL_CTX_set_cipher_list(ctx, tlsoptions->ciphers) == 0) @@ -457,8 +487,8 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server) { unreal_log(ULOG_ERROR, "config", "TLS_CERTIFICATE_CHECK_FAILED", NULL, "There is a problem with your TLS certificate: $quality_check_error\n" - "If you use the standard UnrealIRCd certificates then you can simply run 'make pem' and 'make install' " - "from your UnrealIRCd source directory (eg: ~/unrealircd-6.X.Y/) to create and install new certificates", + "If you use the standard UnrealIRCd certificates then you can simply run './unrealircd mkcert' " + "to create and install new certificates", log_data_string("quality_check_error", errstr)); goto fail; } @@ -579,6 +609,8 @@ SSL_CTX *init_ctx(TLSOptions *tlsoptions, int server) return ctx; fail: + /* Don't leave a partial cached spkifp behind on failure. */ + safe_free_name_list(tlsoptions->spkifp); SSL_CTX_free(ctx); return NULL; } @@ -719,6 +751,56 @@ int early_init_tls(void) return 1; } +/** Return the TLS context an incoming connection on this listener should use. + * A serversonly listener with no per-listener tls-options presents the + * set::server-linking certificate (when configured); everything else uses the + * normal set::tls server context. + */ +SSL_CTX *tls_ctx_for_listener(ConfigItem_listen *listener) +{ + if (listener->ssl_ctx) + return listener->ssl_ctx; + if ((listener->options & LISTENER_SERVERSONLY) && ctx_link_server) + return ctx_link_server; + return ctx_server; +} + +/** Return the TLS context an outgoing server link should use. A link with no + * per-link tls-options presents the set::server-linking certificate (when + * configured); otherwise the normal set::tls client context. + */ +SSL_CTX *tls_ctx_for_outgoing_link(ConfigItem_link *link) +{ + if (link->ssl_ctx) + return link->ssl_ctx; + if (ctx_link_client) + return ctx_link_client; + return ctx_client; +} + +/** Return the effective TLSOptions for an incoming connection on this listener + * (same selection as tls_ctx_for_listener(), but the options struct). Used for + * STARTTLS option flags and by the server-linking certificate advisor. + */ +TLSOptions *tls_options_for_listener(ConfigItem_listen *listener) +{ + if (listener->tls_options) + return listener->tls_options; + if ((listener->options & LISTENER_SERVERSONLY) && iConf.server_linking_tls_options) + return iConf.server_linking_tls_options; + return iConf.tls_options; +} + +/** Return the effective TLSOptions for an outgoing server link. */ +TLSOptions *tls_options_for_outgoing_link(ConfigItem_link *link) +{ + if (link->tls_options) + return link->tls_options; + if (iConf.server_linking_tls_options) + return iConf.server_linking_tls_options; + return iConf.tls_options; +} + /** Initialize the server and client contexts. * This is only possible after reading the configuration file. */ @@ -730,6 +812,15 @@ int init_tls(void) ctx_client = init_ctx(iConf.tls_options, 0); if (!ctx_client) return 0; + if (iConf.server_linking_tls_options) + { + ctx_link_server = init_ctx(iConf.server_linking_tls_options, 1); + if (!ctx_link_server) + return 0; + ctx_link_client = init_ctx(iConf.server_linking_tls_options, 0); + if (!ctx_link_client) + return 0; + } return 1; } @@ -764,6 +855,45 @@ int reinit_tls(void) SSL_CTX_free(ctx_client); ctx_client = tmp; /* activate */ + /* set::server-linking::tls-options (free+NULL if no longer configured) */ + if (iConf.server_linking_tls_options) + { + tmp = init_ctx(iConf.server_linking_tls_options, 1); + if (!tmp) + { + unreal_log(ULOG_ERROR, "config", "TLS_RELOAD_FAILED", NULL, + "TLS Reload failed at set::server-linking::tls-options. See previous errors."); + return 0; + } + if (ctx_link_server) + SSL_CTX_free(ctx_link_server); + ctx_link_server = tmp; /* activate */ + + tmp = init_ctx(iConf.server_linking_tls_options, 0); + if (!tmp) + { + unreal_log(ULOG_ERROR, "config", "TLS_RELOAD_FAILED", NULL, + "TLS Reload failed at set::server-linking::tls-options (client). See previous errors."); + return 0; + } + if (ctx_link_client) + SSL_CTX_free(ctx_link_client); + ctx_link_client = tmp; /* activate */ + } + else + { + if (ctx_link_server) + { + SSL_CTX_free(ctx_link_server); + ctx_link_server = NULL; + } + if (ctx_link_client) + { + SSL_CTX_free(ctx_link_client); + ctx_link_client = NULL; + } + } + /* listen::tls-options.... */ for (listen = conf_listen; listen; listen = listen->next) { @@ -875,6 +1005,16 @@ TLSOptions *get_tls_options_for_client(Client *client) return client->server->conf->tls_options; if (client->local && client->local->listener && client->local->listener->tls_options) return client->local->listener->tls_options; + /* No own tls-options: fall back to set::server-linking for s2s paths (like + * tls_ctx_for_*() does for the context), else set::tls. + */ + if (iConf.server_linking_tls_options) + { + if (client->server && client->server->conf) + return iConf.server_linking_tls_options; + if (client->local->listener && (client->local->listener->options & LISTENER_SERVERSONLY)) + return iConf.server_linking_tls_options; + } return iConf.tls_options; } @@ -882,7 +1022,7 @@ TLSOptions *get_tls_options_for_client(Client *client) void unreal_tls_client_handshake(int fd, int revents, void *data) { Client *client = data; - SSL_CTX *ctx = (client->server && client->server->conf && client->server->conf->ssl_ctx) ? client->server->conf->ssl_ctx : ctx_client; + SSL_CTX *ctx = (client->server && client->server->conf) ? tls_ctx_for_outgoing_link(client->server->conf) : ctx_client; TLSOptions *tlsoptions = get_tls_options_for_client(client); if (!ctx) @@ -1202,7 +1342,13 @@ static int fatal_tls_error(int ssl_error, int where, int my_errno, Client *clien /** Do a TLS handshake after a STARTTLS, as a client */ int client_starttls(Client *client) { - if ((client->local->ssl = SSL_new(ctx_client)) == NULL) + /* If this is an outgoing server link, then use link::outgoing::tls-options, + * falling back to set::server-linking::tls-options. + * Otherwise, use the client context. + */ + SSL_CTX *ctx = (client->server && client->server->conf) ? tls_ctx_for_outgoing_link(client->server->conf) : ctx_client; + + if ((client->local->ssl = SSL_new(ctx)) == NULL) goto fail_starttls; SetTLS(client); @@ -1699,6 +1845,9 @@ EVENT(tls_check_expiry) /* set block */ check_certificate_expiry_tlsoptions_and_warn(iConf.tls_options); + if (iConf.server_linking_tls_options) + check_certificate_expiry_tlsoptions_and_warn(iConf.server_linking_tls_options); + for (listen = conf_listen; listen; listen = listen->next) if (listen->tls_options) check_certificate_expiry_tlsoptions_and_warn(listen->tls_options); diff --git a/src/unrealircdctl.c b/src/unrealircdctl.c index 7655af103f..5c1547ec15 100644 --- a/src/unrealircdctl.c +++ b/src/unrealircdctl.c @@ -193,9 +193,13 @@ void unrealircdctl_spkifp(int argc, char *argv[]) if (!file) { - printf("NOTE: This script uses the default certificate location (any set::tls settings\n" - "are ignored). If this is not what you want then specify a certificate\n" - "explicitly like this: %s spkifp conf/tls/example.pem\n\n", UNREALCMD); + printf("IMPORTANT: If you are configuring server linking then we HIGHLY recommend\n" + "using the '%s genlinkblock' command instead. It outputs a complete\n" + "link block, with the right port and the correct spkifp(s), and it uses\n" + "your configured set::server-linking and set::tls certificates. This\n" + "'spkifp' command ignores those and just reads the default certificate,\n" + "or a file you specify like '%s spkifp conf/tls/example.pem'.\n\n", + UNREALCMD, UNREALCMD); safe_strdup(file, "tls/server.cert.pem"); convert_to_absolute_path(&file, CONFDIR); } @@ -204,7 +208,7 @@ void unrealircdctl_spkifp(int argc, char *argv[]) { printf("Could not open certificate: %s\n" "You can specify a certificate like this: %s spkifp conf/tls/example.pem\n", - UNREALCMD, file); + file, UNREALCMD); exit(1); } diff --git a/src/url_unreal.c b/src/url_unreal.c index 799f6e5f1b..ddb45a5384 100644 --- a/src/url_unreal.c +++ b/src/url_unreal.c @@ -824,9 +824,14 @@ int https_handle_response_header(Download *handle, char *readbuf, int n) } } - if (lastloc) + if (!end_of_request && lastloc) { - /* Last line was cut somewhere, save it for next round. */ + /* Save a cut-off (incomplete) header line for the next packet. + * Only while still reading headers: once end_of_request is set, + * lastloc points at body data that https_handle_response_body() + * already owns via handle->lefttoparse/lefttoparselen, so touching + * it here would desync those two and cause an out-of-bounds read. + */ safe_strdup(handle->lefttoparse, lastloc); } diff --git a/src/user.c b/src/user.c index e2320506ca..c3268c6eb0 100644 --- a/src/user.c +++ b/src/user.c @@ -190,6 +190,7 @@ int target_limit_exceeded(Client *client, void *target, const char *name) add_fake_lag(client, 2000); /* lag them up as well */ flood_limit_exceeded_log(client, "max-concurrent-conversations"); + flood_blocked_increment(client, FLD_CONVERSATIONS); sendnumeric(client, ERR_TARGETTOOFAST, name, (long long)(client->local->nexttarget - TStime())); return 1; @@ -972,6 +973,7 @@ int flood_limit_exceeded(Client *client, FloodOption opt) if (client->local->flood[opt].count > f->limit[opt]) { flood_limit_exceeded_log(client, floodoption_names[opt]); + flood_blocked_increment(client, opt); return 1; /* Flood limit hit! */ } @@ -1031,6 +1033,49 @@ MODVAR const char *floodoption_names[] = { NULL }; +/* Per-session flood-block counter names, parallel to floodoption_names[]. + * These are the type names accepted by the server_flood_count() crule function, eg server_flood_count('away'), + * for use in security-group::rule and spamfilter::rule to react to users who keep + * tripping flood limits. NULL means this flood type is not counted: lag-penalty is a + * soft penalty rather than a block, and multiline is not counted. + */ +MODVAR const char *floodoption_shortnames[] = { + "nick", /* FLD_NICK */ + "join", /* FLD_JOIN */ + "away", /* FLD_AWAY */ + "invite", /* FLD_INVITE */ + "knock", /* FLD_KNOCK */ + "conversations", /* FLD_CONVERSATIONS */ + NULL, /* FLD_LAG_PENALTY: not counted (soft penalty, not a block) */ + "vhost", /* FLD_VHOST */ + NULL, /* FLD_MULTILINE: not counted */ + NULL +}; + +/** Count a flood-block for this client. + * Used for: + * - increments the per-type session counter, which is read via server_flood_count('away') etc in a crule + * - the bump_tag_serial() call is what lets rule-only spamfilters re-check (eg + * server_flood_count('away') > 5) at the next safe boundary, see parse_client_queued(); flood + * counts share that "something changed, re-check" signal with the tag system. + * @param client The local client that got flood-blocked + * @param opt The flood type (eg FLD_AWAY) + */ +void flood_blocked_increment(Client *client, FloodOption opt) +{ + if (!MyConnect(client)) + return; + + /* Some flood types are not counted, eg lag-penalty (a soft penalty, not a block) */ + if (!floodoption_shortnames[opt]) + return; + + if (client->local->flood[opt].blocked < 65535) + client->local->flood[opt].blocked++; + + bump_tag_serial(client); +} + /** Lookup GEO information for an IP address. * @param ip The IP to lookup * @returns A struct containing all the details. Must be freed by caller! @@ -1115,8 +1160,11 @@ Tag *find_tag(Client *client, const char *name) void bump_tag_serial(Client *client) { + /* This is only ever compared with != (a change counter), so we just need a + * wrap point high enough that it never realistically aliases within a session. + */ client->local->tags_serial++; - if (client->local->tags_serial > 10000) + if (client->local->tags_serial >= INT_MAX) client->local->tags_serial = 0; } diff --git a/src/windows/makecert.bat b/src/windows/makecert.bat index 5726262671..64ff6ca183 100755 --- a/src/windows/makecert.bat +++ b/src/windows/makecert.bat @@ -1,6 +1,5 @@ @title Certificate Generation SET OPENSSL_CONF=tls.cnf openssl ecparam -out ../conf/tls/server.key.pem -name secp384r1 -genkey -openssl req -new -config tls.cnf -out ../conf/tls/server.req.pem -key ../conf/tls/server.key.pem -nodes -openssl req -x509 -config tls.cnf -days 3650 -sha256 -in ../conf/tls/server.req.pem -key ../conf/tls/server.key.pem -out ../conf/tls/server.cert.pem +openssl req -new -x509 -config tls.cnf -key ../conf/tls/server.key.pem -days 3650 -sha256 -out ../conf/tls/server.cert.pem diff --git a/src/windows/unrealinst.iss b/src/windows/unrealinst.iss index 97cf12f250..d2eb53652e 100755 --- a/src/windows/unrealinst.iss +++ b/src/windows/unrealinst.iss @@ -43,35 +43,35 @@ Name: "fixperm"; Description: "Make UnrealIRCd folder writable by current user"; [Files] ; UnrealIRCd binaries -Source: "UnrealIRCd.exe"; DestDir: "{app}\bin"; Flags: ignoreversion signonce +Source: "UnrealIRCd.exe"; DestDir: "{app}\bin"; Flags: ignoreversion Source: "UnrealIRCd.pdb"; DestDir: "{app}\bin"; Flags: ignoreversion -Source: "unrealircdctl.exe"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "unrealsvc.exe"; DestDir: "{app}\bin"; Flags: ignoreversion signonce +Source: "unrealircdctl.exe"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "unrealsvc.exe"; DestDir: "{app}\bin"; Flags: ignoreversion ; TLS certificate generation helpers Source: "src\windows\makecert.bat"; DestDir: "{app}\bin"; Flags: ignoreversion -Source: "extras\tls.cnf"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "doc\conf\tls\tls.cnf"; DestDir: "{app}\bin"; Flags: ignoreversion ; UnrealIRCd modules -Source: "src\modules\*.dll"; DestDir: "{app}\modules"; Flags: ignoreversion signonce -Source: "src\modules\chanmodes\*.dll"; DestDir: "{app}\modules\chanmodes"; Flags: ignoreversion signonce -Source: "src\modules\usermodes\*.dll"; DestDir: "{app}\modules\usermodes"; Flags: ignoreversion signonce -Source: "src\modules\extbans\*.dll"; DestDir: "{app}\modules\extbans"; Flags: ignoreversion signonce -Source: "src\modules\rpc\*.dll"; DestDir: "{app}\modules\rpc"; Flags: ignoreversion signonce -Source: "src\modules\third\*.dll"; DestDir: "{app}\modules\third"; Flags: ignoreversion skipifsourcedoesntexist signonce +Source: "src\modules\*.dll"; DestDir: "{app}\modules"; Flags: ignoreversion +Source: "src\modules\chanmodes\*.dll"; DestDir: "{app}\modules\chanmodes"; Flags: ignoreversion +Source: "src\modules\usermodes\*.dll"; DestDir: "{app}\modules\usermodes"; Flags: ignoreversion +Source: "src\modules\extbans\*.dll"; DestDir: "{app}\modules\extbans"; Flags: ignoreversion +Source: "src\modules\rpc\*.dll"; DestDir: "{app}\modules\rpc"; Flags: ignoreversion +Source: "src\modules\third\*.dll"; DestDir: "{app}\modules\third"; Flags: ignoreversion skipifsourcedoesntexist ; Libraries -Source: "c:\dev\unrealircd-6-libs\pcre2\bin\pcre*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\argon2\vs2015\build\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\libsodium\bin\x64\Release\v142\dynamic\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\jansson\bin\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\c-ares\msvc\cares\dll-release\cares.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\openssl\bin\openssl.exe"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\openssl\bin\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\GeoIP\libGeoIP\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce -Source: "c:\dev\unrealircd-6-libs\setacl.exe"; DestDir: "{app}\tmp"; Flags: ignoreversion signonce +Source: "c:\dev\unrealircd-6-libs\pcre2\bin\pcre*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\argon2\vs2015\build\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\libsodium\bin\x64\Release\v142\dynamic\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\jansson\bin\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\c-ares\msvc\cares\dll-release\cares.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\openssl\bin\openssl.exe"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\openssl\bin\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\GeoIP\libGeoIP\*.dll"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "c:\dev\unrealircd-6-libs\setacl.exe"; DestDir: "{app}\tmp"; Flags: ignoreversion #ifdef USE_CURL -Source: "c:\dev\unrealircd-6-libs\curl\bin\libcurl.dll"; DestDir: "{app}\bin"; Flags: ignoreversion signonce +Source: "c:\dev\unrealircd-6-libs\curl\bin\libcurl.dll"; DestDir: "{app}\bin"; Flags: ignoreversion #endif Source: "doc\conf\tls\curl-ca-bundle.crt"; DestDir: "{app}\conf\tls"; Flags: ignoreversion