pissircd/src/modules/reputation.c

1530 lines
42 KiB
C

/*
* reputation - Provides a scoring system for "known users".
* (C) Copyright 2015-2019 Bram Matthys (Syzop) and the UnrealIRCd team.
* License: GPLv2 or later
*
* 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.2"
/* Change to #define to benchmark. Note that this will add random
* reputation entries so should never be used on production servers!!!
*/
#undef BENCHMARK
#undef TEST
/* 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_VERSION,
"Known IP's scoring system",
"UnrealIRCd Team",
"unrealircd-6",
};
/* Defines */
#define MAXEXPIRES 10
#define REPUTATION_SCORE_CAP 10000
#define UPDATE_SCORE_MARGIN 1
#define REPUTATION_HASH_TABLE_SIZE 2048
#define Reputation(client) moddata_client(client, reputation_md).l
#define WARN_WRITE_ERROR(fname) \
do { \
unreal_log(ULOG_ERROR, "reputation", "REPUTATION_FILE_WRITE_ERROR", NULL, \
"[reputation] Error writing to temporary database file $filename: $system_error", \
log_data_string("filename", fname), \
log_data_string("system_error", unrealdb_get_error_string())); \
} while(0)
#define W_SAFE(x) \
do { \
if (!(x)) { \
WARN_WRITE_ERROR(tmpfname); \
unrealdb_close(db); \
return 0; \
} \
} while(0)
/* Definitions (structs, etc.) */
struct cfgstruct {
int expire_score[MAXEXPIRES];
long expire_time[MAXEXPIRES];
char *database;
char *db_secret;
int require_minimum_channel_members;
};
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 */
};
/* Global variables */
static struct cfgstruct cfg; /**< Current configuration */
static struct cfgstruct test; /**< Testing configuration (not active yet) */
long reputation_starttime = 0;
long reputation_writtentime = 0;
static ReputationEntry *ReputationHashTable[REPUTATION_HASH_TABLE_SIZE];
static char siphashkey_reputation[SIPHASH_KEY_LENGTH];
static ModuleInfo ModInf;
ModDataInfo *reputation_md; /* Module Data structure which we acquire */
/* Forward declarations */
void reputation_md_free(ModData *m);
const char *reputation_md_serialize(ModData *m);
void reputation_md_unserialize(const char *str, ModData *m);
void reputation_config_setdefaults(struct cfgstruct *cfg);
void reputation_free_config(struct cfgstruct *cfg);
CMD_FUNC(reputation_cmd);
CMD_FUNC(reputationunperm);
int reputation_whois(Client *client, Client *target, NameValuePrioList **list);
int reputation_set_on_connect(Client *client);
int reputation_pre_lconnect(Client *client);
int reputation_ip_change(Client *client, const char *oldip);
int reputation_connect_extinfo(Client *client, NameValuePrioList **list);
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);
static uint64_t hash_reputation_entry(const char *ip);
ReputationEntry *find_reputation_entry(const char *ip);
void add_reputation_entry(ReputationEntry *e);
EVENT(delete_old_records);
EVENT(add_scores);
EVENT(reputation_save_db_evt);
int reputation_load_db(void);
int reputation_save_db(void);
int reputation_starttime_callback(void);
void _ban_act_set_reputation(Client *client, BanAction *action);
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
memcpy(&ModInf, modinfo, modinfo->size);
memset(&cfg, 0, sizeof(cfg));
memset(&test, 0, sizeof(cfg));
reputation_config_setdefaults(&test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reputation_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, reputation_config_posttest);
CallbackAdd(modinfo->handle, CALLBACKTYPE_REPUTATION_STARTTIME, reputation_starttime_callback);
EfunctionAddVoid(modinfo->handle, EFUNC_BAN_ACT_SET_REPUTATION, _ban_act_set_reputation);
return MOD_SUCCESS;
}
MOD_INIT()
{
ModDataInfo mreq;
MARK_AS_OFFICIAL_MODULE(modinfo);
ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
memset(&ReputationHashTable, 0, sizeof(ReputationHashTable));
siphash_generate_key(siphashkey_reputation);
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();
reputation_config_setdefaults(&cfg);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reputation_config_run);
HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, reputation_whois);
HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, reputation_set_on_connect);
HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 0, reputation_ip_change);
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 2000000000, reputation_pre_lconnect); /* (prio: last) */
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, -1000000000, reputation_set_on_connect); /* (prio: near-first) */
HookAdd(modinfo->handle, HOOKTYPE_CONNECT_EXTINFO, 0, reputation_connect_extinfo); /* (prio: near-first) */
CommandAdd(ModInf.handle, "REPUTATION", reputation_cmd, MAXPARA, CMD_USER|CMD_SERVER);
CommandAdd(ModInf.handle, "REPUTATIONUNPERM", reputationunperm, MAXPARA, CMD_USER|CMD_SERVER);
return MOD_SUCCESS;
}
#ifdef BENCHMARK
void reputation_benchmark(int entries)
{
char ip[64];
int i;
ReputationEntry *e;
srand(1234); // fixed seed
for (i = 0; i < entries; i++)
{
ReputationEntry *e = safe_alloc(sizeof(ReputationEntry) + 64);
snprintf(e->ip, 63, "%d.%d.%d.%d", rand()%255, rand()%255, rand()%255, rand()%255);
e->score = rand()%255 + 1;
e->last_seen = TStime();
if (find_reputation_entry(e->ip))
{
safe_free(e);
continue;
}
add_reputation_entry(e);
}
}
#endif
MOD_LOAD()
{
reputation_load_db();
if (reputation_starttime == 0)
reputation_starttime = TStime();
EventAdd(ModInf.handle, "delete_old_records", delete_old_records, NULL, DELETE_OLD_EVERY*1000, 0);
EventAdd(ModInf.handle, "add_scores", add_scores, NULL, BUMP_SCORE_EVERY*1000, 0);
EventAdd(ModInf.handle, "reputation_save_db", reputation_save_db_evt, NULL, SAVE_DB_EVERY*1000, 0);
#ifdef BENCHMARK
reputation_benchmark(10000);
#endif
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
if (loop.terminating)
reputation_save_db();
reputation_free_config(&test);
reputation_free_config(&cfg);
return MOD_SUCCESS;
}
void reputation_config_setdefaults(struct cfgstruct *cfg)
{
/* data/reputation.db */
safe_strdup(cfg->database, "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;
/* The 'require' settings */
cfg->require_minimum_channel_members = 3;
}
void reputation_free_config(struct cfgstruct *cfg)
{
safe_free(cfg->database);
safe_free(cfg->db_secret);
}
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->name, "reputation"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "score-bump-timer") || !strcmp(cep->name, "target"))
{
config_error("%s:%i: this feature is not implemented yet in this UnrealIRCd version",
cep->file->filename, cep->line_number);
errors++;
} else
if (!cep->value)
{
config_error("%s:%i: blank set::reputation::%s without value",
cep->file->filename, cep->line_number, cep->name);
errors++;
} else
if (!strcmp(cep->name, "score-bump-timer-minimum-channel-members"))
{
} else
if (!strcmp(cep->name, "database"))
{
convert_to_absolute_path(&cep->value, PERMDATADIR);
safe_strdup(test.database, cep->value);
} else
if (!strcmp(cep->name, "db-secret"))
{
const char *err;
if ((err = unrealdb_test_secret(cep->value)))
{
config_error("%s:%i: set::channeldb::db-secret: %s", cep->file->filename, cep->line_number, err);
errors++;
continue;
}
safe_strdup(test.db_secret, cep->value);
} else
{
config_error("%s:%i: unknown directive set::reputation::%s",
cep->file->filename, cep->line_number, cep->name);
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->name, "reputation"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "score-bump-timer-minimum-channel-members"))
{
cfg.require_minimum_channel_members = atoi(cep->value);
} else
if (!strcmp(cep->name, "database"))
{
safe_strdup(cfg.database, cep->value);
} else
if (!strcmp(cep->name, "db-secret"))
{
safe_strdup(cfg.db_secret, cep->value);
}
}
return 1;
}
int reputation_config_posttest(int *errs)
{
int errors = 0;
char *errstr;
if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
{
config_error("[reputation] %s", errstr);
errors++;
}
*errs = errors;
return errors ? -1 : 1;
}
/** Parse database header and set variables appropriately */
int parse_db_header_old(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 reputation_load_db_old(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_warn("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_old(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 = safe_alloc(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);
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
"[reputation] Benchmark: LOAD DB: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
}
#define R_SAFE(x) \
do { \
if (!(x)) { \
config_warn("[reputation] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
unrealdb_close(db); \
safe_free(ip); \
return 0; \
} \
} while(0)
int reputation_load_db_new(UnrealDB *db)
{
uint64_t l_db_version = 0;
uint64_t l_starttime = 0;
uint64_t l_writtentime = 0;
uint64_t count = 0;
uint64_t i;
char *ip = NULL;
uint16_t score;
uint64_t last_seen;
ReputationEntry *e;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
R_SAFE(unrealdb_read_int64(db, &l_db_version)); /* reputation db version */
if (l_db_version > 2)
{
config_error("[reputation] Reputation DB is of a newer version (%ld) than supported by us (%ld). "
"Did you perhaps downgrade your UnrealIRCd?",
(long)l_db_version, (long)2);
unrealdb_close(db);
return 0;
}
R_SAFE(unrealdb_read_int64(db, &l_starttime)); /* starttime of data gathering */
R_SAFE(unrealdb_read_int64(db, &l_writtentime)); /* current time */
R_SAFE(unrealdb_read_int64(db, &count)); /* number of entries */
reputation_starttime = l_starttime;
reputation_writtentime = l_writtentime;
for (i=0; i < count; i++)
{
R_SAFE(unrealdb_read_str(db, &ip));
R_SAFE(unrealdb_read_int16(db, &score));
R_SAFE(unrealdb_read_int64(db, &last_seen));
e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
strcpy(e->ip, ip); /* safe, see alloc above */
e->score = score;
e->last_seen = last_seen;
add_reputation_entry(e);
safe_free(ip);
}
unrealdb_close(db);
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
"Reputation benchmark: LOAD DB: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}
/** Load the reputation DB.
* Strategy is:
* 1) Check for the old header "REPDB 1", if so then call reputation_load_db_old().
* 2) Otherwise, open with unrealdb routine
* 3) If that fails due to a password provided but the file is unrealdb without password
* then fallback to open without a password (so users can easily upgrade to encrypted)
*/
int reputation_load_db(void)
{
FILE *fd;
UnrealDB *db;
char buf[512];
fd = fopen(cfg.database, "r");
if (!fd)
{
/* Database does not exist. Could be first boot */
config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
return 1;
}
*buf = '\0';
if (fgets(buf, sizeof(buf), fd) == NULL)
{
fclose(fd);
config_warn("[reputation] Database at '%s' is 0 bytes", cfg.database);
return 1;
}
fclose(fd);
if (!strncmp(buf, "REPDB 1 ", 8))
{
reputation_load_db_old();
return 1; /* not so good to always pretend succes */
}
/* Otherwise, it is an unrealdb, crypted or not */
db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
if (!db)
{
if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
{
/* Database does not exist. Could be first boot */
config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
return 1;
} else
if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
{
db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
}
if (!db)
{
config_error("[reputation] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
return 0;
}
}
return reputation_load_db_new(db);
}
int reputation_save_db_old(void)
{
FILE *fd;
char tmpfname[512];
int i;
ReputationEntry *e;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
/* We write to a temporary file. Only to rename it later if everything was ok */
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
fd = fopen(tmpfname, "w");
if (!fd)
{
config_error("ERROR: Could not open/write database '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
return 0;
}
if (fprintf(fd, "REPDB 1 %lld %lld\n", (long long)reputation_starttime, (long long)TStime()) < 0)
goto write_fail;
for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
{
for (e = ReputationHashTable[i]; e; e = e->next)
{
if (fprintf(fd, "%s %d %lld\n", e->ip, (int)e->score, (long long)e->last_seen) < 0)
{
write_fail:
config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
fclose(fd);
return 0;
}
}
}
if (fclose(fd) < 0)
{
config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
return 0;
}
/* Everything went fine. We rename our temporary file to the existing
* DB file (will overwrite), which is more or less an atomic operation.
*/
#ifdef _WIN32
/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
unlink(cfg.database);
#endif
if (rename(tmpfname, cfg.database) < 0)
{
config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
tmpfname, cfg.database, strerror(ERRNO));
return 0;
}
reputation_writtentime = TStime();
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
"Reputation benchmark: SAVE DB: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}
int reputation_save_db(void)
{
UnrealDB *db;
char tmpfname[512];
int i;
uint64_t count;
ReputationEntry *e;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
#ifdef TEST
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_TEST", NULL, "Reputation in running in test mode. Saving DB's....");
#endif
/* Comment this out after one or more releases (means you cannot downgrade to <=5.0.9.1 anymore) */
if (cfg.db_secret == NULL)
return reputation_save_db_old();
/* We write to a temporary file. Only to rename it later if everything was ok */
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
if (!db)
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
/* Write header */
W_SAFE(unrealdb_write_int64(db, 2)); /* reputation db version */
W_SAFE(unrealdb_write_int64(db, reputation_starttime)); /* starttime of data gathering */
W_SAFE(unrealdb_write_int64(db, TStime())); /* current time */
/* Count entries */
count = 0;
for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
for (e = ReputationHashTable[i]; e; e = e->next)
count++;
W_SAFE(unrealdb_write_int64(db, count)); /* Number of DB entries */
/* Now write the actual individual entries: */
for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
{
for (e = ReputationHashTable[i]; e; e = e->next)
{
W_SAFE(unrealdb_write_str(db, e->ip));
W_SAFE(unrealdb_write_int16(db, e->score));
W_SAFE(unrealdb_write_int64(db, e->last_seen));
}
}
if (!unrealdb_close(db))
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
/* Everything went fine. We rename our temporary file to the existing
* DB file (will overwrite), which is more or less an atomic operation.
*/
#ifdef _WIN32
/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
unlink(cfg.database);
#endif
if (rename(tmpfname, cfg.database) < 0)
{
config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
tmpfname, cfg.database, strerror(ERRNO));
return 0;
}
reputation_writtentime = TStime();
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
"Reputation benchmark: SAVE DB: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}
static uint64_t hash_reputation_entry(const char *ip)
{
return siphash(ip, siphashkey_reputation) % REPUTATION_HASH_TABLE_SIZE;
}
void add_reputation_entry(ReputationEntry *e)
{
int hashv = hash_reputation_entry(e->ip);
AddListItem(e, ReputationHashTable[hashv]);
}
ReputationEntry *find_reputation_entry(const 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;
}
int reputation_lookup_score_and_set(Client *client)
{
char *ip = client->ip;
ReputationEntry *e;
Reputation(client) = 0; /* (re-)set to zero (yes, important!) */
if (ip)
{
e = find_reputation_entry(ip);
if (e)
{
Reputation(client) = e->score; /* SET MODDATA */
}
}
return Reputation(client);
}
/** Called when the user connects.
* Locally: very early, just after the TCP/IP connection has
* been established, before any data.
* Remote user: early in the HOOKTYPE_REMOTE_CONNECT hook.
*/
int reputation_set_on_connect(Client *client)
{
reputation_lookup_score_and_set(client);
return 0;
}
int reputation_ip_change(Client *client, const char *oldip)
{
reputation_lookup_score_and_set(client);
return 0;
}
int reputation_pre_lconnect(Client *client)
{
/* 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.
*
* Note that we use reputation_lookup_score_and_set() here
* and not Reputation(client) because we want to RE-LOOKUP
* the score for the IP in the database. We do this because
* between reputation_set_on_connect() and reputation_pre_lconnect()
* the IP of the user may have been changed due to IP-spoofing
* (WEBIRC).
*/
int score = reputation_lookup_score_and_set(client);
sendto_server(NULL, 0, 0, NULL, ":%s REPUTATION %s %d", me.id, GetIP(client), score);
return 0;
}
EVENT(add_scores)
{
static int marker = 0;
char *ip;
Client *client;
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(client, &client_list, client_node)
{
if (!IsUser(client))
continue; /* skip servers, unknowns, etc.. */
ip = client->ip;
if (!ip)
continue;
/* Enforce set::reputation::score-bump-timer-minimum-channel-members.
* Simply skip all the bumping and setting if this requirement is not met.
*/
if (cfg.require_minimum_channel_members > 0)
{
if (highest_channel_member_count(client) < cfg.require_minimum_channel_members)
continue;
}
e = find_reputation_entry(ip);
if (!e)
{
/* Create */
e = safe_alloc(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(client) && (e->score < REPUTATION_SCORE_CAP))
{
e->score++;
e->marker = MARKER_REGISTERED_USER;
}
}
} else
if ((e->marker == MARKER_UNREGISTERED_USER) && IsLoggedIn(client) && (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(client) = 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;
}
/** If the reputation changed (due to server syncing) then update the
* individual users reputation score as well.
*/
void reputation_changed_update_users(ReputationEntry *e)
{
Client *client;
list_for_each_entry(client, &client_list, client_node)
{
if (client->ip && !strcmp(e->ip, client->ip))
Reputation(client) = e->score;
}
list_for_each_entry(client, &unknown_list, lclient_node)
{
if (client->ip && !strcmp(e->ip, client->ip))
Reputation(client) = e->score;
}
}
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_TABLE_SIZE; i++)
{
for (e = ReputationHashTable[i]; e; e = e_next)
{
e_next = e->next;
if (is_reputation_expired(e))
{
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_EXPIRY", NULL,
"Deleting expired entry for $ip (score $score, last seen $time_delta seconds ago)",
log_data_string("ip", e->ip),
log_data_integer("score", e->score),
log_data_integer("time_delta", TStime() - e->last_seen));
#endif
DelListItem(e, ReputationHashTable[i]);
safe_free(e);
}
}
}
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
"Reputation benchmark: EXPIRY IN MEM: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
}
EVENT(reputation_save_db_evt)
{
reputation_save_db();
}
CMD_FUNC(reputationunperm)
{
if (!IsOper(client))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return;
}
ModuleSetOptions(ModInf.handle, MOD_OPT_PERM, 0);
unreal_log(ULOG_INFO, "reputation", "REPUTATIONUNPERM_COMMAND", client,
"$client 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.");
}
int reputation_connect_extinfo(Client *client, NameValuePrioList **list)
{
add_fmt_nvplist(list, 0, "reputation", "%d", GetReputation(client));
return 0;
}
int count_reputation_records(void)
{
int i;
ReputationEntry *e;
int total = 0;
for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
for (e = ReputationHashTable[i]; e; e = e->next)
total++;
return total;
}
void reputation_channel_query(Client *client, Channel *channel)
{
Member *m;
char buf[512];
char tbuf[256];
char **nicks;
int *scores;
int cnt = 0, i, j;
ReputationEntry *e;
sendtxtnumeric(client, "Users and reputation scores for %s:", channel->name);
/* Step 1: build a list of nicks and their reputation */
nicks = safe_alloc((channel->users+1) * sizeof(char *));
scores = safe_alloc((channel->users+1) * sizeof(int));
for (m = channel->members; m; m = m->next)
{
nicks[cnt] = m->client->name;
if (m->client->ip)
{
e = find_reputation_entry(m->client->ip);
if (e)
scores[cnt] = e->score;
}
if (++cnt > channel->users)
{
unreal_log(ULOG_WARNING, "bug", "REPUTATION_CHANNEL_QUERY_BUG", client,
"[BUG] reputation_channel_query() expected $expected_users users, but $found_users (or more) users were present in $channel",
log_data_integer("expected_users", channel->users),
log_data_integer("found_users", cnt),
log_data_string("channel", channel->name));
#ifdef DEBUGMODE
abort();
#endif
break; /* safety net */
}
}
/* Step 2: lazy selection sort */
for (i = 0; i < cnt && nicks[i]; i++)
{
for (j = i+1; j < cnt && nicks[j]; j++)
{
if (scores[i] < scores[j])
{
char *nick_tmp;
int score_tmp;
nick_tmp = nicks[i];
score_tmp = scores[i];
nicks[i] = nicks[j];
scores[i] = scores[j];
nicks[j] = nick_tmp;
scores[j] = score_tmp;
}
}
}
/* Step 3: send the (ordered) list to the user */
*buf = '\0';
for (i = 0; i < cnt && nicks[i]; i++)
{
snprintf(tbuf, sizeof(tbuf), "%s\00314(%d)\003 ", nicks[i], scores[i]);
if ((strlen(tbuf)+strlen(buf) > 400) || !nicks[i+1])
{
sendtxtnumeric(client, "%s%s", buf, tbuf);
*buf = '\0';
} else {
strlcat(buf, tbuf, sizeof(buf));
}
}
sendtxtnumeric(client, "End of list.");
safe_free(nicks);
safe_free(scores);
}
void reputation_list_query(Client *client, int maxscore)
{
Client *target;
ReputationEntry *e;
sendtxtnumeric(client, "Users and reputation scores <%d:", maxscore);
list_for_each_entry(target, &client_list, client_node)
{
int score = 0;
if (!IsUser(target) || IsULine(target) || !target->ip)
continue;
e = find_reputation_entry(target->ip);
if (e)
score = e->score;
if (score >= maxscore)
continue;
sendtxtnumeric(client, "%s!%s@%s [%s] \017(score: %d)",
target->name,
target->user->username,
target->user->realhost,
target->ip,
score);
}
sendtxtnumeric(client, "End of list.");
}
CMD_FUNC(reputation_user_cmd)
{
ReputationEntry *e;
const char *ip;
if (!IsOper(client))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return;
}
if ((parc < 2) || BadPtr(parv[1]))
{
sendnotice(client, "Reputation module statistics:");
sendnotice(client, "Recording for: %lld seconds (since unixtime %lld)",
(long long)(TStime() - reputation_starttime),
(long long)reputation_starttime);
if (reputation_writtentime)
{
sendnotice(client, "Last successful db write: %lld seconds ago (unixtime %lld)",
(long long)(TStime() - reputation_writtentime),
(long long)reputation_writtentime);
} else {
sendnotice(client, "Last successful db write: never");
}
sendnotice(client, "Current number of records (IP's): %d", count_reputation_records());
sendnotice(client, "-");
sendnotice(client, "Available commands:");
sendnotice(client, "/REPUTATION <nick> Show reputation info about nick name");
sendnotice(client, "/REPUTATION <nick> <value> Adjust the reputation score of the IP address of nick to <value>");
sendnotice(client, "/REPUTATION <ip> Show reputation info about IP address");
sendnotice(client, "/REPUTATION <ip> <value> Adjust the reputation score of the IP address to <value>");
sendnotice(client, "/REPUTATION <channel> List users in channel along with their reputation score");
sendnotice(client, "/REPUTATION <NN List users with reputation score below value NN");
return;
}
if (strchr(parv[1], '.') || strchr(parv[1], ':'))
{
ip = parv[1];
} else
if (parv[1][0] == '#')
{
Channel *channel = find_channel(parv[1]);
if (!channel)
{
sendnumeric(client, ERR_NOSUCHCHANNEL, parv[1]);
return;
}
/* corner case: ircop without proper permissions and not in channel */
if (!ValidatePermissionsForPath("channel:see:names:invisible",client,NULL,NULL,NULL) && !IsMember(client,channel))
{
sendnumeric(client, ERR_NOTONCHANNEL, channel->name);
return;
}
reputation_channel_query(client, channel);
return;
} else
if (parv[1][0] == '<')
{
int max = atoi(parv[1] + 1);
if (max < 1)
{
sendnotice(client, "REPUTATION: Invalid search value specified. Use for example '/REPUTATION <5' to search on less-than-five");
return;
}
reputation_list_query(client, max);
return;
} else {
Client *target = find_user(parv[1], NULL);
if (!target)
{
sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
return;
}
ip = target->ip;
if (!ip)
{
sendnotice(client, "No IP address information available for user '%s'.", parv[1]); /* e.g. services */
return;
}
}
e = find_reputation_entry(ip);
if (!e)
{
sendnotice(client, "No reputation record found for IP %s", ip);
return;
}
if ((parc > 2) && !BadPtr(parv[2]))
{
/* Request to change value */
if (!ValidatePermissionsForPath("client:set:reputation",client,NULL,NULL,NULL))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return;
}
int v = atoi(parv[2]);
if (v > REPUTATION_SCORE_CAP)
v = REPUTATION_SCORE_CAP;
if (v < 0)
v = 0;
e->score = v;
reputation_changed_update_users(e);
sendto_server(&me, 0, 0, NULL,
":%s REPUTATION %s *%d*",
me.id,
e->ip,
e->score);
sendnotice(client, "Reputation of IP %s set to %hd", e->ip, e->score);
return;
}
sendnotice(client, "****************************************************");
sendnotice(client, "Reputation record for IP %s:", ip);
sendnotice(client, " Score: %hd", e->score);
sendnotice(client, "Last seen: %lld seconds ago (unixtime: %lld)",
(long long)(TStime() - e->last_seen),
(long long)e->last_seen);
sendnotice(client, "****************************************************");
}
/** 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.
*
* Since UnrealIRCd 6.0.2+ there is now also asterisk-score-asterisk:
* :server REPUTATION 1.2.3.4 *2*
* The leading asterisk means no reply will be sent back, ever, and the
* trailing asterisk will mean it is a "FORCED SET", which means that
* servers should set the reputation to that value, even if it is lower.
* This way reputation can be reduced and the reducation can be synced
* across servers, which was not possible before 6.0.2.
*
* 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;
const char *ip;
int score;
int allow_reply;
int forced = 0;
/* :server REPUTATION <ip> <score> */
if ((parc < 3) || BadPtr(parv[2]))
{
sendnumeric(client, ERR_NEEDMOREPARAMS, "REPUTATION");
return;
}
ip = parv[1];
if (parv[2][0] == '*')
{
allow_reply = 0;
score = atoi(parv[2]+1);
if (parv[2][1] && (parv[2][strlen(parv[2])-1] == '*'))
forced = 1;
} else {
allow_reply = 1;
score = atoi(parv[2]);
}
if (score > REPUTATION_SCORE_CAP)
score = REPUTATION_SCORE_CAP;
if (score < 0)
score = 0;
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 client direction about it.
* This will prefix the score with a * so servers will never reply to it.
*/
sendto_one(client, NULL, ":%s REPUTATION %s *%d", me.id, parv[1], e->score);
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
"Reputation score for for $ip from $client is $their_score, but we have $score, sending back $score",
log_data_string("ip", ip),
log_data_integer("their_score", score),
log_data_integer("score", e->score));
#endif
score = e->score; /* Update for propagation in the non-client direction */
}
/* Update our score if sender has a higher score */
if (e && (score > e->score))
{
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
"Reputation score for for $ip from $client is $their_score, but we have $score, updating our score to $score",
log_data_string("ip", ip),
log_data_integer("their_score", score),
log_data_integer("score", e->score));
#endif
e->score = score;
reputation_changed_update_users(e);
} else
if (e && forced)
{
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DECREASE", client,
"Reputation score for for $ip from $client is $score, force-setting to $their_score.",
log_data_string("ip", ip),
log_data_integer("their_score", score),
log_data_integer("score", e->score));
#endif
e->score = score;
reputation_changed_update_users(e);
}
/* If we don't have any entry for this IP, add it now. */
if (!e && (score > 0))
{
#ifdef DEBUGMODE
unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_NEW", client,
"Reputation score for for $ip from $client is $their_score, we had no entry, adding it",
log_data_string("ip", ip),
log_data_integer("their_score", score),
log_data_integer("score", 0));
#endif
e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
strcpy(e->ip, ip); /* safe, see alloc above */
e->score = score;
e->last_seen = TStime();
add_reputation_entry(e);
reputation_changed_update_users(e);
}
/* Propagate to the non-client direction (score may be updated) */
sendto_server(client, 0, 0, NULL,
":%s REPUTATION %s %s%d%s",
client->id,
parv[1],
allow_reply ? "" : "*",
score,
forced ? "*" : "");
}
CMD_FUNC(reputation_cmd)
{
if (MyUser(client))
CALL_CMD_FUNC(reputation_user_cmd);
else if (IsServer(client) || IsMe(client))
CALL_CMD_FUNC(reputation_server_cmd);
}
int reputation_whois(Client *client, Client *target, NameValuePrioList **list)
{
int reputation;
if (whois_get_policy(client, target, "reputation") != WHOIS_CONFIG_DETAILS_FULL)
return 0;
reputation = Reputation(target);
if (reputation > 0)
{
add_nvplist_numeric_fmt(list, 0, "reputation", client, RPL_WHOISSPECIAL,
"%s :is using an IP with a reputation score of %d",
target->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;
}
const 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(const char *str, ModData *m)
{
m->i = atoi(str);
}
int reputation_starttime_callback(void)
{
/* NB: fix this by 2038 */
return (int)reputation_starttime;
}
void _ban_act_set_reputation(Client *client, BanAction *action)
{
ReputationEntry *e;
int value;
if ((client->ip == NULL) || IsDead(client))
return; /* Wait, this is impossible, right? */
/* There's a problem with adjusting reputation scores
* when a user is in "unknown state", especially when decreasing
* it, which is the common case here.
* This is because we might be a new server with not much
* reputation scores in the database, so giving everyone a 0,
* this is automatically fixed post-connect by other servers
* syncing in normally.
* However, if we allow setting/decreasing reputation here in
* this function, before the adjustment happens, then we may be
* decreasing/setting to a very low value (because we think
* the user has 0 rep) while in fact the user has high rep
* and should still have high rep (just a little lower than before).
* Instead of breaking my head on this, let's just not deal with
* unknown users for now. Can always enhance this later.
*/
if (!IsUser(client))
return;
e = find_reputation_entry(client->ip);
if (!e)
{
/* Create */
e = safe_alloc(sizeof(ReputationEntry)+strlen(client->ip));
strcpy(e->ip, client->ip); /* safe, allocated above */
add_reputation_entry(e);
}
value = e->score;
switch (action->var_action)
{
case VAR_ACT_SET:
value = action->value;
break;
case VAR_ACT_INCREASE:
value += action->value;
if (value > REPUTATION_SCORE_CAP)
value = REPUTATION_SCORE_CAP;
break;
case VAR_ACT_DECREASE:
value -= action->value;
if (value < 0)
value = 0;
break;
default:
abort();
}
/* Nothing changed? Then we are done. */
if (e->score == value)
return;
e->score = value;
reputation_changed_update_users(e);
sendto_server(&me, 0, 0, NULL,
":%s REPUTATION %s *%d*",
me.id,
e->ip,
e->score);
}