mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-06-12 17:14:46 +02:00
Add 'reputation' and 'connthrottle' modules to fight drones.
See https://www.unrealircd.org/docs/Connthrottle
This commit is contained in:
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+10
-1
@@ -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
|
||||
#############################################################################
|
||||
|
||||
@@ -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 <count>:<period> (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 <count>:<period> (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);
|
||||
}
|
||||
}
|
||||
@@ -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 <version> <starttime> <writtentime>
|
||||
* Where:
|
||||
* REPDB: Literally the string "REPDB".
|
||||
* <version> This is version 1 at the time of this writing.
|
||||
* <starttime> The time that recording of reputation started,
|
||||
* in other words: when this module was first loaded, ever.
|
||||
* <writtentime> 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> <score> <last seen> */
|
||||
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 <ip> <score>
|
||||
* Where the <score> 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 <ip> <score> */
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user