diff --git a/modules/auth_aka/README b/modules/auth_aka/README index 335c535dad..e528c6b490 100644 --- a/modules/auth_aka/README +++ b/modules/auth_aka/README @@ -8,50 +8,52 @@ Auth_aka Module 1.1. Overview 1.2. Authentication Vectors 1.3. Supported algorithms - 1.4. Dependencies + 1.4. Clustering / Multi-Node Support + 1.5. Dependencies - 1.4.1. OpenSIPS Modules - 1.4.2. External Libraries or Applications + 1.5.1. OpenSIPS Modules + 1.5.2. External Libraries or Applications - 1.5. Exported Parameters + 1.6. Exported Parameters - 1.5.1. default_av_mgm (string) - 1.5.2. default_qop (string) - 1.5.3. default_algorithm (string) - 1.5.4. hash_size (integer) - 1.5.5. sync_timeout (integer) - 1.5.6. async_timeout (integer) - 1.5.7. unused_timeout (integer) - 1.5.8. unused_timeout (integer) + 1.6.1. default_av_mgm (string) + 1.6.2. default_qop (string) + 1.6.3. default_algorithm (string) + 1.6.4. hash_size (integer) + 1.6.5. sync_timeout (integer) + 1.6.6. async_timeout (integer) + 1.6.7. unused_timeout (integer) + 1.6.8. pending_timeout (integer) + 1.6.9. cachedb_url (string) - 1.6. Exported Functions + 1.7. Exported Functions - 1.6.1. aka_www_authorize([realm]]) - 1.6.2. aka_proxy_authorize([realm]]) - 1.6.3. aka_www_challenge([av_mgm[, realm[ ,qop[, + 1.7.1. aka_www_authorize([realm]]) + 1.7.2. aka_proxy_authorize([realm]]) + 1.7.3. aka_www_challenge([av_mgm[, realm[ ,qop[, alg]]]]) - 1.6.4. aka_proxy_challenge([realm]]) - 1.6.5. aka_av_add(public_identity, private_identity, + 1.7.4. aka_proxy_challenge([realm]]) + 1.7.5. aka_av_add(public_identity, private_identity, authenticate, authorize, confidentiality_key, integrity_key[, algorithms]) - 1.6.6. aka_av_drop(public_identity, + 1.7.6. aka_av_drop(public_identity, private_identity, authenticate) - 1.6.7. aka_av_drop_all(public_identity, + 1.7.7. aka_av_drop_all(public_identity, private_identity[, count]) - 1.6.8. aka_av_fail(public_identity, + 1.7.8. aka_av_fail(public_identity, private_identity[, count]) - 1.7. Exported MI Functions + 1.8. Exported MI Functions - 1.7.1. aka_av_add - 1.7.2. aka_av_drop - 1.7.3. aka_av_drop_all - 1.7.4. aka_av_fail + 1.8.1. aka_av_add + 1.8.2. aka_av_drop + 1.8.3. aka_av_drop_all + 1.8.4. aka_av_fail 2. Contributors @@ -79,18 +81,19 @@ Auth_aka Module 1.6. async_timeout parameter usage 1.7. unused_timeout parameter usage 1.8. pending_timeout parameter usage - 1.9. aka_www_authorize usage - 1.10. aka_proxy_authorize usage - 1.11. aka_www_challenge usage - 1.12. aka_proxy_challenge usage - 1.13. aka_av_add usage - 1.14. aka_av_drop usage - 1.15. aka_av_drop_all usage - 1.16. aka_av_fail usage - 1.17. aka_av_add usage - 1.18. aka_av_drop usage - 1.19. aka_av_drop_all usage - 1.20. aka_av_drop usage + 1.9. cachedb_url parameter usage + 1.10. aka_www_authorize usage + 1.11. aka_proxy_authorize usage + 1.12. aka_www_challenge usage + 1.13. aka_proxy_challenge usage + 1.14. aka_av_add usage + 1.15. aka_av_drop usage + 1.16. aka_av_drop_all usage + 1.17. aka_av_fail usage + 1.18. aka_av_add usage + 1.19. aka_av_drop usage + 1.20. aka_av_drop_all usage + 1.21. aka_av_drop usage Chapter 1. Admin Guide @@ -148,23 +151,55 @@ Chapter 1. Admin Guide algorithms as well, but the response cannot be handled by this module, and an appropriate error will be returned. -1.4. Dependencies +1.4. Clustering / Multi-Node Support -1.4.1. OpenSIPS Modules + In distributed deployments where multiple OpenSIPS nodes handle + SIP REGISTER requests, the AKA authentication flow requires that + the authentication vector (AV) issued in the 401 challenge is + available on the node that receives the subsequent authenticated + REGISTER. + + To support this scenario, the module can optionally use the + OpenSIPS CacheDB infrastructure to synchronize authentication + vectors across nodes. When the cachedb_url parameter is set: + + * AVs are stored in the external cache when created + * AVs are fetched from the cache when not found locally + * AV state changes are synchronized to the cache + * AVs are removed from the cache when they expire + + Supported CacheDB backends include: + * Redis (cachedb_redis module) + * MongoDB (cachedb_mongodb module) + * Cassandra (cachedb_cassandra module) + * Any other CacheDB-compatible backend + + Example configuration for a clustered setup: + +loadmodule "cachedb_redis.so" +loadmodule "auth_aka.so" +modparam("cachedb_redis", "cachedb_url", "redis://redis-cluster:6379/") +modparam("auth_aka", "cachedb_url", "redis://redis-cluster:6379/") + +1.5. Dependencies + +1.5.1. OpenSIPS Modules The module depends on the following modules (in the other words the listed modules must be loaded before this module): * auth -- Authentication framework * AV manage module -- at least one module that fetches AVs and pushes them in the AV storage + * cachedb_* module (optional) -- required only if cachedb_url + is set for multi-node AV synchronization -1.4.2. External Libraries or Applications +1.5.2. External Libraries or Applications This module does not depend on any external library. -1.5. Exported Parameters +1.6. Exported Parameters -1.5.1. default_av_mgm (string) +1.6.1. default_av_mgm (string) The default AV Manager used in case the functions do not provide them explicitly. @@ -174,7 +209,7 @@ Chapter 1. Admin Guide modparam("auth_aka", "default_av_mgm", "diameter") # fetch AVs through t he Cx interface -1.5.2. default_qop (string) +1.6.2. default_qop (string) The default qop parameter used during challenge, if the functions do not provide them explicitly. @@ -185,7 +220,7 @@ he Cx interface modparam("auth_aka", "default_qop", "auth,auth-int") -1.5.3. default_algorithm (string) +1.6.3. default_algorithm (string) The default algorithm to be advertise during challenge, if the functions do not provide them explicitly. Note that at least @@ -200,7 +235,7 @@ modparam("auth_aka", "default_qop", "auth,auth-int") modparam("auth_aka", "default_algorithm", "AKAv2-MD5") -1.5.4. hash_size (integer) +1.6.4. hash_size (integer) The size of the hash that stores the AVs for each user. Must be a power of 2 number. @@ -211,7 +246,7 @@ modparam("auth_aka", "default_algorithm", "AKAv2-MD5") modparam("auth_aka", "hash_size", 1024) -1.5.5. sync_timeout (integer) +1.6.5. sync_timeout (integer) The amount of milliseconds a synchronous call should wait for getting an authentication vector. @@ -225,7 +260,7 @@ modparam("auth_aka", "hash_size", 1024) modparam("auth_aka", "sync_timeout", 200) -1.5.6. async_timeout (integer) +1.6.6. async_timeout (integer) The amount of milliseconds an asynchronous call should wait for getting an authentication vector. @@ -242,7 +277,7 @@ modparam("auth_aka", "sync_timeout", 200) modparam("auth_aka", "async_timeout", 2000) -1.5.7. unused_timeout (integer) +1.6.7. unused_timeout (integer) The amount of seconds an authentication vector that has not been used can stay in memory. Once this timeout is reached, the @@ -256,7 +291,7 @@ modparam("auth_aka", "async_timeout", 2000) modparam("auth_aka", "unused_timeout", 120) -1.5.8. unused_timeout (integer) +1.6.8. pending_timeout (integer) The amount of seconds an authentication vector that is being used in the authentication process shall stay in memory. Once @@ -271,9 +306,43 @@ modparam("auth_aka", "unused_timeout", 120) modparam("auth_aka", "pending_timeout", 10) -1.6. Exported Functions +1.6.9. cachedb_url (string) + + If set, this parameter enables the synchronization of + authentication vectors across multiple OpenSIPS nodes through + the CacheDB interface. This is essential for distributed/clustered + deployments where one node may issue the 401 challenge and another + node may receive the authenticated REGISTER request. + + When enabled, authentication vectors are stored in the configured + CacheDB backend (e.g., Redis, MongoDB, Cassandra) with a TTL based + on the pending_timeout parameter plus a small margin. + + The flow for multi-node authentication is: + 1. Node A receives initial REGISTER without credentials + 2. Node A fetches AV, stores it in CacheDB, sends 401 + 3. Node B receives REGISTER with credentials + 4. Node B looks up AV locally, on miss fetches from CacheDB + 5. Node B validates credentials using the cached AV + + If not set (default), authentication vectors are only stored + locally and multi-node authentication will not work. + + Example 1.9. cachedb_url parameter usage + +# Using Redis for AV synchronization +loadmodule "cachedb_redis.so" +modparam("cachedb_redis", "cachedb_url", "redis://localhost:6379/") +modparam("auth_aka", "cachedb_url", "redis://localhost:6379/") + +# Using MongoDB for AV synchronization +loadmodule "cachedb_mongodb.so" +modparam("cachedb_mongodb", "cachedb_url", "mongodb://localhost:27017/opensips") +modparam("auth_aka", "cachedb_url", "mongodb://localhost:27017/opensips") + +1.7. Exported Functions -1.6.1. aka_www_authorize([realm]]) +1.7.1. aka_www_authorize([realm]]) The function verifies credentials according to RFC3310, by using an authentication vector priorly allocated by an @@ -321,7 +390,7 @@ if (!aka_www_authorize("diameter", "siphub.com")) ... -1.6.2. aka_proxy_authorize([realm]]) +1.7.2. aka_proxy_authorize([realm]]) The function behaves the same as aka_www_authorize(), but it authenticates the user from a proxy perspective. It receives @@ -338,7 +407,7 @@ if (!aka_proxy_authorize("siphub.com")) ... -1.6.3. aka_www_challenge([av_mgm[, realm[ ,qop[, alg]]]]) +1.7.3. aka_www_challenge([av_mgm[, realm[ ,qop[, alg]]]]) The function challenges a user agent. It fetches an authentication vector for each algorigthm used through the @@ -408,7 +477,7 @@ if (!aka_www_authorize("siphub.com")) { } ... -1.6.4. aka_proxy_challenge([realm]]) +1.7.4. aka_proxy_challenge([realm]]) The function behaves the same as aka_www_challenge(), but it challenges the user from a proxy perspective. It receives the @@ -429,7 +498,7 @@ if (!aka_proxy_authorize("siphub.com")) ... -1.6.5. aka_av_add(public_identity, private_identity, authenticate, +1.7.5. aka_av_add(public_identity, private_identity, authenticate, authorize, confidentiality_key, integrity_key[, algorithms]) Adds an authentication vector for the user identitied by @@ -467,7 +536,7 @@ uthorize */ "6151667b9ef815c1dcb87473685f062a" /* ik */); ... -1.6.6. aka_av_drop(public_identity, private_identity, authenticate) +1.7.6. aka_av_drop(public_identity, private_identity, authenticate) Drops the authentication vector corresponding to the authenticate/nonce value for an user identitied by @@ -490,7 +559,7 @@ aka_av_drop("sip:test@siphub.com", "test@siphub.com", "KFQ/MpR3cE3V9PxucEQS5KED8uUNYIAALFyk59sIJI4="); ... -1.6.7. aka_av_drop_all(public_identity, private_identity[, count]) +1.7.7. aka_av_drop_all(public_identity, private_identity[, count]) Drops all authentication vectors for an user identitied by public_identity and private_identity. This function is useful @@ -512,7 +581,7 @@ aka_av_drop("sip:test@siphub.com", "test@siphub.com", aka_av_drop_all("sip:test@siphub.com", "test@siphub.com", $var(count)); ... -1.6.8. aka_av_fail(public_identity, private_identity[, count]) +1.7.8. aka_av_fail(public_identity, private_identity[, count]) Marks the engine that an authentication vector query for a user has failed, unlocking the processing of the message. @@ -537,9 +606,9 @@ aka_av_drop_all("sip:test@siphub.com", "test@siphub.com", $var(count)); aka_av_fail("sip:test@siphub.com", "test@siphub.com", 3); ... -1.7. Exported MI Functions +1.8. Exported MI Functions -1.7.1. aka_av_add +1.8.1. aka_av_add Adds an Authentication Vector through the MI interface. @@ -574,7 +643,7 @@ JI4= 6151667b9ef815c1dcb87473685f062a ... -1.7.2. aka_av_drop +1.8.2. aka_av_drop Invalidates an Authentication Vector of an user identified by its authenticate value. @@ -597,7 +666,7 @@ $ opensips-cli -x mi aka_av_drop \ JI4= ... -1.7.3. aka_av_drop_all +1.8.3. aka_av_drop_all Invalidates all Authentication Vectors of an user through the MI interface. @@ -616,7 +685,7 @@ $ opensips-cli -x mi aka_av_drop_all \ test@siphub.com ... -1.7.4. aka_av_fail +1.8.4. aka_av_fail Indicates the fact that the fetching of an authentication vector has failed, unlocking the processing of the message. diff --git a/modules/auth_aka/aka_av_mgm.c b/modules/auth_aka/aka_av_mgm.c index 4e6233e642..c92a57767d 100644 --- a/modules/auth_aka/aka_av_mgm.c +++ b/modules/auth_aka/aka_av_mgm.c @@ -30,17 +30,291 @@ static gen_hash_t *aka_users; OSIPS_LIST_HEAD(aka_av_managers); +/* Forward declaration for static functions */ +static void aka_av_insert(struct aka_user *user, struct aka_av *av); -int aka_init_mgm(int hash_size) +/* CacheDB AV TTL (set from pending_timeout) */ +static int aka_cdb_av_ttl = 30; + +/* CacheDB key prefix */ +#define AKA_CDB_KEY_PREFIX "aka_av:" +#define AKA_CDB_KEY_PREFIX_LEN (sizeof(AKA_CDB_KEY_PREFIX) - 1) + +/* Serialization delimiter */ +#define AKA_CDB_DELIM '|' + + +int aka_init_mgm(int hash_size, int pending_timeout) { aka_users = hash_init(hash_size); if (!aka_users) { LM_ERR("cannot create AKA users hash\n"); return -1; } + /* Add some margin to TTL for race conditions */ + aka_cdb_av_ttl = pending_timeout + 5; + return 0; +} + +/* + * Build CacheDB key: aka_av::: + * Returns allocated pkg memory that must be freed by caller + */ +static int aka_cdb_build_key(str *impu, str *impi, str *nonce, str *key) +{ + key->len = AKA_CDB_KEY_PREFIX_LEN + impu->len + 1 + impi->len + 1 + nonce->len; + key->s = pkg_malloc(key->len + 1); + if (!key->s) { + LM_ERR("oom for cachedb key\n"); + return -1; + } + memcpy(key->s, AKA_CDB_KEY_PREFIX, AKA_CDB_KEY_PREFIX_LEN); + memcpy(key->s + AKA_CDB_KEY_PREFIX_LEN, impu->s, impu->len); + key->s[AKA_CDB_KEY_PREFIX_LEN + impu->len] = ':'; + memcpy(key->s + AKA_CDB_KEY_PREFIX_LEN + impu->len + 1, impi->s, impi->len); + key->s[AKA_CDB_KEY_PREFIX_LEN + impu->len + 1 + impi->len] = ':'; + memcpy(key->s + AKA_CDB_KEY_PREFIX_LEN + impu->len + 1 + impi->len + 1, + nonce->s, nonce->len); + key->s[key->len] = '\0'; + return 0; +} + +/* + * Serialize AV to string: state|algmask|alg|authenticate|authorize|ck|ik + * Returns allocated pkg memory that must be freed by caller + */ +static int aka_cdb_serialize_av(struct aka_av *av, str *value) +{ + char state_buf[16], algmask_buf[16], alg_buf[16]; + int state_len, algmask_len, alg_len; + + state_len = snprintf(state_buf, sizeof(state_buf), "%d", av->state); + algmask_len = snprintf(algmask_buf, sizeof(algmask_buf), "%d", av->algmask); + alg_len = snprintf(alg_buf, sizeof(alg_buf), "%d", av->alg); + + value->len = state_len + 1 + algmask_len + 1 + alg_len + 1 + + av->authenticate.len + 1 + av->authorize.len + 1 + + av->ck.len + 1 + av->ik.len; + value->s = pkg_malloc(value->len + 1); + if (!value->s) { + LM_ERR("oom for cachedb value\n"); + return -1; + } + + snprintf(value->s, value->len + 1, "%s%c%s%c%s%c%.*s%c%.*s%c%.*s%c%.*s", + state_buf, AKA_CDB_DELIM, + algmask_buf, AKA_CDB_DELIM, + alg_buf, AKA_CDB_DELIM, + av->authenticate.len, av->authenticate.s, AKA_CDB_DELIM, + av->authorize.len, av->authorize.s, AKA_CDB_DELIM, + av->ck.len, av->ck.s, AKA_CDB_DELIM, + av->ik.len, av->ik.s); return 0; } +/* + * Parse a field from serialized string + * Updates pos to point after the delimiter + */ +static int aka_cdb_parse_field(char *start, char *end, str *field, char **next) +{ + char *delim = memchr(start, AKA_CDB_DELIM, end - start); + if (delim) { + field->s = start; + field->len = delim - start; + *next = delim + 1; + } else { + /* Last field */ + field->s = start; + field->len = end - start; + *next = end; + } + return 0; +} + +/* + * Deserialize AV from string: state|algmask|alg|authenticate|authorize|ck|ik + * Creates a new aka_av in shared memory + */ +static struct aka_av *aka_cdb_deserialize_av(str *value) +{ + struct aka_av *av; + str field; + char *pos, *end; + int state, algmask, alg; + str authenticate, authorize, ck, ik; + char *p; + + pos = value->s; + end = value->s + value->len; + + /* Parse state */ + aka_cdb_parse_field(pos, end, &field, &pos); + if (str2sint(&field, &state) < 0) { + LM_ERR("invalid state in cached AV\n"); + return NULL; + } + + /* Parse algmask */ + aka_cdb_parse_field(pos, end, &field, &pos); + if (str2sint(&field, &algmask) < 0) { + LM_ERR("invalid algmask in cached AV\n"); + return NULL; + } + + /* Parse alg */ + aka_cdb_parse_field(pos, end, &field, &pos); + if (str2sint(&field, &alg) < 0) { + LM_ERR("invalid alg in cached AV\n"); + return NULL; + } + + /* Parse authenticate */ + aka_cdb_parse_field(pos, end, &authenticate, &pos); + + /* Parse authorize */ + aka_cdb_parse_field(pos, end, &authorize, &pos); + + /* Parse ck */ + aka_cdb_parse_field(pos, end, &ck, &pos); + + /* Parse ik */ + aka_cdb_parse_field(pos, end, &ik, &pos); + + /* Allocate AV structure */ + av = shm_malloc(sizeof(*av) + authenticate.len + authorize.len + ck.len + ik.len); + if (!av) { + LM_ERR("oom for cached AV\n"); + return NULL; + } + memset(av, 0, sizeof(*av)); + av->state = state; + av->algmask = algmask; + av->alg = alg; + + p = av->buf; + av->authenticate.s = p; + av->authenticate.len = authenticate.len; + memcpy(p, authenticate.s, authenticate.len); + p += authenticate.len; + + av->authorize.s = p; + av->authorize.len = authorize.len; + memcpy(p, authorize.s, authorize.len); + p += authorize.len; + + av->ck.s = p; + av->ck.len = ck.len; + memcpy(p, ck.s, ck.len); + p += ck.len; + + av->ik.s = p; + av->ik.len = ik.len; + memcpy(p, ik.s, ik.len); + + INIT_LIST_HEAD(&av->list); + av->ts = av->new_ts = get_ticks(); + + LM_DBG("deserialized AV state=%d algmask=%d alg=%d nonce=%.*s\n", + av->state, av->algmask, av->alg, av->authenticate.len, av->authenticate.s); + return av; +} + +/* + * Store AV in CacheDB + */ +int aka_cdb_store_av(str *impu, str *impi, struct aka_av *av) +{ + str key, value; + int ret = -1; + + if (!aka_cdb) { + return 0; /* CacheDB not configured, silently succeed */ + } + + if (aka_cdb_build_key(impu, impi, &av->authenticate, &key) < 0) + return -1; + + if (aka_cdb_serialize_av(av, &value) < 0) { + pkg_free(key.s); + return -1; + } + + LM_DBG("storing AV key=%.*s ttl=%d\n", key.len, key.s, aka_cdb_av_ttl); + if (aka_cdbf.set(aka_cdb, &key, &value, aka_cdb_av_ttl) < 0) { + LM_ERR("failed to store AV in cachedb\n"); + } else { + ret = 0; + } + + pkg_free(key.s); + pkg_free(value.s); + return ret; +} + +/* + * Fetch AV from CacheDB + * Returns new AV allocated in shm memory, or NULL if not found + */ +struct aka_av *aka_cdb_fetch_av(str *impu, str *impi, str *nonce) +{ + str key, value; + struct aka_av *av = NULL; + + if (!aka_cdb) { + return NULL; /* CacheDB not configured */ + } + + if (aka_cdb_build_key(impu, impi, nonce, &key) < 0) + return NULL; + + value.s = NULL; + value.len = 0; + + LM_DBG("fetching AV key=%.*s\n", key.len, key.s); + if (aka_cdbf.get(aka_cdb, &key, &value) <= 0 || value.s == NULL) { + LM_DBG("AV not found in cachedb for key=%.*s\n", key.len, key.s); + pkg_free(key.s); + return NULL; + } + + av = aka_cdb_deserialize_av(&value); + if (av) { + LM_DBG("fetched AV from cachedb key=%.*s state=%d\n", + key.len, key.s, av->state); + } + + pkg_free(key.s); + pkg_free(value.s); + return av; +} + +/* + * Remove AV from CacheDB + */ +int aka_cdb_remove_av(str *impu, str *impi, str *nonce) +{ + str key; + int ret = -1; + + if (!aka_cdb) { + return 0; /* CacheDB not configured, silently succeed */ + } + + if (aka_cdb_build_key(impu, impi, nonce, &key) < 0) + return -1; + + LM_DBG("removing AV key=%.*s\n", key.len, key.s); + if (aka_cdbf.remove(aka_cdb, &key) < 0) { + LM_DBG("failed to remove AV from cachedb (may not exist)\n"); + } else { + ret = 0; + } + + pkg_free(key.s); + return ret; +} + struct aka_av_mgm *aka_get_mgm(str *name) { @@ -288,6 +562,34 @@ struct aka_av *aka_av_get_nonce(struct aka_user *user, int algmask, str *nonce) av->state = AKA_AV_USED; } cond_unlock(&user->cond); + + /* If not found locally, try CacheDB */ + if (!av && aka_cdb) { + LM_DBG("AV not found locally, checking CacheDB for nonce=%.*s\n", + nonce->len, nonce->s); + av = aka_cdb_fetch_av(&user->impu, &user->impi->impi, nonce); + if (av) { + /* Check algorithm compatibility */ + if (algmask >= 0 && av->algmask >= 0 && !(algmask & av->algmask)) { + LM_DBG("AV found in CacheDB but algorithm mismatch\n"); + shm_free(av); + return NULL; + } + /* Check state - only USING or USED states are valid for authorization */ + if (av->state != AKA_AV_USING && av->state != AKA_AV_USED) { + LM_DBG("AV found in CacheDB but invalid state %d\n", av->state); + shm_free(av); + return NULL; + } + /* Insert into local user's AV list */ + cond_lock(&user->cond); + av->state = AKA_AV_USED; + aka_av_insert(user, av); + cond_unlock(&user->cond); + LM_DBG("AV fetched from CacheDB and inserted locally\n"); + } + } + return av; } @@ -369,6 +671,10 @@ int aka_av_get_new_wait(struct aka_user *user, int algmask, } end: cond_unlock(&user->cond); + /* Update CacheDB with USING state after releasing lock */ + if (ret == 1 && *av) { + aka_cdb_store_av(&user->impu, &user->impi->impi, *av); + } return ret; } @@ -389,6 +695,10 @@ int aka_av_get_new(struct aka_user *user, int algmask, struct aka_av **av) user->error_count--; } cond_unlock(&user->cond); + /* Update CacheDB with USING state after releasing lock */ + if (ret == 1 && *av) { + aka_cdb_store_av(&user->impu, &user->impi->impi, *av); + } return ret; } @@ -448,8 +758,12 @@ static struct aka_av *aka_av_new(int algmask, str *authenticate, str *authorize, return av; } -void aka_av_free(struct aka_av *av) +void aka_av_free(struct aka_av *av, str *impu, str *impi) { + /* Remove from CacheDB if configured */ + if (impu && impi) { + aka_cdb_remove_av(impu, impi, &av->authenticate); + } list_del(&av->list); shm_free(av); } @@ -486,6 +800,11 @@ int aka_av_add(str *pub_id, str *priv_id, int algmask, av->ts = av->new_ts = get_ticks(); ret = 1; LM_DBG("adding av %p\n", av); + + /* Store AV in CacheDB for cross-node synchronization */ + if (aka_cdb_store_av(pub_id, priv_id, av) < 0) { + LM_WARN("failed to store AV in cachedb, cross-node auth may fail\n"); + } end: aka_user_release(user); return ret; @@ -570,6 +889,9 @@ void aka_av_set_new(struct aka_user *user, struct aka_av *av) av->state = AKA_AV_NEW; av->ts = av->new_ts; /* restore the new timestamp */ cond_unlock(&user->cond); + + /* Update state in CacheDB */ + aka_cdb_store_av(&user->impu, &user->impi->impi, av); } void aka_push_async(struct aka_user *user, struct list_head *subs) @@ -605,7 +927,8 @@ static int aka_async_hash_iterator(void *param, str key, void *value) aka_check_expire_async(ticks, it); } list_for_each_safe(it, safe, &user->avs) { - aka_check_expire_av(ticks, list_entry(it, struct aka_av, list)); + aka_check_expire_av(ticks, list_entry(it, struct aka_av, list), + &user->impu, &user->impi->impi); } cond_unlock(&user->cond); aka_user_try_free(user); diff --git a/modules/auth_aka/auth_aka.c b/modules/auth_aka/auth_aka.c index 2dfc93527b..0063f4215a 100644 --- a/modules/auth_aka/auth_aka.c +++ b/modules/auth_aka/auth_aka.c @@ -46,9 +46,15 @@ #include "../../parser/parse_to.h" #include "../../parser/parse_uri.h" +#include "../../cachedb/cachedb.h" auth_api_t auth_api; +/* CacheDB support for AV synchronization across nodes */ +static str aka_cachedb_url = {NULL, 0}; +cachedb_funcs aka_cdbf; +cachedb_con *aka_cdb = NULL; + static int aka_www_authorize(struct sip_msg *msg, str *realm); static int aka_proxy_authorize(struct sip_msg *msg, str *realm); static int aka_www_challenge(struct sip_msg *msg, struct aka_av_mgm *mgm, @@ -184,7 +190,8 @@ static const param_export_t params[] = { {"default_algorithm", STR_PARAM, &aka_default_alg_s.s }, {"hash_size", INT_PARAM, &aka_hash_size }, {"sync_timeout", INT_PARAM, &aka_sync_timeout }, - {"async_timeout", INT_PARAM, &aka_async_timeout }, + {"async_timeout", INT_PARAM, &aka_async_timeout }, + {"cachedb_url", STR_PARAM, &aka_cachedb_url.s }, {0, 0, 0} }; @@ -214,9 +221,11 @@ static const mi_export_t mi_cmds[] = { static const dep_export_t deps = { { /* OpenSIPS module dependencies */ { MOD_TYPE_DEFAULT, "auth", DEP_ABORT }, + { MOD_TYPE_CACHEDB, NULL, DEP_SILENT }, { MOD_TYPE_NULL, NULL, 0 }, }, { /* modparam dependencies */ + { "cachedb_url", get_deps_cachedb_url }, { NULL, NULL }, }, }; @@ -276,7 +285,25 @@ static int mod_init(void) } aka_async_timeout /= 1000; /* XXX: add support for milliseconds */ - if (aka_init_mgm(aka_hash_size) < 0) { + /* Initialize CacheDB if URL is provided */ + if (aka_cachedb_url.s) { + aka_cachedb_url.len = strlen(aka_cachedb_url.s); + if (cachedb_bind_mod(&aka_cachedb_url, &aka_cdbf) < 0) { + LM_ERR("cannot bind functions for cachedb_url %.*s\n", + aka_cachedb_url.len, aka_cachedb_url.s); + return -1; + } + aka_cdb = aka_cdbf.init(&aka_cachedb_url); + if (!aka_cdb) { + LM_ERR("cannot connect to cachedb_url %.*s\n", + aka_cachedb_url.len, aka_cachedb_url.s); + return -1; + } + LM_INFO("CacheDB AV sync enabled with %.*s\n", + aka_cachedb_url.len, aka_cachedb_url.s); + } + + if (aka_init_mgm(aka_hash_size, aka_pending_timeout) < 0) { LM_ERR("cannot initialize aka management hash\n"); return -1; } @@ -876,6 +903,33 @@ static int aka_authorize(struct sip_msg *_msg, str *_realm, private_id->len, private_id->s); user = aka_user_find(public_id, private_id); if (user == NULL) { + /* User not found locally - check CacheDB if configured */ + if (aka_cdb && digest->nonce.len) { + LM_DBG("user not found locally, checking CacheDB for nonce %.*s\n", + digest->nonce.len, digest->nonce.s); + av = aka_cdb_fetch_av(public_id, private_id, &digest->nonce); + if (av) { + /* Check state - only USING or USED states are valid */ + if (av->state != AKA_AV_USING && av->state != AKA_AV_USED) { + LM_DBG("AV found in CacheDB but invalid state %d\n", av->state); + shm_free(av); + return STALE_NONCE; + } + /* Create user locally and attach the AV */ + user = aka_user_get(public_id, private_id); + if (user) { + cond_lock(&user->cond); + av->state = AKA_AV_USED; + list_add_tail(&av->list, &user->avs); + cond_unlock(&user->cond); + LM_DBG("created local user from CacheDB AV\n"); + goto av_found; + } else { + shm_free(av); + av = NULL; + } + } + } if (digest->nonce.len) LM_ERR("could not get AKA user %.*s/%.*s with nonce %.*s\n", public_id->len, public_id->s, private_id->len, private_id->s, @@ -893,6 +947,7 @@ static int aka_authorize(struct sip_msg *_msg, str *_realm, ret = STALE_NONCE; goto release; } +av_found: /* now that we are trusting the user, check whether it has an auts * parameter - if it does, we need to re-challenge him */ @@ -1164,7 +1219,7 @@ void aka_check_expire_async(unsigned int ticks, struct list_head *subs) aka_signal_async_resume(param, aka_challenge_resume_tout); } -void aka_check_expire_av(unsigned int ticks, struct aka_av *av) +void aka_check_expire_av(unsigned int ticks, struct aka_av *av, str *impu, str *impi) { int timeout; switch (av->state) { @@ -1186,7 +1241,7 @@ void aka_check_expire_av(unsigned int ticks, struct aka_av *av) return; LM_DBG("removing av %p in state %d after %ds now %ds\n", av, av->state, timeout, ticks); - aka_av_free(av); + aka_av_free(av, impu, impi); } diff --git a/modules/auth_aka/auth_aka.h b/modules/auth_aka/auth_aka.h index 1f5d080cc9..a92f417928 100644 --- a/modules/auth_aka/auth_aka.h +++ b/modules/auth_aka/auth_aka.h @@ -28,8 +28,13 @@ #include "../../lib/list.h" #include "../../parser/digest/digest_parser.h" #include "../../lib/digest_auth/digest_auth.h" +#include "../../cachedb/cachedb.h" #include "aka_av_mgm.h" +/* CacheDB support for AV synchronization across nodes */ +extern cachedb_funcs aka_cdbf; +extern cachedb_con *aka_cdb; + enum aka_user_state { AKA_USER_STATE_INIT = 0, }; @@ -82,7 +87,7 @@ struct aka_av_mgm { -int aka_init_mgm(int hash_size); +int aka_init_mgm(int hash_size, int pending_timeout); struct aka_av_mgm *aka_get_mgm(str *name); struct aka_av_mgm *aka_load_mgm(str *name); @@ -111,9 +116,14 @@ void aka_pop_async(struct aka_user *user, struct list_head *subs); void aka_pop_unsafe_async(struct aka_user *user, struct list_head *subs); void aka_signal_async(struct aka_user *user, struct list_head *subs); void aka_check_expire_async(unsigned int ticks, struct list_head *subs); -void aka_check_expire_av(unsigned int ticks, struct aka_av *av); -void aka_av_free(struct aka_av *av); +void aka_check_expire_av(unsigned int ticks, struct aka_av *av, str *impu, str *impi); +void aka_av_free(struct aka_av *av, str *impu, str *impi); void aka_async_expire(unsigned int ticks, void* param); +/* CacheDB helper functions */ +int aka_cdb_store_av(str *impu, str *impi, struct aka_av *av); +struct aka_av *aka_cdb_fetch_av(str *impu, str *impi, str *nonce); +int aka_cdb_remove_av(str *impu, str *impi, str *nonce); + #endif /* AUTH_AKA_H */ diff --git a/modules/auth_aka/doc/auth_aka_admin.xml b/modules/auth_aka/doc/auth_aka_admin.xml index d17081c435..d1a3fbec77 100644 --- a/modules/auth_aka/doc/auth_aka_admin.xml +++ b/modules/auth_aka/doc/auth_aka_admin.xml @@ -77,6 +77,64 @@ +
+ Clustering / Multi-Node Support + + In distributed deployments where multiple OpenSIPS nodes handle + SIP REGISTER requests, the AKA authentication flow requires that + the authentication vector (AV) issued in the 401 challenge is + available on the node that receives the subsequent authenticated + REGISTER. + + + To support this scenario, the module can optionally use the + OpenSIPS CacheDB infrastructure to synchronize authentication + vectors across nodes. When the + parameter is set: + + + AVs are stored in the external cache when created + + + AVs are fetched from the cache when not found locally + + + AV state changes are synchronized to the cache + + + AVs are removed from the cache when they expire + + + + + Supported CacheDB backends include: + + + Redis (cachedb_redis module) + + + MongoDB (cachedb_mongodb module) + + + Cassandra (cachedb_cassandra module) + + + Any other CacheDB-compatible backend + + + + + Clustered setup configuration + + +loadmodule "cachedb_redis.so" +loadmodule "auth_aka.so" +modparam("cachedb_redis", "cachedb_url", "redis://redis-cluster:6379/") +modparam("auth_aka", "cachedb_url", "redis://redis-cluster:6379/") + + +
+
Dependencies
@@ -95,6 +153,13 @@ them in the AV storage + + + cachedb_* module (optional) + -- required only if + is set for multi-node AV synchronization + +
@@ -246,7 +311,7 @@ modparam("auth_aka", "unused_timeout", 120)
- <varname>unused_timeout</varname> (integer) + <varname>pending_timeout</varname> (integer) The amount of seconds an authentication vector that is being used in the authentication process shall stay in memory. @@ -267,6 +332,61 @@ modparam("auth_aka", "pending_timeout", 10)
+
+ <varname>cachedb_url</varname> (string) + + If set, this parameter enables the synchronization of + authentication vectors across multiple OpenSIPS nodes through + the CacheDB interface. This is essential for distributed/clustered + deployments where one node may issue the 401 challenge and another + node may receive the subsequent authenticated REGISTER request. + + + When enabled, authentication vectors are stored in the configured + CacheDB backend (e.g., Redis, MongoDB, Cassandra) with a TTL based + on the parameter plus a + small margin. + + + The flow for multi-node authentication is: + + + Node A receives initial REGISTER without credentials + + + Node A fetches AV, stores it in CacheDB, sends 401 + + + Node B receives REGISTER with credentials + + + Node B looks up AV locally, on miss fetches from CacheDB + + + Node B validates credentials using the cached AV + + + + + If not set (default), authentication vectors are only stored + locally and multi-node authentication will not work. + + + <varname>cachedb_url</varname> parameter usage + + +# Using Redis for AV synchronization +loadmodule "cachedb_redis.so" +modparam("cachedb_redis", "cachedb_url", "redis://localhost:6379/") +modparam("auth_aka", "cachedb_url", "redis://localhost:6379/") + +# Using MongoDB for AV synchronization +loadmodule "cachedb_mongodb.so" +modparam("cachedb_mongodb", "cachedb_url", "mongodb://localhost:27017/opensips") +modparam("auth_aka", "cachedb_url", "mongodb://localhost:27017/opensips") + + +