From f765905b15a629526d65078fb14b3928d32e3a32 Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Tue, 5 May 2026 16:30:11 +0200 Subject: [PATCH] New snomask 'x' (set by default): maxperip/connthrottle connect rejections When a client is rejected by maxperip (not new) or connthrottle ipv6-unknown-users-limit (that one is new), a notice to +s +x will be sent. maxperip ipv4 example: *** Client testuser4 with IP 1.2.3.4 rejected: maxperip limit exceeded (4 global, max 3) maxperip ipv6 with /64 example: *** Client testuser4 with IP 2001:dbe:0:0:0:0:0:4 rejected: maxperip limit exceeded for 2001:dbe::/64 (4 local, max 3) connthrottle example where /56 limit is exceeded: *** Client testuser5 with IP 2001:db8:cafe:abcd:0:0:0:5 rejected: connthrottle ipv6-unknown-users-limit (cidr-56, max 4) exceeded for 2001:db8:cafe::/56 (5 unknown / 0 excepted / 0 known) Oh and this commit also fixes a typo in existing CONNTHROTTLE events, which previously were CONNTHROTLE (a missing T). --- doc/conf/snomasks.default.conf | 11 +++++++++++ include/h.h | 1 + include/struct.h | 2 +- src/match.c | 16 ++++++++++++++++ src/modules/connthrottle.c | 20 +++++++++++++++----- src/modules/maxperip.c | 26 ++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/doc/conf/snomasks.default.conf b/doc/conf/snomasks.default.conf index 4c64a8683..cf1345aa1 100644 --- a/doc/conf/snomasks.default.conf +++ b/doc/conf/snomasks.default.conf @@ -201,6 +201,17 @@ log { } } +/* Connection-limit rejections (maxperip / connthrottle) - 'x' */ +log { + source { + connthrottle.CONNTHROTTLE_IPV6_LIMIT; + maxperip.MAXPERIP_LIMIT; + } + destination { + snomask x; + } +} + /* Snomask s (server notices) - the "catch all" snomask for all other things */ log { source { diff --git a/include/h.h b/include/h.h index 0694f4f51..e3c239528 100644 --- a/include/h.h +++ b/include/h.h @@ -530,6 +530,7 @@ extern const char *inetntop(int af, const void *in, char *local_dummy, size_t th extern void mask_ipv6_rawip(const char *src, int prefix, char *dst); extern const char *get_clone_mask_ipstr(Client *client, char *buf, size_t buflen); extern const char *format_ipv6_prefix_reject_message(const char *template, const char *masked_rawip, int prefix); +extern const char *format_ipv6_addr(const char *rawip); extern void delletterfromstring(char *s, char letter); extern void addlettertodynamicstringsorted(char **str, char letter); diff --git a/include/struct.h b/include/struct.h index 75e9f5c05..7a922e6d4 100644 --- a/include/struct.h +++ b/include/struct.h @@ -448,7 +448,7 @@ typedef enum ClientStatus { #define CLIENT_FLAG_IPV6 0x800000000 /**< client is using IPv6 */ /** @} */ -#define OPER_SNOMASKS "+bBcdfkqsSoO" +#define OPER_SNOMASKS "+bBcdfxkqsSoO" #define SEND_UMODES (SendUmodes) #define ALL_UMODES (AllUmodes) diff --git a/src/match.c b/src/match.c index 3eda94de8..fd30fd0e9 100644 --- a/src/match.c +++ b/src/match.c @@ -976,3 +976,19 @@ const char *format_ipv6_prefix_reject_message(const char *template, buildvarstring(template, buf, sizeof(buf), vars, values); return buf; } + +/** Format an IPv6 raw address as a compressed string (e.g. "2001:db8::"). + * + * Returns a pointer to internal static storage, overwritten on each call. + * + * @param rawip 16-byte raw IPv6 address. + * @return inet_ntop result, or "?" on failure. + */ +const char *format_ipv6_addr(const char *rawip) +{ + static char buf[128]; /* generously oversized; longest IPv6 string form is ~46 chars */ + + if (!inet_ntop(AF_INET6, rawip, buf, sizeof(buf))) + strlcpy(buf, "?", sizeof(buf)); + return buf; +} diff --git a/src/modules/connthrottle.c b/src/modules/connthrottle.c index e5d54d962..5eaef7a8b 100644 --- a/src/modules/connthrottle.c +++ b/src/modules/connthrottle.c @@ -559,7 +559,7 @@ EVENT(connthrottle_evt) if (ucounter->rejected_clients) { - unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTLE_REPORT", NULL, + unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTTLE_REPORT", NULL, "ConnThrottle] Stats for this server past 60 secs: " "Connections rejected: $num_rejected. " "Accepted: $num_accepted_except except user(s) and " @@ -615,7 +615,7 @@ int ct_pre_lconnect(Client *client) /* We send the LARGE banner if throttling was activated */ if (!ucounter->throttling_previous_minute && !ucounter->throttling_banner_displayed) { - unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_ACTIVATED", NULL, + unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_ACTIVATED", NULL, "[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE.\n" "Users with IP addresses that have not been seen before will be rejected above the set connection rate. Known users can still get in.\n" "or more information see https://www.unrealircd.org/docs/ConnThrottle"); @@ -745,7 +745,7 @@ void ct_off(Client *client) return; /* Already off */ ucounter->disabled = 1; - unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_MODULE_DISABLED", client, + unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_MODULE_DISABLED", client, "[ConnThrottle] $client.details DISABLED the connthrottle module."); } @@ -754,7 +754,7 @@ void ct_on(Client *client) if (!ucounter->disabled) return; /* Already on */ - unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_MODULE_ENABLED", client, + unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_MODULE_ENABLED", client, "[ConnThrottle] $client.details ENABLED the connthrottle module."); ucounter->disabled = 0; } @@ -762,7 +762,7 @@ void ct_on(Client *client) void ct_reset(Client *client) { memset(ucounter, 0, sizeof(UCounter)); - unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_RESET", client, + unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_RESET", client, "[ConnThrottle] $client.details did a RESET on the statistics/counters."); } @@ -1245,9 +1245,19 @@ const char *ct_allow_client(Client *client, ConfigItem_allow *aconf) ct_make_rawip(client, tier, masked); b = ct_find_bucket(tier, masked); if (b && (b->unknown_users > effective_limit)) + { + unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTTLE_IPV6_LIMIT", client, + "Client $client.name with IP $client.ip rejected: connthrottle ipv6-unknown-users-limit (cidr-$prefix_len, max $max) exceeded for $prefix_addr/$prefix_len ($unknown_users unknown / $excepted_users excepted / $known_users known)", + log_data_string("prefix_addr", format_ipv6_addr(masked)), + log_data_integer("prefix_len", ct_tier_prefix[tier]), + log_data_integer("max", effective_limit), + log_data_integer("unknown_users", b->unknown_users), + log_data_integer("excepted_users", b->excepted_unknowns), + log_data_integer("known_users", b->known_users)); return format_ipv6_prefix_reject_message( iConf.reject_message_too_many_new_connections_ipv6_range, masked, ct_tier_prefix[tier]); + } } return NULL; diff --git a/src/modules/maxperip.c b/src/modules/maxperip.c index add255652..131d94bac 100644 --- a/src/modules/maxperip.c +++ b/src/modules/maxperip.c @@ -459,14 +459,40 @@ const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf) { if (exceeds_maxperip(client, aconf)) { + IpUsersBucket *bucket = find_ipusers_bucket(client); + if (IsIPV6(client) && iConf.default_ipv6_clone_mask < 128) { char masked[16]; mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, masked); + if (bucket && bucket->local_clients > aconf->maxperip) + unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client, + "Client $client.name with IP $client.ip rejected: maxperip limit exceeded for $prefix_addr/$prefix_len ($count local, max $max)", + log_data_string("prefix_addr", format_ipv6_addr(masked)), + log_data_integer("prefix_len", iConf.default_ipv6_clone_mask), + log_data_integer("count", bucket->local_clients), + log_data_integer("max", aconf->maxperip)); + else + unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client, + "Client $client.name with IP $client.ip rejected: maxperip limit exceeded for $prefix_addr/$prefix_len ($count global, max $max)", + log_data_string("prefix_addr", format_ipv6_addr(masked)), + log_data_integer("prefix_len", iConf.default_ipv6_clone_mask), + log_data_integer("count", bucket ? bucket->global_clients : 0), + log_data_integer("max", aconf->global_maxperip)); return format_ipv6_prefix_reject_message( iConf.reject_message_too_many_connections_ipv6_range, masked, iConf.default_ipv6_clone_mask); } + if (bucket && bucket->local_clients > aconf->maxperip) + unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client, + "Client $client.name with IP $client.ip rejected: maxperip limit exceeded ($count local, max $max)", + log_data_integer("count", bucket->local_clients), + log_data_integer("max", aconf->maxperip)); + else + unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client, + "Client $client.name with IP $client.ip rejected: maxperip limit exceeded ($count global, max $max)", + log_data_integer("count", bucket ? bucket->global_clients : 0), + log_data_integer("max", aconf->global_maxperip)); return iConf.reject_message_too_many_connections; } return NULL;