diff --git a/src/Makefile.am b/src/Makefile.am index 305e57e..6f2b00e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright (C) 2002-2009 Thomas Ries +# Copyright (C) 2002-2017 Thomas Ries # # This file is part of Siproxd. # @@ -24,6 +24,7 @@ if use_convenience_ltdl endif AM_CFLAGS = -D_GNU_SOURCE $(LTDLDEF) \ + -Werror-implicit-function-declaration \ -DBUILDSTR="\"`cat .buildno`\"" \ -DBUILDDATE="\"`date -u '+%Y-%m-%dT%H:%M:%S'`\"" @@ -43,7 +44,8 @@ pkglib_LTLIBRARIES = plugin_demo.la \ plugin_stripheader.la \ plugin_siptrunk.la \ plugin_fix_fbox_anoncall.la \ - plugin_stats.la + plugin_stats.la \ + plugin_blacklist.la DLOPENPLUGINS = -dlopen plugin_demo.la \ -dlopen plugin_shortdial.la \ @@ -58,7 +60,8 @@ DLOPENPLUGINS = -dlopen plugin_demo.la \ -dlopen plugin_stripheader.la \ -dlopen plugin_siptrunk.la \ -dlopen plugin_fix_fbox_anoncall.la \ - -dlopen plugin_stats.la + -dlopen plugin_stats.la \ + -dlopen plugin_blacklist.la # plugin_demo_la_SOURCES = plugin_demo.c plugin_demo_la_LDFLAGS = -module -avoid-version -shrext '.so' @@ -101,6 +104,9 @@ plugin_fix_fbox_anoncall_la_LDFLAGS = -module -avoid-version -shrext '.so' # plugin_stats_la_SOURCES = plugin_stats.c plugin_stats_la_LDFLAGS = -module -avoid-version -shrext '.so' +# +plugin_blacklist_la_SOURCES = plugin_blacklist.c +plugin_blacklist_la_LDFLAGS = -module -avoid-version -shrext '.so' -lsqlite3 # diff --git a/src/plugin_blacklist.c b/src/plugin_blacklist.c new file mode 100644 index 0000000..0aebdaa --- /dev/null +++ b/src/plugin_blacklist.c @@ -0,0 +1,443 @@ +/* + Copyright (C) 2017 Thomas Ries + + This file is part of Siproxd. + + Siproxd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Siproxd is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warrantry of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Siproxd; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +/* must be defined before including */ +#define PLUGIN_NAME plugin_blacklist + +#include "config.h" + +#include +#include +#include + +#include +#include +#include + +#include + +#include "siproxd.h" +#include "plugins.h" +#include "log.h" + +static char const ident[]="$Id$"; + +/*&&&+++ Workaround sqlite3 3.3.6 header/symbol errors)*/ +#define sqlite3_clear_bindings UNDEFINED_SYMBOL +#define sqlite3_prepare_v2 UNDEFINED_SYMBOL +/*&&&---*/ + +/* Plug-in identification */ +static char name[]="plugin_blacklist"; +static char desc[]="Blacklists client IPs / SIP accounts upon auth failures"; + +/* global configuration storage - required for config file location */ +extern struct siproxd_config configuration; + +/* plugin configuration storage */ +static struct plugin_config { + char *dbpath; /* path to sqlite DB file (/var/lib/siproxd/bl.db */ + int block_mode; /* 0: no, 1: IP based, 2: IP & SIP-user */ + int block_duration; /* in seconds, 0: forever, dont' expire */ + int block_hitcount; /* required attempts until blocked */ +} plugin_cfg; + +/* Instructions for config parser */ +static cfgopts_t plugin_cfg_opts[] = { + { "plugin_blacklist_dbpath", TYP_STRING, &plugin_cfg.dbpath, {0, "/var/lib/siproxd/blacklist.sqlite"} }, + { "plugin_blacklist_mode", TYP_INT4, &plugin_cfg.block_mode, {0, NULL} }, + { "plugin_blacklist_duration", TYP_INT4, &plugin_cfg.block_duration, {3600, NULL} }, + { "plugin_blacklist_hitcount", TYP_INT4, &plugin_cfg.block_hitcount, {10, NULL} }, + {0, 0, 0} +}; + +/* SQLITE related variables */ +static sqlite3 *db=NULL; + +/* prepared SQL statements */ +typedef struct { + int id; + sqlite3_stmt *stmt; + char *sql_query; +} sql_statement_t; + +static sql_statement_t sql_statement[] = { + /* blacklist_check() */ + { 0, NULL, "SELECT count(id) from blacklist WHERE ip=?001 and sipuri=?002 AND failcount>?003;" }, + { 1, NULL, "UPDATE OR IGNORE blacklist SET lastseen=?003 WHERE ip=?001 and sipuri=?002;" }, + /* blacklist_update_fail() */ + { 2, NULL, "INSERT OR IGNORE INTO blacklist (ip, sipuri, failcount, lastseen, lastfail) VALUES (?001, ?002, 0, ?003, ?003);" }, + { 3, NULL, "UPDATE OR IGNORE blacklist SET failcount=failcount+1, lastseen=?003, lastfail=?003 WHERE ip=?001 and sipuri=?002;" }, +}; + +/* string magic in C preprocessor */ +#define xstr(s) str(s) +#define str(s) #s + +/* SQL statements */ +#define DB_SQL_CREATE \ + "CREATE TABLE IF NOT EXISTS "\ + "control ( "\ + "action VARCHAR(32) UNIQUE, "\ + "count INTEGER DEFAULT 0, "\ + "time VARCHAR(32) "\ + ");" \ + "CREATE TABLE IF NOT EXISTS "\ + "blacklist ( "\ + "id INTEGER PRIMARY KEY AUTOINCREMENT, "\ + "type INTEGER DEFAULT 0, "\ + "ip VARCHAR(" xstr(IPSTRING_SIZE) "), "\ + "sipuri VARCHAR(" xstr(USERNAME_SIZE) "), "\ + "failcount INTEGER DEFAULT 0, "\ + "lastfail INTEGER DEFAULT 0, "\ + "lastseen INTEGER DEFAULT 0, "\ + "CONSTRAINT unique_src UNIQUE (ip, sipuri) " \ + ");" + +/* tables +control +blacklist + - id + - type 0: automatic entry, 1: manual entry (manually added to DB, will not expire) + - ip IP address of source (xxx.xxx.xxx.xxx) + - sipuri SIP authentication username + - failcount count of failed attempts + - lastfail UNIX timestamp of last failure activity (last failed auth) + - lastseen UNIX timestamp of last activity +*/ + + +/* local prototypes */ +static int blacklist_check(sip_ticket_t *ticket); +static int blacklist_update_fail(sip_ticket_t *ticket); +static int blacklist_expire(sip_ticket_t *ticket); + +static int blacklist_sqlite_begin(void); +static int blacklist_sqlite_end(void); +static int blacklict_exec_prep_query_int(sqlite3_stmt *stmt1, int *retval); + +/* + * Initialization. + * Called once suring siproxd startup. + */ +int PLUGIN_INIT(plugin_def_t *plugin_def) { + /* API version number of siproxd that this plugin is built against. + * This constant will change whenever changes to the API are made + * that require adaptions in the plugin. */ + plugin_def->api_version=SIPROXD_API_VERSION; + + /* Name and descriptive text of the plugin */ + plugin_def->name=name; + plugin_def->desc=desc; + + /* Execution mask - during what stages of SIP processing shall + * the plugin be called. */ + plugin_def->exe_mask=PLUGIN_VALIDATE | PLUGIN_POST_PROXY; + + /* read the config file */ + if (read_config(configuration.configfile, + configuration.config_search, + plugin_cfg_opts, name) == STS_FAILURE) { + ERROR("Plugin '%s': could not load config file", name); + return STS_FAILURE; + } + + if (blacklist_sqlite_begin() != STS_SUCCESS) { + return STS_FAILURE; + } + + +blacklist_check(NULL); + INFO("plugin_blacklist is initialized (sqlite version %s)", sqlite3_libversion()); + return STS_SUCCESS; +} + +/* + * Processing. + * + */ +int PLUGIN_PROCESS(int stage, sip_ticket_t *ticket){ + int sts; + /* stage contains the PLUGIN_* value - the stage of SIP processing. */ + DEBUGC(DBCLASS_BABBLE, "plugin_blacklist: processing - stage %i",stage); + + if ((stage == PLUGIN_VALIDATE) + && MSG_IS_REQUEST(ticket->sipmsg)) { + sts = blacklist_check(ticket); + if (sts != STS_SUCCESS) { + return STS_FAILURE; + } + } else if ((stage == PLUGIN_POST_PROXY) + && MSG_IS_RESPONSE(ticket->sipmsg) + && MSG_IS_REGISTER(ticket->sipmsg)) { + sts = blacklist_update_fail(ticket); + } + + return STS_SUCCESS; +} + +/* + * De-Initialization. + * Called during shutdown of siproxd. Gives the plugin the chance + * to clean up its mess (e.g. dynamic memory allocation, database + * connections, whatever the plugin messes around with) + */ +int PLUGIN_END(plugin_def_t *plugin_def){ + int sts; + + sts = blacklist_sqlite_end(); + + INFO("plugin_blacklist ends here"); + return STS_SUCCESS; +} + +/*--------------------------------------------------------------------*/ +/* private plugin code */ +static int blacklist_check(sip_ticket_t *ticket) { + int sts; + sqlite3_stmt *stmt1 = sql_statement[0].stmt; + sqlite3_stmt *stmt2 = sql_statement[1].stmt; + + DEBUGC(DBCLASS_BABBLE, "entering blacklist_check"); + + /* bind */ + sts = sqlite3_bind_text(stmt1, 001, "1.2.3.4", -1, SQLITE_STATIC); + sts = sqlite3_bind_text(stmt1, 002, "foo@bar.org", -1, SQLITE_STATIC); + sts = sqlite3_bind_int(stmt1, 003, plugin_cfg.block_hitcount); + /* execute & eval result */ + do { + sts = sqlite3_step(stmt1); + if (sts == SQLITE_ROW) { + INFO("Blacklist Lookup returned %i matches", sqlite3_column_int(stmt1, 0)); + } + } while (sts == SQLITE_ROW); + if ( sts == SQLITE_ERROR) { + sts = sqlite3_reset(stmt1); + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } else if ( sts != SQLITE_DONE ) { + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } + /* cleanup */ + sts = sqlite3_reset(stmt1); + + + /* bind */ + sts = sqlite3_bind_text(stmt2, 001, "1.2.3.4", -1, SQLITE_STATIC); + sts = sqlite3_bind_text(stmt2, 002, "foo@bar.org", -1, SQLITE_STATIC); + sts = sqlite3_bind_int(stmt2, 003, time(NULL)); + /* execute & eval result */ + do { + sts = sqlite3_step(stmt2); + } while (sts == SQLITE_ROW); + if ( sts == SQLITE_ERROR) { + sts = sqlite3_reset(stmt2); + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } else if ( sts != SQLITE_DONE ) { + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } + /* cleanup */ + sts = sqlite3_reset(stmt2); + +// not present in 3.3.6 sts = sqlite3_clear_bindings(stmt1); + + + + + + + /* SELECT and find if a record exists + last_seen no older than block_duration + failcount >0 + return failcount */ + + /* update last_seen */ + + /* if failcount > maxfail, block */ + + + +#define DB_SQL_BL_CHECK \ + "SELECT * from blacklist where " + + + DEBUGC(DBCLASS_BABBLE, "leaving blacklist_check"); + return STS_SUCCESS; +} + +static int blacklist_update_fail(sip_ticket_t *ticket) { + int sts; + sqlite3_stmt *stmt11 = sql_statement[2].stmt; + sqlite3_stmt *stmt12 = sql_statement[3].stmt; + + DEBUGC(DBCLASS_BABBLE, "entering blacklist_update_fail"); + + + /* bind */ + //&&& fetch UAC IP from telegram + //&&& fetch SIP URI (contact?) from telegram + sts = sqlite3_bind_text(stmt11, 001, "1.2.3.4", -1, SQLITE_STATIC); + sts = sqlite3_bind_text(stmt11, 002, "foo@bar.org", -1, SQLITE_STATIC); + sts = sqlite3_bind_int(stmt11, 003, time(NULL)); + /* execute & eval result */ + do { + sts = sqlite3_step(stmt11); + } while (sts == SQLITE_ROW); + if ( sts == SQLITE_ERROR) { + sts = sqlite3_reset(stmt11); + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } else if ( sts != SQLITE_DONE ) { + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } + /* cleanup */ + sts = sqlite3_reset(stmt11); + + + /* bind */ + //&&& fetch UAC IP from telegram + //&&& fetch SIP URI (contact?) from telegram + sts = sqlite3_bind_text(stmt12, 001, "1.2.3.4", -1, SQLITE_STATIC); + sts = sqlite3_bind_text(stmt12, 002, "foo@bar.org", -1, SQLITE_STATIC); + sts = sqlite3_bind_int(stmt12, 003, time(NULL)); + /* execute & eval result */ + do { + sts = sqlite3_step(stmt12); + } while (sts == SQLITE_ROW); + if ( sts == SQLITE_ERROR) { + sts = sqlite3_reset(stmt12); + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } else if ( sts != SQLITE_DONE ) { + ERROR("SQL step error [%i]: %s\n", sts, sqlite3_errmsg(db)); + } + /* cleanup */ + sts = sqlite3_reset(stmt12); + + + /* INSERT OR IGNORE records to DB: IP, sipuri */ + /* UPDATE records failcount=failcount+1, lastseen=now, lastfail=now */ + + DEBUGC(DBCLASS_BABBLE, "leaving blacklist_update_fail"); + return STS_SUCCESS; +} + +static int blacklist_expire(sip_ticket_t *ticket) { + int sts; + char *zErrMsg = NULL; + + DEBUGC(DBCLASS_BABBLE, "entering blacklist_expire"); + /* set failcount=0 for all records where last_seen is older than block_period */ + /* or remove records */ + + DEBUGC(DBCLASS_BABBLE, "leaving blacklist_expire"); + return STS_SUCCESS; +} + + +/*--------------------------------------------------------------------*/ +/* helper functions */ +static int blacklist_sqlite_begin(void){ + int sts; + int i; + char *zErrMsg = NULL; + + /* open the database */ + sts = sqlite3_open(plugin_cfg.dbpath, &db); + if( sts != SQLITE_OK ){ + ERROR("Can't open database: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return STS_FAILURE; + } + + /* create table structure if not existing */ + sts = sqlite3_exec(db, DB_SQL_CREATE, NULL, 0, &zErrMsg); + if( sts != SQLITE_OK ){ + ERROR( "SQL exec error: %s\n", zErrMsg); + sqlite3_free(zErrMsg); + sqlite3_close(db); + return STS_FAILURE; + } + + /* write check (DB update) */ +#define DB_SQL_STARTUP \ + "INSERT OR IGNORE INTO control (action, count) VALUES ('bl_started', 0); "\ + "UPDATE control set count = count + 1, time = datetime('now') where action ='bl_started';" + sts = sqlite3_exec(db, DB_SQL_STARTUP, NULL, 0, &zErrMsg); + if( sts != SQLITE_OK ){ + ERROR( "SQL exec error: %s\n", zErrMsg); + sqlite3_free(zErrMsg); + sqlite3_close(db); + return STS_FAILURE; + } + + /* create prepared statements */ + DEBUGC(DBCLASS_BABBLE, "PLUGIN_INIT: preparing %i statements", + sizeof(sql_statement) / sizeof(sql_statement[0])); + for (i=0; i < sizeof(sql_statement) / sizeof(sql_statement[0]); i++) { + if (sql_statement[i].sql_query == NULL) { + DEBUGC(DBCLASS_BABBLE, "PLUGIN_INIT: skiping empty SQL statement"); + continue; + } + if (sql_statement[i].stmt == NULL) { + DEBUGC(DBCLASS_BABBLE, "PLUGIN_INIT: preparing stmt %i [%s]", + i, sql_statement[i].sql_query); + sts = sqlite3_prepare(db, sql_statement[i].sql_query, -1, + &sql_statement[i].stmt, NULL ); + if( sts != SQLITE_OK ){ + ERROR("SQL prepare error [query=%i]: %s\n", i, sqlite3_errmsg(db)); + sqlite3_close(db); + return STS_FAILURE; + } + } + } + + return STS_SUCCESS; +} + +static int blacklist_sqlite_end(void){ + int sts; + int i; + char *zErrMsg = NULL; + + /* Mark shutdown in DB (pure informational reasons) */ +#define DB_SQL_SHUTDOWN \ + "INSERT OR IGNORE INTO control (action, count) VALUES ('bl_stopped', 0); "\ + "UPDATE control set count = count + 1, time = datetime('now') where action ='bl_stopped';" + sts = sqlite3_exec(db, DB_SQL_SHUTDOWN, NULL, 0, &zErrMsg); + if( sts != SQLITE_OK ){ + ERROR( "SQL exec error: %s\n", zErrMsg); + sqlite3_free(zErrMsg); + } + + /* free ressources of prepared queries */ + for (i=0; i < sizeof(sql_statement) / sizeof(sql_statement[0]); i++) { + if (sql_statement[i].stmt != NULL) { + sts = sqlite3_finalize(sql_statement[i].stmt); + } + } + + sqlite3_close(db); + + return STS_SUCCESS; +} + +static int blacklict_exec_prep_query_int(sqlite3_stmt *stmt1, int *retval){ +} + +//&&& implement cache of open REGISTER requests, only honor failed REGISTERS responses where a +//&&& REQUEST has been sent before from one of our clients.