diff --git a/doc/conf/modules.optional.conf b/doc/conf/modules.optional.conf index 60ccd5f74..139472019 100644 --- a/doc/conf/modules.optional.conf +++ b/doc/conf/modules.optional.conf @@ -197,3 +197,63 @@ set { // timeout in which users must complete the handshake. // By uncommenting the following, you can raise it from 30 to 60 seconds: // set { handshake-timeout 60s; }; + +/* + * The following will configure connection throttling of "unknown users". + * + * When UnrealIRCd detects a high number of users connecting from IP addresses + * that have not been seen before, then connections from new IP's are rejected + * above the set rate. For example at 10:60 only 10 users per minute can connect + * that have not been seen before. Known IP addresses can always get in, + * regardless of the set rate. Same for users who login using SASL. + * + * See also https://www.unrealircd.org/docs/Connthrottle + * Or just keep reading the default configuration below: + */ + +loadmodule "third/reputation"; +loadmodule "third/connthrottle"; + +set { + connthrottle { + /* First we must configure what we call "known users". + * By default these are users on IP addresses that have + * a score of 24 or higher. A score of 24 means that the + * IP was connected to this network for at least 2 hours + * in the past month (or minimum 1 hour if registered). + * The sasl-bypass option is another setting. It means + * that users who authenticate to services via SASL + * are considered known users as well. + * Users in the "known-users" group (either by reputation + * or by SASL) are always allowed in by this module. + */ + known-users { + minimum-reputation-score 24; + sasl-bypass yes; + }; + + /* New users are all users that do not belong in the + * known-users group. They are considered "new" and in + * case of a high number of such new users connecting + * they are subject to connection rate limiting. + * By default the rate is 20 new local users per minute + * and 30 new global users per minute. + */ + new-users { + local-throttle 20:60; + global-throttle 30:60; + }; + + /* This configures when this module will NOT be active. + * The default settings will disable the module when: + * - The reputation module has been running for less than + * a week. If running less than 1 week then there is + * insufficient data to consider who is a "known user". + * - The server has just been booted up (first 3 minutes). + */ + disabled-when { + reputation-gathering 1w; + start-delay 3m; + }; + }; +}; diff --git a/include/modules.h b/include/modules.h index d0b316cbb..12712c56f 100644 --- a/include/modules.h +++ b/include/modules.h @@ -426,8 +426,8 @@ struct _irccallback { * for things like do_join, join_channel, etc. * The difference between callbacks and efunctions are: * - efunctions are mandatory, while callbacks can be optional (depends!) - * - efunctions are ment for internal usage, so 3rd party modules are not allowed - * to add them. + * - efunctions are meant for internal usage, so 3rd party modules are + * not allowed to add them. * - all efunctions are declared as function pointers in modules.c */ struct _ircefunction { @@ -1045,6 +1045,7 @@ _UNREAL_ERROR(_hook_error_incompatible, "Incompatible hook function. Check argum #define CALLBACKTYPE_CLOAKKEYCSUM 2 #define CALLBACKTYPE_CLOAK_EX 3 #define CALLBACKTYPE_BLACKLIST_CHECK 4 +#define CALLBACKTYPE_REPUTATION_STARTTIME 5 /* Efunction types */ #define EFUNC_DO_JOIN 1 diff --git a/makefile.win32 b/makefile.win32 index 036028986..27b939761 100644 --- a/makefile.win32 +++ b/makefile.win32 @@ -233,6 +233,8 @@ DLL_FILES=SRC/MODULES/M_CHGHOST.DLL SRC/MODULES/M_SDESC.DLL SRC/MODULES/M_SETIDE SRC/MODULES/ANTIMIXEDUTF8.DLL \ SRC/MODULES/AUTHPROMPT.DLL \ SRC/MODULES/M_SINFO.DLL \ + SRC/MODULES/REPUTATION.DLL \ + SRC/MODULES/CONNTHROTTLE.DLL \ SRC/MODULES/CHANMODES/CENSOR.DLL \ SRC/MODULES/CHANMODES/DELAYJOIN.DLL \ SRC/MODULES/CHANMODES/FLOODPROT.DLL \ @@ -871,6 +873,12 @@ src/modules/authprompt.dll: src/modules/authprompt.c $(INCLUDES) src/modules/m_sinfo.dll: src/modules/m_sinfo.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/m_sinfo.c $(MODLFLAGS) +src/modules/reputation.dll: src/modules/reputation.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/reputation.c $(MODLFLAGS) + +src/modules/connthrottle.dll: src/modules/connthrottle.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/connthrottle.c $(MODLFLAGS) + src/modules/chanmodes/censor.dll: src/modules/chanmodes/censor.c $(INCLUDES) $(CC) $(MODCFLAGS) /Fosrc/modules/chanmodes/ /Fesrc/modules/chanmodes/ src/modules/chanmodes/censor.c $(MODLFLAGS) diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index 6f460b384..5a89da114 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -62,7 +62,8 @@ R_MODULES= \ blacklist.so jointhrottle.so \ antirandom.so hideserver.so jumpserver.so \ m_ircops.so m_staff.so nocodes.so \ - charsys.so antimixedutf8.so authprompt.so m_sinfo.so + charsys.so antimixedutf8.so authprompt.so m_sinfo.so \ + reputation.so connthrottle.so MODULES=cloak.so $(R_MODULES) MODULEFLAGS=@MODULEFLAGS@ @@ -530,6 +531,14 @@ m_sinfo.so: m_sinfo.c $(INCLUDES) $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ -o m_sinfo.so m_sinfo.c +reputation.so: reputation.c $(INCLUDES) + $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ + -o reputation.so reputation.c + +connthrottle.so: connthrottle.c $(INCLUDES) + $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ + -o connthrottle.so connthrottle.c + ############################################################################# # capabilities ############################################################################# diff --git a/src/modules/connthrottle.c b/src/modules/connthrottle.c new file mode 100644 index 000000000..10b65ea9f --- /dev/null +++ b/src/modules/connthrottle.c @@ -0,0 +1,788 @@ +/* + * connthrottle - Connection throttler + * (C) Copyright 2004-2019 Bram Matthys (Syzop) and the UnrealIRCd team + * License: GPLv2 + * See https://www.unrealircd.org/docs/Connthrottle + */ + +#include "unrealircd.h" + +#define CONNTHROTTLE_VERSION "1.1" + +#ifndef CALLBACKTYPE_REPUTATION_STARTTIME + #define CALLBACKTYPE_REPUTATION_STARTTIME 5 +#endif + +ModuleHeader MOD_HEADER(connthrottle) + = { + "connthrottle", + CONNTHROTTLE_VERSION, + "Connection throttler - by Syzop", + "3.2-b8-1", + NULL + }; + +typedef struct { + int count; + int period; +} ThrottleSetting; + +struct cfgstruct { + /* set::connthrottle::known-users: */ + ThrottleSetting local; + ThrottleSetting global; + /* set::connthrottle::new-users: */ + int minimum_reputation_score; + int sasl_bypass; + /* set::connthrottle::disabled-when: */ + long reputation_gathering; + int start_delay; + /* set::connthrottle (generic): */ + char *reason; +}; +static struct cfgstruct cfg; + +typedef struct { + int count; + long t; +} ThrottleCounter; + +struct _ucounter { + ThrottleCounter local; /**< Local counter */ + ThrottleCounter global; /**< Global counter */ + int rejected_clients; /**< Number of rejected clients this minute */ + int allowed_score; /**< Number of allowed clients of type known-user */ + int allowed_sasl; /**< Number of allowed clients of type SASL */ + int allowed_other; /**< Number of allowed clients of type other (new) */ + char disabled; /**< Module disabled by oper? */ + int throttling_this_minute; /**< Did we do any throttling this minute? */ + int throttling_previous_minute; /**< Did we do any throttling previous minute? */ + int throttling_banner_displayed;/**< Big we-are-now-throttling banner displayed? */ + time_t next_event; /**< When is next event? (for "last 60 seconds" stats) */ +}; +static struct _ucounter ucounter; + +static char rehash_dump_filename[512]; + +#define MSG_THROTTLE "THROTTLE" + +#define GetReputation(acptr) (moddata_client_get(acptr, "reputation") ? atoi(moddata_client_get(acptr, "reputation")) : 0) + +/* Forward declarations */ +int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); +int ct_config_posttest(int *errs); +int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type); +int ct_pre_lconnect(aClient *sptr); +int ct_lconnect(aClient *); +int ct_rconnect(aClient *); +int ct_throttle(aClient *cptr, aClient *sptr, int parc, char *parv[]); +void rehash_dump_settings(void); +void rehash_read_settings(void); +EVENT(connthrottle_evt); + +MOD_TEST(connthrottle) +{ + memset(&cfg, 0, sizeof(cfg)); + memset(&ucounter, 0, sizeof(ucounter)); + + /* Defaults: */ + cfg.local.count = 20; cfg.local.period = 60; + cfg.global.count = 30; cfg.global.period = 60; + cfg.start_delay = 180; /* 3 minutes */ + cfg.reason = strdup("Throttled: Too many users trying to connect, please wait a while and try again"); + cfg.minimum_reputation_score = 24; + cfg.sasl_bypass = 1; + + HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, ct_config_test); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, ct_config_posttest); + return MOD_SUCCESS; +} + +MOD_INIT(connthrottle) +{ + MARK_AS_OFFICIAL_MODULE(modinfo); + snprintf(rehash_dump_filename, sizeof(rehash_dump_filename), "%s/connthrottle.tmp", TMPDIR); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, ct_config_run); + HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, ct_pre_lconnect); + HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, ct_lconnect); + HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, ct_rconnect); + CommandAdd(modinfo->handle, MSG_THROTTLE, ct_throttle, MAXPARA, M_USER|M_SERVER); + return MOD_SUCCESS; +} + +MOD_LOAD(connthrottle) +{ + rehash_read_settings(); + EventAddEx(modinfo->handle, "connthrottle_evt", 1, 0, connthrottle_evt, NULL); + return MOD_SUCCESS; +} + +MOD_UNLOAD(connthrottle) +{ + rehash_dump_settings(); + return MOD_SUCCESS; +} + +/** This function checks if the reputation module is loaded. + * If not, then the module will error, since we depend on it. + */ +int ct_config_posttest(int *errs) +{ + int errors = 0; + + /* Note: we use Callbacks[] here, but this is only for checking. Don't + * let this confuse you. At any other place you must use RCallbacks[]. + */ + if (Callbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL) + { + config_error("The 'connthrottle' module requires the 'reputation' " + "module to be loaded as well."); + config_error("Add the following to your configuration file: " + "loadmodule \"reputation\";"); + errors++; + } + + *errs = errors; + return errors ? -1 : 1; +} + +#ifndef CheckNull + #define CheckNull(x) if ((!(x)->ce_vardata) || (!(*((x)->ce_vardata)))) { config_error("%s:%i: missing parameter", (x)->ce_fileptr->cf_filename, (x)->ce_varlinenum); errors++; continue; } +#endif +/** Test the set::connthrottle configuration */ +int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) +{ + int errors = 0; + ConfigEntry *cep, *cepp; + + if (type != CONFIG_SET) + return 0; + + /* We are only interrested in set::connthrottle.. */ + if (!ce || !ce->ce_varname || strcmp(ce->ce_varname, "connthrottle")) + return 0; + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!strcmp(cep->ce_varname, "known-users")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + CheckNull(cepp); + if (!strcmp(cepp->ce_varname, "minimum-reputation-score")) + { + int cnt = atoi(cepp->ce_vardata); + if (cnt < 1) + { + config_error("%s:%i: set::connthrottle::known-users::minimum-reputation-score should be at least 1", + cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum); + errors++; + continue; + } + } else + if (!strcmp(cepp->ce_varname, "sasl-bypass")) + { + } else + { + config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum, + "set::connthrottle::known-users", cepp->ce_varname); + errors++; + } + } + } else + if (!strcmp(cep->ce_varname, "new-users")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + CheckNull(cepp); + if (!strcmp(cepp->ce_varname, "local-throttle")) + { + int cnt, period; + if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) || + (cnt < 1) || (cnt > 2000000000) || (period > 2000000000)) + { + config_error("%s:%i: set::connthrottle::new-users::local-throttle error. " + "Syntax is : (eg 6:60), " + "and count and period should be non-zero.", + cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum); + errors++; + continue; + } + } else + if (!strcmp(cepp->ce_varname, "global-throttle")) + { + int cnt, period; + if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) || + (cnt < 1) || (cnt > 2000000000) || (period > 2000000000)) + { + config_error("%s:%i: set::connthrottle::new-users::global-throttle error. " + "Syntax is : (eg 6:60), " + "and count and period should be non-zero.", + cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum); + errors++; + continue; + } + } else + { + config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum, + "set::connthrottle::new-users", cepp->ce_varname); + errors++; + } + } + } else + if (!strcmp(cep->ce_varname, "disabled-when")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + CheckNull(cepp); + if (!strcmp(cepp->ce_varname, "start-delay")) + { + int cnt = config_checkval(cepp->ce_vardata, CFG_TIME); + if ((cnt < 0) || (cnt > 3600)) + { + config_error("%s:%i: set::connthrottle::disabled-when::start-delay should be in range 0-3600", + cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum); + errors++; + continue; + } + } else + if (!strcmp(cepp->ce_varname, "reputation-gathering")) + { + } else + { + config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum, + "set::connthrottle::disabled-when", cepp->ce_varname); + errors++; + } + } + } else + if (!strcmp(cep->ce_varname, "reason")) + { + CheckNull(cep); + } else + { + config_error("%s:%i: unknown directive set::connthrottle::%s", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + errors++; + continue; + } + } + + *errs = errors; + return errors ? -1 : 1; +} + +/* Configure ourselves based on the set::connthrottle settings */ +int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type) +{ + ConfigEntry *cep, *cepp; + + if (type != CONFIG_SET) + return 0; + + /* We are only interrested in set::connthrottle.. */ + if (!ce || !ce->ce_varname || strcmp(ce->ce_varname, "connthrottle")) + return 0; + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!strcmp(cep->ce_varname, "known-users")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + if (!strcmp(cepp->ce_varname, "minimum-reputation-score")) + cfg.minimum_reputation_score = atoi(cepp->ce_vardata); + else if (!strcmp(cepp->ce_varname, "sasl-bypass")) + cfg.sasl_bypass = config_checkval(cepp->ce_vardata, CFG_YESNO); + } + } else + if (!strcmp(cep->ce_varname, "new-users")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + if (!strcmp(cepp->ce_varname, "local-throttle")) + config_parse_flood(cepp->ce_vardata, &cfg.local.count, &cfg.local.period); + else if (!strcmp(cepp->ce_varname, "global-throttle")) + config_parse_flood(cepp->ce_vardata, &cfg.global.count, &cfg.global.period); + } + } else + if (!strcmp(cep->ce_varname, "disabled-when")) + { + for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) + { + if (!strcmp(cepp->ce_varname, "start-delay")) + cfg.start_delay = config_checkval(cepp->ce_vardata, CFG_TIME); + else if (!strcmp(cepp->ce_varname, "reputation-gathering")) + cfg.reputation_gathering = config_checkval(cepp->ce_vardata, CFG_TIME); + } + } else + if (!strcmp(cep->ce_varname, "reason")) + { + safefree(cfg.reason); + cfg.reason = MyMalloc(strlen(cep->ce_vardata)+16); + sprintf(cfg.reason, "Throttled: %s", cep->ce_vardata); + } + } + return 1; +} + +/** Returns 1 if the 'reputation' module is still gathering + * data, such as in the first week of when it is loaded. + * This behavior is configured via set::disabled-when::reputation-gathering + */ +int still_reputation_gathering(void) +{ + int v; + + if (RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL) + return 1; /* Reputation module not loaded, disable us */ + + v = RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME]->func.intfunc(); + + if (TStime() - v < cfg.reputation_gathering) + return 1; /* Still gathering reputation data (eg: first week) */ + + return 0; +} + +EVENT(connthrottle_evt) +{ + char buf[512]; + + if (ucounter.next_event > TStime()) + return; + ucounter.next_event = TStime() + 60; + + if (ucounter.rejected_clients) + { + snprintf(buf, sizeof(buf), + "[ConnThrottle] Stats for this server past 60 secs: Connections rejected: %d. Accepted: %d known user(s), %d SASL and %d new user(s).", + ucounter.rejected_clients, + ucounter.allowed_score, + ucounter.allowed_sasl, + ucounter.allowed_other); + + sendto_realops("%s", buf); + ircd_log(LOG_ERROR, "%s", buf); + } + + /* Reset stats for next message */ + ucounter.rejected_clients = 0; + ucounter.allowed_score = 0; + ucounter.allowed_sasl = 0; + ucounter.allowed_other = 0; + + ucounter.throttling_previous_minute = ucounter.throttling_this_minute; + ucounter.throttling_this_minute = 0; /* reset */ + ucounter.throttling_banner_displayed = 0; /* reset */ +} + +#define THROT_LOCAL 1 +#define THROT_GLOBAL 2 +int ct_pre_lconnect(aClient *sptr) +{ + int throttle=0; + int score; + + if (me.local->firsttime + cfg.start_delay > TStime()) + return 0; /* no throttle: start delay */ + + if (ucounter.disabled) + return 0; /* protection disabled: allow user */ + + if (still_reputation_gathering()) + return 0; /* still gathering reputation data */ + + if (cfg.sasl_bypass && IsLoggedIn(sptr)) + { + /* Allowed in: user authenticated using SASL */ + return 0; + } + + score = GetReputation(sptr); + if (score >= cfg.minimum_reputation_score) + { + /* Allowed in: IP has enough reputation ("known user") */ + return 0; + } + + /* If we reach this then the user is NEW */ + + /* +1 global client would reach global limit? */ + if ((TStime() - ucounter.global.t < cfg.global.period) && (ucounter.global.count+1 > cfg.global.count)) + throttle |= THROT_GLOBAL; + + /* +1 local client would reach local limit? */ + if ((TStime() - ucounter.local.t < cfg.local.period) && (ucounter.local.count+1 > cfg.local.count)) + throttle |= THROT_LOCAL; + + if (throttle) + { + ucounter.throttling_this_minute = 1; + ucounter.rejected_clients++; + /* We send the LARGE banner if throttling was activated */ + if (!ucounter.throttling_previous_minute && !ucounter.throttling_banner_displayed) + { + ircd_log(LOG_ERROR, "[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE."); + sendto_realops("[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE."); + sendto_realops("[ConnThrottle] Users with IP addresses that have not been seen before will be rejected above the set connection rate. Known users can still get in."); + sendto_realops("[ConnThrottle] For more information see https://www.unrealircd.org/docs/ConnThrottle"); + ucounter.throttling_banner_displayed = 1; + } + return exit_client(sptr, sptr, &me, cfg.reason); + } + + return 0; +} + +/** Increase the connect counter(s), nothing else. */ +void bump_connect_counter(int local_connect) +{ + if (local_connect) + { + /* Bump local connect counter */ + if (TStime() - ucounter.local.t >= cfg.local.period) + { + ucounter.local.t = TStime(); + ucounter.local.count = 1; + } else { + ucounter.local.count++; + } + } + + /* Bump global connect counter */ + if (TStime() - ucounter.global.t >= cfg.global.period) + { + ucounter.global.t = TStime(); + ucounter.global.count = 1; + } else { + ucounter.global.count++; + } +} + +int ct_lconnect(aClient *sptr) +{ + int score; + + if (me.local->firsttime + cfg.start_delay > TStime()) + return 0; /* no throttle: start delay */ + + if (ucounter.disabled) + return 0; /* protection disabled: allow user */ + + if (still_reputation_gathering()) + return 0; /* still gathering reputation data */ + + if (cfg.sasl_bypass && IsLoggedIn(sptr)) + { + /* Allowed in: user authenticated using SASL */ + ucounter.allowed_sasl++; + return 0; + } + + score = GetReputation(sptr); + if (score >= cfg.minimum_reputation_score) + { + /* Allowed in: IP has enough reputation ("known user") */ + ucounter.allowed_score++; + return 0; + } + + /* Allowed NEW user */ + ucounter.allowed_other++; + + bump_connect_counter(1); + + return 0; +} + +int ct_rconnect(aClient *sptr) +{ + if (sptr->srvptr && !IsSynched(sptr->srvptr)) + return 0; /* Netmerge: skip */ + + if (IsULine(sptr)) + return 0; /* U:lined, such as services: skip */ + +#if UNREAL_VERSION_TIME >= 201915 + /* On UnrealIRCd 4.2.3+ we can see the boot time (start time) + * of the remote server. This way we can apply the + * set::disabled-when::start-delay restriction on remote + * servers as well. + */ + if (sptr->srvptr && sptr->srvptr->serv && sptr->srvptr->serv->boottime && + (TStime() - sptr->srvptr->serv->boottime < cfg.start_delay)) + { + return 0; + } +#endif + + bump_connect_counter(0); + + return 0; +} + +static void ct_throttle_usage(aClient *sptr) +{ + sendnotice(sptr, "Usage: /THROTTLE [ON|OFF|STATUS|RESET]"); + sendnotice(sptr, " ON: Enabled protection"); + sendnotice(sptr, " OFF: Disables protection"); + sendnotice(sptr, " STATUS: Status report"); + sendnotice(sptr, " RESET: Resets all counters(&more)"); + sendnotice(sptr, "NOTE: All commands only affect this server. Remote servers are not affected."); +} + +int ct_throttle(aClient *cptr, aClient *sptr, int parc, char *parv[]) +{ + if (!IsOper(sptr)) + { + sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, sptr->name); + return 0; + } + + if ((parc < 2) || BadPtr(parv[1])) + { + ct_throttle_usage(sptr); + return 0; + } + + if (!strcasecmp(parv[1], "STATS") || !strcasecmp(parv[1], "STATUS")) + { + sendnotice(sptr, "STATUS:"); + if (ucounter.disabled) + { + sendnotice(sptr, "Module DISABLED on oper request. To re-enable, type: /THROTTLE ON"); + } else { + if (still_reputation_gathering()) + { + sendnotice(sptr, "Module DISABLED because the 'reputation' module has not gathered enough data yet (set::connthrottle::disabled-when::reputation-gathering)."); + } else + if (me.local->firsttime + cfg.start_delay > TStime()) + { + sendnotice(sptr, "Module DISABLED due to start-delay (set::connthrottle::disabled-when::start-delay), will be enabled in %ld second(s).", + (me.local->firsttime + cfg.start_delay) - TStime()); + } else + { + sendnotice(sptr, "Module ENABLED"); + } + } + } else + if (!strcasecmp(parv[1], "OFF")) + { + if (ucounter.disabled == 1) + { + sendnotice(sptr, "Already OFF"); + return 0; + } + ucounter.disabled = 1; + sendto_realops("[connthrottle] %s (%s@%s) DISABLED the connthrottle module.", + sptr->name, sptr->user->username, sptr->user->realhost); + } else + if (!strcasecmp(parv[1], "ON")) + { + if (ucounter.disabled == 0) + { + sendnotice(sptr, "Already ON"); + return 0; + } + sendto_realops("[connthrottle] %s (%s@%s) ENABLED the connthrottle module.", + sptr->name, sptr->user->username, sptr->user->realhost); + ucounter.disabled = 0; + } else + if (!strcasecmp(parv[1], "RESET")) + { + memset(&ucounter, 0, sizeof(ucounter)); + sendto_realops("[connthrottle] %s (%s@%s) did a RESET on the stats/counters!!", + sptr->name, sptr->user->username, sptr->user->realhost); + } else + { + sendnotice(sptr, "Unknown option '%s'", parv[1]); + ct_throttle_usage(sptr); + } + return 0; +} + +void rehash_dump_settings(void) +{ + FILE *fd = fopen(rehash_dump_filename, "w"); + + if (!fd) + { + config_status("WARNING: could not write to tmp/connthrottle.tmp (%s): " + "throttling counts and status will be RESET", strerror(errno)); + return; + } + fprintf(fd, "# THROTTLE DUMP v1 == DO NOT EDIT!\n"); + fprintf(fd, "TSME %ld\n", me.local->firsttime); + fprintf(fd, "TSNOW %ld\n", TStime()); + fprintf(fd, "next_event %ld\n", ucounter.next_event); + fprintf(fd, "local.count %d\n", ucounter.local.count); + fprintf(fd, "local.t %ld\n", ucounter.local.t); + fprintf(fd, "global.count %d\n", ucounter.global.count); + fprintf(fd, "global.t %ld\n", ucounter.global.t); + fprintf(fd, "rejected_clients %d\n", ucounter.rejected_clients); + fprintf(fd, "allowed_score %d\n", ucounter.allowed_score); + fprintf(fd, "allowed_sasl %d\n", ucounter.allowed_sasl); + fprintf(fd, "allowed_other %d\n", ucounter.allowed_other); + fprintf(fd, "disabled %d\n", (int)ucounter.disabled); + fprintf(fd, "throttling_this_minute %d\n", ucounter.throttling_this_minute); + fprintf(fd, "throttling_previous_minute %d\n", ucounter.throttling_previous_minute); + fprintf(fd, "throttling_banner_displayed %d\n", ucounter.throttling_banner_displayed); + if (fclose(fd)) + { + /* fclose(/fprintf) error */ + config_status("WARNING: error while writing to tmp/connthrottle.tmp (%s): " + "throttling counts and status will be RESET", strerror(errno)); + } +} + +/** Helper for rehash_read_settings() to parse connthrottle temp file */ +int parse_connthrottle_file(char *str, char **name, char **value) +{ + static char buf[512]; + char *p; + + /* Initialize */ + *name = *value = NULL; + strlcpy(buf, str, sizeof(buf)); + + /* Strtoken */ + p = strchr(buf, ' '); + if (!p) + return 0; + *p++ = '\0'; + + /* Success */ + *name = buf; + *value = p; + return 1; +} + +void rehash_read_settings(void) +{ + FILE *fd = fopen(rehash_dump_filename, "r"); + char buf[512], *name, *value; + time_t ts; + int num = 0; + + if (!fd) + return; + + /* 1. Check header */ + if (!fgets(buf, sizeof(buf), fd) || strncmp(buf, "# THROTTLE DUMP v1 == DO NOT EDIT!", 34)) + { + config_status("WARNING: tmp/connthrottle.tmp corrupt (I)"); + fclose(fd); + return; + } + + /* 2. Check if boottime matches exactly */ + if (!fgets(buf, sizeof(buf), fd) || + !parse_connthrottle_file(buf, &name, &value) || + strcmp(name, "TSME")) + { + config_status("WARNING: tmp/connthrottle.tmp corrupt (II)"); + fclose(fd); + return; + } + ts = atoi(buf+5); + if (ts != me.local->firsttime) /* Not rehashing, possible restart or die */ + { + fclose(fd); +#ifdef DEBUGMODE + config_status("ts!=me.local->firsttime: ts=%ld, me.local->firsttime=%ld", + ts, me.local->firsttime); +#endif + unlink("tmp/connthrottle.tmp"); + return; + } + + /* 3. Now parse the rest */ + while((fgets(buf, sizeof(buf), fd))) + { + if (!parse_connthrottle_file(buf, &name, &value)) + { + config_warn("Corrupt connthrottle temp file. Settings may be lost."); + continue; + } + + if (!strcmp(name, "TSNOW")) + { + /* Ignored */ + } else + if (!strcmp(name, "next_event")) + { ucounter.next_event = atol(value); + num++; + } else + if (!strcmp(name, "local.count")) + { ucounter.local.count = atoi(value); + num++; + } else + if (!strcmp(name, "local.t")) + { + ucounter.local.t = atol(value); + num++; + } else + if (!strcmp(name, "global.count")) + { + ucounter.global.count = atoi(value); + num++; + } else + if (!strcmp(name, "global.t")) + { + ucounter.global.t = atol(value); + num++; + } else + if (!strcmp(name, "rejected_clients")) + { + ucounter.rejected_clients = atoi(value); + num++; + } else + if (!strcmp(name, "allowed_score")) + { + ucounter.allowed_score = atoi(value); + num++; + } else + if (!strcmp(name, "allowed_sasl")) + { + ucounter.allowed_sasl = atoi(value); + num++; + } else + if (!strcmp(name, "allowed_other")) + { + ucounter.allowed_other = atoi(value); + num++; + } else + if (!strcmp(name, "disabled")) + { + ucounter.disabled = (char)atoi(value); + num++; + } else + if (!strcmp(name, "throttling_this_minute")) + { + ucounter.throttling_this_minute = atoi(value); + num++; + } else + if (!strcmp(name, "throttling_previous_minute")) + { + ucounter.throttling_previous_minute = atoi(value); + num++; + } else + if (!strcmp(name, "throttling_banner_displayed")) + { + ucounter.throttling_banner_displayed = atoi(value); + num++; + } else + { + config_warn("[BUG] Unknown variable in temporary connthrottle file: %s", name); + } + } + fclose(fd); + #define EXPECT_VAR_COUNT 13 + if (num != EXPECT_VAR_COUNT) + { + config_status("[connthrottle] WARNING: Only %d variables read but expected %d: " + "some information may have been lost during the rehash!", + num, EXPECT_VAR_COUNT); + } +} diff --git a/src/modules/reputation.c b/src/modules/reputation.c new file mode 100644 index 000000000..7f2dd2990 --- /dev/null +++ b/src/modules/reputation.c @@ -0,0 +1,900 @@ +/* + * reputation - Provides a scoring system for "known users". + * (C) Copyright 2015-2019 Bram Matthys (Syzop) and the UnrealIRCd team. + * License: GPLv2 + * + * How this works is simple: + * Every 5 minutes the IP address of all the connected users receive + * a point. Registered users receive 2 points every 5 minutes. + * The total reputation score is then later used, by other modules, for + * example to make decisions such as to reject or allow a user if the + * server is under attack. + * The reputation scores are saved in a database. By default this file + * is data/reputation.db (often ~/unrealircd/data/reputation.db). + * + * See also https://www.unrealircd.org/docs/Connthrottle + */ + +#include "unrealircd.h" + +#define REPUTATION_VERSION "1.0.1" + +#undef TEST + +#define BENCHMARK +/* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux): + * 10k random IP's with various expire times: + * - load db: 23 ms + * - expiry: 1 ms + * - save db: 7 ms + * 100k random IP's with various expire times: + * - load db: 103 ms + * - expiry: 10 ms + * - save db: 32 ms + * So, even for 100,000 unique IP's, the initial load of the database + * would delay the UnrealIRCd boot process only for 0.1 second. + * The writing of the db, which happens every 5 minutes, for such + * amount of IP's takes 32ms (0.03 second). + * Of course, exact figures will depend on the storage and cache. + * That being said, the file for 100k random IP's is slightly under + * 3MB, so not big, which likely means the timing will be similar + * for a broad number of (storage) systems. + */ + +#ifndef TEST + #define BUMP_SCORE_EVERY 300 + #define DELETE_OLD_EVERY 605 + #define SAVE_DB_EVERY 902 +#else + #define BUMP_SCORE_EVERY 3 + #define DELETE_OLD_EVERY 3 + #define SAVE_DB_EVERY 3 +#endif + +#ifndef CALLBACKTYPE_REPUTATION_STARTTIME + #define CALLBACKTYPE_REPUTATION_STARTTIME 5 +#endif + +ModuleHeader MOD_HEADER(reputation) + = { + "reputation", + REPUTATION_VERSION, + "Known IP's scoring system", + "3.2-b8-1", + NULL + }; + +#define MAXEXPIRES 10 + +#define REPUTATION_SCORE_CAP 10000 + +#define UPDATE_SCORE_MARGIN 1 + +#define Reputation(acptr) moddata_client(acptr, reputation_md).l + +struct cfgstruct { + int expire_score[MAXEXPIRES]; + long expire_time[MAXEXPIRES]; + char *database; +}; +static struct cfgstruct cfg; + +typedef struct reputationentry ReputationEntry; + +struct reputationentry { + ReputationEntry *prev, *next; + unsigned short score; /**< score for the user */ + long last_seen; /**< user last seen (unix timestamp) */ + int marker; /**< internal marker, not written to db */ + char ip[1]; /*< ip address */ +}; + +long reputation_starttime = 0; +long reputation_writtentime = 0; + +#define REPUTATION_HASH_SIZE 1327 +static ReputationEntry *ReputationHashTable[REPUTATION_HASH_SIZE]; + +static ModuleInfo ModInf; + +ModDataInfo *reputation_md; /* Module Data structure which we acquire */ + +/* Forward declarations */ +void reputation_md_free(ModData *m); +char *reputation_md_serialize(ModData *m); +void reputation_md_unserialize(char *str, ModData *m); +void config_setdefaults(void); +CMD_FUNC(reputation_cmd); +CMD_FUNC(reputationunperm); +int reputation_whois(aClient *sptr, aClient *acptr); +int reputation_handshake(aClient *sptr); +int reputation_pre_lconnect(aClient *sptr); +int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); +int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type); +int reputation_config_posttest(int *errs); +unsigned long hash_djb2(char *str); +int hash_reputation_entry(char *ip); +void add_reputation_entry(ReputationEntry *e); +EVENT(delete_old_records); +EVENT(add_scores); +EVENT(save_db_evt); +void load_db(void); +void save_db(void); +int reputation_starttime_callback(void); + +MOD_TEST(reputation) +{ + memcpy(&ModInf, modinfo, modinfo->size); + memset(&cfg, 0, sizeof(cfg)); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reputation_config_test); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, reputation_config_posttest); + CallbackAddEx(modinfo->handle, CALLBACKTYPE_REPUTATION_STARTTIME, reputation_starttime_callback); + return MOD_SUCCESS; +} + +MOD_INIT(reputation) +{ + ModDataInfo mreq; + + MARK_AS_OFFICIAL_MODULE(modinfo); + ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1); + memset(&ReputationHashTable, 0, sizeof(ReputationHashTable)); + + memset(&mreq, 0, sizeof(mreq)); + mreq.name = "reputation"; + mreq.free = reputation_md_free; + mreq.serialize = reputation_md_serialize; + mreq.unserialize = reputation_md_unserialize; + mreq.sync = 0; /* local! */ + mreq.type = MODDATATYPE_CLIENT; + reputation_md = ModDataAdd(modinfo->handle, mreq); + if (!reputation_md) + abort(); + + config_setdefaults(); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reputation_config_run); + HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, reputation_whois); + HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, reputation_handshake); + HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 2000000000, reputation_pre_lconnect); /* (prio: last) */ + CommandAdd(ModInf.handle, "REPUTATION", reputation_cmd, MAXPARA, M_USER|M_SERVER); + CommandAdd(ModInf.handle, "REPUTATIONUNPERM", reputationunperm, MAXPARA, M_USER|M_SERVER); + return MOD_SUCCESS; +} + +MOD_LOAD(reputation) +{ + load_db(); + if (reputation_starttime == 0) + reputation_starttime = TStime(); + EventAddEx(ModInf.handle, "delete_old_records", DELETE_OLD_EVERY, 0, delete_old_records, NULL); + EventAddEx(ModInf.handle, "add_scores", BUMP_SCORE_EVERY, 0, add_scores, NULL); + EventAddEx(ModInf.handle, "save_db", SAVE_DB_EVERY, 0, save_db_evt, NULL); + return MOD_SUCCESS; +} + +MOD_UNLOAD(reputation) +{ + save_db(); + return MOD_SUCCESS; +} + +void config_setdefaults(void) +{ + /* data/reputation.db */ + cfg.database = strdup("reputation.db"); + convert_to_absolute_path(&cfg.database, PERMDATADIR); + + /* EXPIRES the following entries if the IP does appear for some time: */ + /* <=2 points after 1 hour */ + cfg.expire_score[0] = 2; +#ifndef TEST + cfg.expire_time[0] = 3600; +#else + cfg.expire_time[0] = 36; +#endif + /* <=6 points after 7 days */ + cfg.expire_score[1] = 6; + cfg.expire_time[1] = 86400*7; + /* ANY result that has not been seen for 30 days */ + cfg.expire_score[2] = -1; + cfg.expire_time[2] = 86400*30; +} + +int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) +{ + int errors = 0; + ConfigEntry *cep; + + if (type != CONFIG_SET) + return 0; + + /* We are only interrested in set::reputation.. */ + if (!ce || strcmp(ce->ce_varname, "reputation")) + return 0; + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!cep->ce_vardata) + { + config_error("%s:%i: blank set::reputation::%s without value", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + errors++; + continue; + } else + if (!strcmp(cep->ce_varname, "database")) + { + convert_to_absolute_path(&cep->ce_vardata, PERMDATADIR); + } else + { + config_error("%s:%i: unknown directive set::reputation::%s", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + errors++; + continue; + } + } + + *errs = errors; + return errors ? -1 : 1; +} + +int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type) +{ + ConfigEntry *cep; + + if (type != CONFIG_SET) + return 0; + + /* We are only interrested in set::reputation.. */ + if (!ce || strcmp(ce->ce_varname, "reputation")) + return 0; + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!strcmp(cep->ce_varname, "database")) + { + safestrdup(cfg.database, cep->ce_vardata); + } + } + return 1; +} + +int reputation_config_posttest(int *errs) +{ + int errors = 0; + + *errs = errors; + return errors ? -1 : 1; +} + +/** Cut off string on first occurance of CR or LF */ +void stripcrlf(char *buf) +{ + for (; *buf; buf++) + { + if ((*buf == '\n') || (*buf == '\r')) + { + *buf = '\0'; + return; + } + } +} + +/** Parse database header and set variables appropriately */ +int parse_db_header(char *buf) +{ + char *header=NULL, *version=NULL, *starttime=NULL, *writtentime=NULL; + char *p=NULL; + + if (strncmp(buf, "REPDB", 5)) + return 0; + + header = strtoken(&p, buf, " "); + if (!header) + return 0; + + version = strtoken(&p, NULL, " "); + if (!version || (atoi(version) != 1)) + return 0; + + starttime = strtoken(&p, NULL, " "); + if (!starttime) + return 0; + + writtentime = strtoken(&p, NULL, " "); + if (!writtentime) + return 0; + + reputation_starttime = atol(starttime); + reputation_writtentime = atol(writtentime); + + return 1; +} + +void load_db(void) +{ + FILE *fd; + char buf[512], *p; +#ifdef BENCHMARK + struct timeval tv_alpha, tv_beta; + + gettimeofday(&tv_alpha, NULL); +#endif + + fd = fopen(cfg.database, "r"); + if (!fd) + { + config_error("WARNING: Could not open/read database '%s': %s", cfg.database, strerror(ERRNO)); + return; + } + + memset(buf, 0, sizeof(buf)); + if (fgets(buf, 512, fd) == NULL) + { + config_error("WARNING: Database file corrupt ('%s')", cfg.database); + fclose(fd); + return; + } + + /* Header contains: REPDB + * Where: + * REPDB: Literally the string "REPDB". + * This is version 1 at the time of this writing. + * The time that recording of reputation started, + * in other words: when this module was first loaded, ever. + * Time that the database was last written. + */ + if (!parse_db_header(buf)) + { + config_error("WARNING: Cannot load database %s. Error reading header. " + "Database corrupt? Or are you downgrading from a newer " + "UnrealIRCd version perhaps? This is not supported.", + cfg.database); + fclose(fd); + return; + } + + while(fgets(buf, 512, fd) != NULL) + { + char *ip = NULL, *score = NULL, *last_seen = NULL; + ReputationEntry *e; + + stripcrlf(buf); + /* Format: */ + ip = strtoken(&p, buf, " "); + if (!ip) + continue; + score = strtoken(&p, NULL, " "); + if (!score) + continue; + last_seen = strtoken(&p, NULL, " "); + if (!last_seen) + continue; + + e = MyMallocEx(sizeof(ReputationEntry)+strlen(ip)); + strcpy(e->ip, ip); /* safe, see alloc above */ + e->score = atoi(score); + e->last_seen = atol(last_seen); + + add_reputation_entry(e); + } + fclose(fd); + +#ifdef BENCHMARK + gettimeofday(&tv_beta, NULL); + ircd_log(LOG_ERROR, "Reputation benchmark: LOAD DB: %ld microseconds", + ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)); +#endif +} + +void save_db(void) +{ + FILE *fd; + char tmpfname[512]; + char buf[512], *p; + int i; + ReputationEntry *e; +#ifdef BENCHMARK + struct timeval tv_alpha, tv_beta; + + gettimeofday(&tv_alpha, NULL); +#endif + +#ifdef TEST + sendto_realops("REPUTATION IS RUNNING IN TEST MODE. SAVING DB'S..."); +#endif + + /* We write to a temporary file. Only to rename it later if everything was ok */ + snprintf(tmpfname, sizeof(tmpfname), "%s.tmp", cfg.database); + + fd = fopen(tmpfname, "w"); + if (!fd) + { + config_error("ERROR: Could not open/write database '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO)); + return; + } + + if (fprintf(fd, "REPDB 1 %ld %ld\n", reputation_starttime, TStime()) < 0) + goto write_fail; + + for (i = 0; i < REPUTATION_HASH_SIZE; i++) + { + for (e = ReputationHashTable[i]; e; e = e->next) + { + if (fprintf(fd, "%s %d %ld\n", e->ip, (int)e->score, e->last_seen) < 0) + { +write_fail: + config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO)); + fclose(fd); + return; + } + } + } + + if (fclose(fd) < 0) + { + config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO)); + return; + } + + /* Everything went fine. We rename our temporary file to the existing + * DB file (will overwrite), which is more or less an atomic operation. + */ + if (rename(tmpfname, cfg.database) < 0) + { + config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!", + tmpfname, cfg.database, strerror(ERRNO)); + return; + } + + reputation_writtentime = TStime(); + +#ifdef BENCHMARK + gettimeofday(&tv_beta, NULL); + ircd_log(LOG_ERROR, "Reputation benchmark: SAVE DB: %ld microseconds", + ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)); +#endif + + return; +} + +/* One of DJB2's hashing algorithm. Modified to use tolower(). */ +unsigned long hash_djb2(char *str) +{ + unsigned long hash = 5381; + int c; + + while ((c = *str++)) + hash = ((hash << 5) + hash) + tolower(c); /* hash * 33 + c */ + + return hash; +} + +int hash_reputation_entry(char *ip) +{ + unsigned long alpha, beta, result; + + return hash_djb2(ip) % REPUTATION_HASH_SIZE; +} + +void add_reputation_entry(ReputationEntry *e) +{ + int hashv = hash_reputation_entry(e->ip); + + AddListItem(e, ReputationHashTable[hashv]); +} + +ReputationEntry *find_reputation_entry(char *ip) +{ + ReputationEntry *e; + int hashv = hash_reputation_entry(ip); + + for (e = ReputationHashTable[hashv]; e; e = e->next) + if (!strcmp(e->ip, ip)) + return e; + + return NULL; +} + +/** Called when the user connects (very early, just after the + * TCP/IP connection has been established, before any data). + */ +int reputation_handshake(aClient *acptr) +{ + char *ip = acptr->ip; + ReputationEntry *e; + + if (ip) + { + e = find_reputation_entry(ip); + if (e) + { + Reputation(acptr) = e->score; /* SET MODDATA */ + } + } + return 0; +} + +int reputation_pre_lconnect(aClient *sptr) +{ + /* User will likely be accepted. Inform other servers about the score + * we have for this user. For more information about this type of + * server to server traffic, see the reputation_server_cmd function. + */ + ReputationEntry *e = find_reputation_entry(GetIP(sptr)); + sendto_server(NULL, 0, 0, ":%s REPUTATION %s %hd", me.name, GetIP(sptr), e ? e->score : 0); + + return 0; +} + +EVENT(add_scores) +{ + static int marker = 0; + char *ip; + aClient *acptr; + ReputationEntry *e; + + /* This marker is used so we only bump score for an IP entry + * once and not twice (or more) if there are multiple users + * with the same IP address. + */ + marker += 2; + + /* These macros make the code below easier to read. Also, + * this explains why we just did marker+=2 and not marker++. + */ + #define MARKER_UNREGISTERED_USER (marker) + #define MARKER_REGISTERED_USER (marker+1) + + list_for_each_entry(acptr, &client_list, client_node) + { + if (!IsPerson(acptr)) + continue; /* skip servers, unknowns, etc.. */ + + ip = acptr->ip; + if (!ip) + continue; + + e = find_reputation_entry(ip); + if (!e) + { + /* Create */ + e = MyMallocEx(sizeof(ReputationEntry)+strlen(ip)); + strcpy(e->ip, ip); /* safe, allocated above */ + add_reputation_entry(e); + } + + /* If this is not a duplicate entry, then bump the score.. */ + if ((e->marker != MARKER_UNREGISTERED_USER) && (e->marker != MARKER_REGISTERED_USER)) + { + e->marker = MARKER_UNREGISTERED_USER; + if (e->score < REPUTATION_SCORE_CAP) + { + /* Regular users receive a point. */ + e->score++; + /* Registered users receive an additional point */ + if (IsLoggedIn(acptr) && (e->score < REPUTATION_SCORE_CAP)) + { + e->score++; + e->marker = MARKER_REGISTERED_USER; + } + } + } else + if ((e->marker == MARKER_UNREGISTERED_USER) && IsLoggedIn(acptr) && (e->score < REPUTATION_SCORE_CAP)) + { + /* This is to catch a special case: + * If there are 2 or more users with the same IP + * address and the first user was not registered + * then the IP entry only received a score bump of +1. + * If the 2nd user (with same IP) is a registered + * user then the IP should actually receive a + * score bump of +2 (in total). + */ + e->score++; + e->marker = MARKER_REGISTERED_USER; + } + + e->last_seen = TStime(); + Reputation(acptr) = e->score; /* update moddata */ + } +} + +/** Is this entry expired? */ +static inline int is_reputation_expired(ReputationEntry *e) +{ + int i; + for (i = 0; i < MAXEXPIRES; i++) + { + if (cfg.expire_time[i] == 0) + break; /* end of all entries */ + if ((e->score <= cfg.expire_score[i]) && (TStime() - e->last_seen > cfg.expire_time[i])) + return 1; + } + return 0; +} + +EVENT(delete_old_records) +{ + int i; + ReputationEntry *e, *e_next; +#ifdef BENCHMARK + struct timeval tv_alpha, tv_beta; + + gettimeofday(&tv_alpha, NULL); +#endif + + for (i = 0; i < REPUTATION_HASH_SIZE; i++) + { + for (e = ReputationHashTable[i]; e; e = e_next) + { + e_next = e->next; + + if (is_reputation_expired(e)) + { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "Deleting expired entry for '%s' (score %hd, last seen %ld seconds ago)", + e->ip, e->score, TStime() - e->last_seen); +#endif + DelListItem(e, ReputationHashTable[i]); + MyFree(e); + } + } + } + +#ifdef BENCHMARK + gettimeofday(&tv_beta, NULL); + ircd_log(LOG_ERROR, "Reputation benchmark: EXPIRY IN MEM: %ld microseconds", + ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)); +#endif +} + +EVENT(save_db_evt) +{ + save_db(); +} + +CMD_FUNC(reputationunperm) +{ + if (!IsOper(sptr)) + { + sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, sptr->name); + return 0; + } + + ModuleSetOptions(ModInf.handle, MOD_OPT_PERM, 0); + + sendto_realops("%s used /REPUTATIONUNPERM. On next REHASH the module can be RELOADED or UNLOADED. " + "Note however that for a few minutes the scoring may be skipped, so don't do this too often.", + sptr->name); + return 0; +} + +int count_reputation_records(void) +{ + int i; + ReputationEntry *e; + int total = 0; + + for (i = 0; i < REPUTATION_HASH_SIZE; i++) + for (e = ReputationHashTable[i]; e; e = e->next) + total++; + + return total; +} + +CMD_FUNC(reputation_user_cmd) +{ + ReputationEntry *e; + char *ip; + + if (!IsOper(sptr)) + { + sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, sptr->name); + return 0; + } + + if ((parc < 2) || BadPtr(parv[1])) + { + sendnotice(sptr, "Reputation module statistics:"); + sendnotice(sptr, "Recording for: %ld seconds (since unixtime %ld)", + TStime() - reputation_starttime, reputation_starttime); + if (reputation_writtentime) + { + sendnotice(sptr, "Last successful db write: %ld seconds ago (unixtime %ld)", + TStime() - reputation_writtentime, reputation_writtentime); + } else { + sendnotice(sptr, "Last successful db write: never"); + } + sendnotice(sptr, "Current number of records (IP's): %d", count_reputation_records()); + sendnotice(sptr, "-"); + sendnotice(sptr, "For more specific information, use: /REPUTATION [nick|IP-address]"); + return 0; + } + + if (strchr(parv[1], '.') || strchr(parv[1], ':')) + { + ip = parv[1]; + } else { + aClient *acptr = find_person(parv[1], NULL); + if (!acptr) + { + sendto_one(sptr, err_str(ERR_NOSUCHNICK), me.name, sptr->name, parv[1]); + return 0; + } + ip = acptr->ip; + if (!ip) + { + sendnotice(sptr, "No IP address information available for user '%s'.", parv[1]); /* e.g. services */ + return 0; + } + } + + e = find_reputation_entry(ip); + if (!e) + { + sendnotice(sptr, "No reputation record found for IP %s", ip); + return 0; + } + + sendnotice(sptr, "****************************************************"); + sendnotice(sptr, "Reputation record for IP %s:", ip); + sendnotice(sptr, " Score: %hd", e->score); + sendnotice(sptr, "Last seen: %ld seconds ago (unixtime: %ld)", + TStime() - e->last_seen, e->last_seen); + sendnotice(sptr, "****************************************************"); + return 0; +} + +/** The REPUTATION server command handler. + * Syntax: :server REPUTATION + * Where the may be prefixed by an asterisk (*). + * + * The best way to explain this command is to illustrate by example: + * :servera REPUTATION 1.2.3.4 0 + * Then serverb, which might have a score of 2 for this IP, will: + * - Send back to the servera direction: :serverb REPUTATION 1.2.3.4 *2 + * So the original server (and direction) receive a score update. + * - Propagate to non-servera direction: :servera REPUTATION 1.2.3.4 2 + * So use the new higher score (2 rather than 0). + * Then the next server may do the same. It MUST propagate to non-serverb + * direction and MAY (again) update the score even higher. + * + * If the score is not prefixed by * then the server may do as above and + * send back to the uplink an "update" of the score. If, however, the + * score is prefixed by * then the server will NEVER send back to the + * uplink, it may only propagate. This is to prevent loops. + * + * Note that some margin is used when deciding if the server should send + * back score updates. This is defined by UPDATE_SCORE_MARGIN. + * If this is for example set to 1 then a point difference of 1 will not + * yield a score update since such a minor score update is not worth the + * server to server traffic. Also, due to timing differences a score + * difference of 1 is quite likely to hapen in normal circumstances. + */ +CMD_FUNC(reputation_server_cmd) +{ + ReputationEntry *e; + char *ip; + int score; + long since; + int allow_reply; + + /* :server REPUTATION */ + if ((parc < 3) || BadPtr(parv[2])) + { + sendto_one(sptr, err_str(ERR_NEEDMOREPARAMS), me.name, sptr->name, "REPUTATION"); + return 0; + } + + ip = parv[1]; + + if (parv[2][0] == '*') + { + allow_reply = 0; + score = atoi(parv[2]+1); + } else { + allow_reply = 1; + score = atoi(parv[2]); + } + + if (score > REPUTATION_SCORE_CAP) + score = REPUTATION_SCORE_CAP; + + e = find_reputation_entry(ip); + if (allow_reply && e && (e->score > score) && (e->score - score > UPDATE_SCORE_MARGIN)) + { + /* We have a higher score, inform the cptr direction about it. + * This will prefix the score with a * so servers will never reply to it. + */ + sendto_one(cptr, ":%s REPUTATION %s *%d", me.name, parv[1], e->score); +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[reputation] Score for '%s' from %s is %d, but we have %d, sending back %d", + ip, sptr->name, score, e->score, e->score); +#endif + score = e->score; /* Update for propagation in the non-cptr direction */ + } + + /* Update our score if sender has a higher score */ + if (e && (score > e->score)) + { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[reputation] Score for '%s' from %s is %d, but we have %d, updating our score to %d", + ip, sptr->name, score, e->score, score); +#endif + e->score = score; + } + + /* If we don't have any entry for this IP, add it now. */ + if (!e && (score > 0)) + { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[reputation] Score for '%s' from %s is %d, we had no entry, adding it", + ip, sptr->name, score); +#endif + e = MyMallocEx(sizeof(ReputationEntry)+strlen(ip)); + strcpy(e->ip, ip); /* safe, see alloc above */ + e->score = score; + e->last_seen = TStime(); + add_reputation_entry(e); + } + + /* Propagate to the non-cptr direction (score may be updated) */ + sendto_server(cptr, 0, 0, ":%s REPUTATION %s %s%d", + sptr->name, + parv[1], + allow_reply ? "" : "*", + score); + + return 0; +} + +CMD_FUNC(reputation_cmd) +{ + if (MyClient(sptr)) + return reputation_user_cmd(cptr, sptr, parc, parv); + + if (IsServer(sptr)) + return reputation_server_cmd(cptr, sptr, parc, parv); + return 0; +} + +int reputation_whois(aClient *sptr, aClient *acptr) +{ + int reputation = Reputation(acptr); + + if (!IsOper(sptr)) + return 0; /* only opers can see this.. */ + + if (reputation > 0) + { + sendto_one(sptr, ":%s %d %s %s :is using an IP with a reputation score of %d", + me.name, RPL_WHOISSPECIAL, sptr->name, + acptr->name, reputation); + } + return 0; +} + +void reputation_md_free(ModData *m) +{ + /* we have nothing to free actually, but we must set to zero */ + m->l = 0; +} + +char *reputation_md_serialize(ModData *m) +{ + static char buf[32]; + if (m->i == 0) + return NULL; /* not set (reputation always starts at 1) */ + snprintf(buf, sizeof(buf), "%d", m->i); + return buf; +} + +void reputation_md_unserialize(char *str, ModData *m) +{ + m->i = atoi(str); +} + +int reputation_starttime_callback(void) +{ + /* NB: fix this by 2038 */ + return (int)reputation_starttime; +}