/* * Copyright (c) 2005, 2006 Sendmail, Inc. and its suppliers. * All rights reserved. * * By using this file, you agree to the terms and conditions set * forth in the LICENSE file which can be found at the top level of * the sendmail distribution. */ #include "sm/generic.h" SM_RCSID("@(#)$Id: greyctl.c,v 1.19 2007/01/26 05:36:24 ca Exp $") #include "sm/assert.h" #include "sm/error.h" #include "sm/memops.h" #include "sm/heap.h" #include "sm/queue.h" #include "sm/net.h" #include "sm/greyctl.h" #include "sm/map.h" #include "sm/bdb.h" #if MTA_USE_PTHREADS #include "sm/pthread.h" #endif #include "greyctl.h" /* ** SM_GREY_COMPARE -- compare two time stamps from DB (callback) ** ** Parameters: ** dbp -- ignored ** d1 -- data 1 ** d2 -- data 2 ** ** Returns: ** data 1 minus data 2 (as time_t) */ int sm_grey_compare(DB *dbp, const DBT *d1, const DBT *d2) { time_t t1, t2; sm_memcpy(&t1, d1->data, sizeof(time_t)); sm_memcpy(&t2, d2->data, sizeof(time_t)); return (t1 - t2); } /* ** SM_GREY_SI -- get secondary index from main index (callback) ** ** Parameters: ** db -- ignored ** pkey -- primary key ** pdata -- primary data ** skey -- secondary key (output) ** ** Returns: ** 0 */ int sm_grey_si(DB *db, const DBT *pkey, const DBT *pdata, DBT *skey) { SM_REQUIRE(skey != NULL); SM_REQUIRE(pdata != NULL); sm_memzero(skey, sizeof(skey)); skey->data = (void *)&(((greyentry_P) pdata->data)->ge_expire); skey->size = sizeof(((greyentry_P) pdata->data)->ge_expire); return 0; } /* ** SM_GREY_OPENDB -- open databases ** ** Parameters: ** db_name -- name of primary DB ** db -- primary DB (output) ** sdb_name -- name of secondary DB ** sdb -- secondary DB (output) ** ** Returns: ** usual sm_error code ** ** ToDo: error handling */ static sm_ret_T sm_grey_opendb(const char *db_name, DB **pdb, const char *sdb_name, DB **psdb) { int ret; DB *db, *sdb; #define dbenv NULL SM_REQUIRE(db_name != NULL); SM_REQUIRE(pdb != NULL); SM_REQUIRE(sdb_name != NULL); SM_REQUIRE(psdb != NULL); /* create/open primary */ ret = db_create(&db, dbenv, 0); if (ret != 0) return BDB_ERR2RET(ret); ret = db->open(db, NULL, db_name, NULL, DB_BTREE, DB_CREATE, 0600); if (ret != 0) return BDB_ERR2RET(ret); /* create/open secondary */ ret = db_create(&sdb, dbenv, 0); if (ret != 0) return BDB_ERR2RET(ret); ret = sdb->set_bt_compare(sdb, sm_grey_compare); if (ret != 0) return BDB_ERR2RET(ret); ret = sdb->set_flags(sdb, DB_DUP | DB_DUPSORT); if (ret != 0) return BDB_ERR2RET(ret); ret = sdb->open(sdb, NULL, sdb_name, NULL, DB_BTREE, DB_CREATE, 0600); if (ret != 0) return BDB_ERR2RET(ret); /* Associate the secondary with the primary. */ ret = db->associate(db, NULL, sdb, sm_grey_si, 0); if (ret != 0) return BDB_ERR2RET(ret); *pdb = db; *psdb = sdb; return SM_SUCCESS; } /* ** SM_GREYCTL_FREE -- free grey control context ** ** Parameters: ** greyctx -- connection control context ** ** Returns: ** usual sm_error code */ sm_ret_T sm_greyctl_free(greyctx_P greyctx) { #if MTA_USE_PTHREADS int r; #endif if (NULL == greyctx) return SM_SUCCESS; #if MTA_USE_PTHREADS r = pthread_mutex_lock(&greyctx->greyc_mutex); SM_LOCK_OK(r); #endif if (greyctx->greyc_grey_sdb != NULL) { greyctx->greyc_grey_sdb->close(greyctx->greyc_grey_sdb, 0); greyctx->greyc_grey_sdb = NULL; } if (greyctx->greyc_grey_db != NULL) { greyctx->greyc_grey_db->close(greyctx->greyc_grey_db, 0); greyctx->greyc_grey_db = NULL; } #if MTA_USE_PTHREADS r = pthread_mutex_destroy(&greyctx->greyc_mutex); #endif sm_free_size(greyctx, sizeof(*greyctx)); #if SM_GREYCTL_CHECK greyctx->sm_magic = SM_MAGIC_NULL; #endif return SM_SUCCESS; } /* ** SM_GREYCTL_CRT -- create new grey control context ** ** Parameters: ** pgreyctx -- grey control context (output) ** ** Returns: ** usual sm_error code */ sm_ret_T sm_greyctl_crt(greyctx_P *pgreyctx) { sm_ret_T ret; greyctx_P greyctx; #if MTA_USE_PTHREADS int r; #endif SM_REQUIRE(pgreyctx != NULL); ret = SM_SUCCESS; greyctx = (greyctx_P) sm_zalloc(sizeof(*greyctx)); if (NULL == greyctx) return sm_err_temp(ENOMEM); #if MTA_USE_PTHREADS r = pthread_mutex_init(&greyctx->greyc_mutex, SM_PTHREAD_MUTEXATTR); if (r != 0) { ret = sm_err_perm(r); goto error; } #endif greyctx->greyc_used = 0; greyctx->greyc_cnf.greycnf_limit = 10000; greyctx->greyc_cnf.greycnf_min_grey_wait = 60; greyctx->greyc_cnf.greycnf_max_grey_wait = 60 * 60 * 24; greyctx->greyc_cnf.greycnf_white_expire = 60 * 60 * 24 * 33; greyctx->greyc_cnf.greycnf_white_reconfirm = 60 * 60 * 24 * 40; /* CONF */ greyctx->greyc_cnf.greycnf_grey_name = "grey_grey_m.db"; greyctx->greyc_cnf.greycnf_grey_sname = "grey_grey_s.db"; #if SM_GREYCTL_CHECK greyctx->sm_magic = SM_GREYCTL_MAGIC; #endif *pgreyctx = greyctx; return ret; #if MTA_USE_PTHREADS error: SM_FREE_SIZE(greyctx, sizeof(*greyctx)); return ret; #endif } /* ** SM_GREYCTL_OPEN -- open grey control context ** ** Parameters: ** greyctx -- grey control context ** ** Returns: ** usual sm_error code */ sm_ret_T sm_greyctl_open(greyctx_P greyctx, greycnf_P greycnf) { sm_ret_T ret; SM_IS_GREYCTX(greyctx); if (greycnf != NULL) sm_memcpy(&greyctx->greyc_cnf, greycnf, sizeof(greyctx->greyc_cnf)); if (greyctx->greyc_cnf.greycnf_netmask == 0) greyctx->greyc_cnf.greycnf_netmask = (ipv4_T) 0xFFFFFFFF; ret = sm_grey_opendb( greyctx->greyc_cnf.greycnf_grey_name, &greyctx->greyc_grey_db, greyctx->greyc_cnf.greycnf_grey_sname, &greyctx->greyc_grey_sdb); return ret; } /* ** SM_GREY_EXPIRE1 -- remove one entry ** ** Parameters: ** db -- main DB ** sdb -- secondary DB ** now -- time of connection ** ** Returns: ** usual sm_error code */ static sm_ret_T sm_grey_expire1(DB *db, DB *sdb, time_t now) { sm_ret_T ret; DBC *dbcp; DBT db_data, db_key; greyentry_T ge; ret = db->cursor(sdb, NULL, &dbcp, 0); if (ret != 0) return ret; sm_memzero(&db_key, sizeof(db_key)); sm_memzero(&db_data, sizeof(db_data)); db_data.flags = DB_DBT_USERMEM; db_data.data = ≥ db_data.ulen = sizeof(ge); /* this could be done in a loop for some elements */ ret = dbcp->c_get(dbcp, &db_key, &db_data, DB_LAST); if (0 == ret) { if (ge.ge_time > now) { ret = dbcp->c_del(dbcp, 0); if (0 == ret) ret = 1; } } ret = dbcp->c_close(dbcp); return ret; } /* ** SM_GREY_EXPIRE -- remove some entries ** ** Parameters: ** greyctx -- connection control context ** t -- time of connection ** ** Returns: ** usual sm_error code */ static sm_ret_T sm_grey_expire(greyctx_P greyctx, time_t t) { sm_ret_T ret; ret = sm_grey_expire1(greyctx->greyc_grey_db, greyctx->greyc_grey_sdb, t); return ret; } #define GE_KEY(ge) ((ge)->ge_key) #define GE_LEN(ge) ((ge)->ge_len) /* ** SM_GREY_FIND -- find an entry ** ** Parameters: ** ** Returns: ** usual sm_error code */ static sm_ret_T sm_grey_find(DB *db, void *key, uint len, greyentry_P ge) { sm_ret_T ret; DBT db_data, db_key; sm_memzero(&db_key, sizeof(db_key)); sm_memzero(&db_data, sizeof(db_data)); db_key.data = key; db_key.size = len; db_data.flags = DB_DBT_USERMEM; db_data.data = ge; db_data.ulen = sizeof(*ge); ret = db->get(db, NULL, &db_key, &db_data, 0); return ret; } /* ** SM_GREY_ADD -- add an entry ** ** Parameters: ** db -- main DB ** ge -- grey_entry to be added ** ** Returns: ** usual sm_error code */ static sm_ret_T sm_grey_add(DB *db, greyentry_P ge) { sm_ret_T ret; DBT db_data, db_key; sm_memzero(&db_key, sizeof(db_key)); sm_memzero(&db_data, sizeof(db_data)); db_key.data = GE_KEY(ge); db_key.size = GE_LEN(ge); db_data.data = ge; db_data.size = sizeof(*ge); ret = db->put(db, NULL, &db_key, &db_data, 0); return ret; } /* ** SM_GREY_RM -- remove an entry ** ** Parameters: ** db -- main DB ** key -- key to be removed ** len -- length of key ** ** Returns: ** usual sm_error code */ sm_ret_T sm_grey_rm(DB *db, void *key, uint len) { sm_ret_T ret; DBT db_key; sm_memzero(&db_key, sizeof(db_key)); db_key.data = key; db_key.size = len; ret = db->del(db, NULL, &db_key, 0); return ret; } /* ** SM_GREYCTL -- perform greylisting checks ** ** Parameters: ** greyctx -- connection control context ** key -- key ** keylen -- length of key ** t -- time of connection ** ** Returns: ** SM_GREY_OK: ok to accept now (just moved from grey to white) ** SM_GREY_WHITE: entry is whitelisted by now ** SM_GREY_FIRST: new entry ** SM_GREY_WAIT: need to wait ** SM_GREY_AGAIN: was whitelisted, but expired ** <0: usual sm_error code ** ** Question: how to handle overflows of the table? ** just return an error if hash table limit is reached. */ #define GREL_REMOVE_WHITE(greyctx, ge) do { \ ret = sm_grey_rm((greyctx)->greyc_grey_db, GE_KEY(ge), GE_LEN(ge));\ if (sm_is_err(ret)) \ goto error; \ } while (0) #define GREL_REMOVE_GREY(greyctx, ge) do { \ ret = sm_grey_rm((greyctx)->greyc_grey_db, GE_KEY(ge), GE_LEN(ge));\ if (sm_is_err(ret)) \ goto error; \ } while (0) #define GREL_APPEND_WHITE(greyctx, ge) do { \ ret = sm_grey_add((greyctx)->greyc_grey_db, (ge)); \ if (sm_is_err(ret)) \ goto error; \ } while (0) #define GREL_APPEND_GREY(greyctx, ge) do { \ ret = sm_grey_add((greyctx)->greyc_grey_db, (ge)); \ if (sm_is_err(ret)) \ goto error; \ } while (0) sm_ret_T sm_greyctl(greyctx_P greyctx, uchar *key, uint keylen, time_t t) { sm_ret_T ret; greyentry_T ges; greyentry_P ge; size_t len; #if MTA_USE_PTHREADS int r; #endif if (NULL == greyctx) return sm_err_perm(SM_E_NOMAP); SM_IS_GREYCTX(greyctx); #if MTA_USE_PTHREADS r = pthread_mutex_lock(&greyctx->greyc_mutex); SM_LOCK_OK(r); if (r != 0) { /* LOG? */ return sm_err_perm(r); } #endif ge = ⩾ ret = sm_grey_find(greyctx->greyc_grey_db, key, keylen, ge); if (ret != 0) { /* does not exist: add it and return "wait" */ if (greyctx->greyc_used >= greyctx->greyc_cnf.greycnf_limit) ret = sm_grey_expire(greyctx, t); ++greyctx->greyc_used; len = keylen; if (len >= sizeof(ge->ge_key)) len = sizeof(ge->ge_key); sm_memcpy(ge->ge_key, key, len); ge->ge_len = len; ge->ge_time = t; ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait; ge->ge_type = GREY_TYPE_GREY; ret = sm_grey_add(greyctx->greyc_grey_db, ge); if (sm_is_err(ret)) { SM_ASSERT(greyctx->greyc_used > 0); --greyctx->greyc_used; goto error; } ret = SM_GREY_FIRST; } else if (GREY_TYPE_WHITE == ge->ge_type) { if (ge->ge_time + greyctx->greyc_cnf.greycnf_white_reconfirm <= t) { /* it's too old: need to reconfirm */ GREL_REMOVE_WHITE(greyctx, ge); if (sm_is_err(ret)) goto error; ge->ge_time = t; ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait; ge->ge_type = GREY_TYPE_GREY; GREL_APPEND_GREY(greyctx, ge); ret = SM_GREY_AGAIN; } else if (ge->ge_time != t) { /* update time stamps */ GREL_REMOVE_WHITE(greyctx, ge); ge->ge_time = t; ge->ge_expire = t + greyctx->greyc_cnf.greycnf_white_expire; GREL_APPEND_WHITE(greyctx, ge); ret = SM_GREY_WHITE; } else ret = SM_GREY_WHITE; } else if (GREY_TYPE_GREY == ge->ge_type) { if (ge->ge_time + greyctx->greyc_cnf.greycnf_max_grey_wait <= t) { /* confirmation window passed; restart it */ GREL_REMOVE_GREY(greyctx, ge); ge->ge_time = t; ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait; GREL_APPEND_GREY(greyctx, ge); ret = SM_GREY_WAIT; } else if (ge->ge_time + greyctx->greyc_cnf.greycnf_min_grey_wait <= t) { /* waiting time expired, within confirmation window */ GREL_REMOVE_GREY(greyctx, ge); ge->ge_type = GREY_TYPE_WHITE; ge->ge_time = t; ge->ge_expire = t + greyctx->greyc_cnf.greycnf_white_expire; GREL_APPEND_WHITE(greyctx, ge); ret = SM_GREY_OK; } else ret = SM_GREY_WAIT; } else ret = sm_err_perm(SM_E_UNEXPECTED); /* fall through for unlocking */ error: #if MTA_USE_PTHREADS r = pthread_mutex_unlock(&greyctx->greyc_mutex); SM_ASSERT(0 == r); /* r isn't checked further; will fail on next iteration */ #endif return ret; }