diff --git a/modules/topology_hiding/README b/modules/topology_hiding/README index 51afa8b6ff8..de785cfa40b 100644 --- a/modules/topology_hiding/README +++ b/modules/topology_hiding/README @@ -24,6 +24,9 @@ topology_hiding Module 1.3.9. th_contact_caller_username_var (string) 1.3.10. th_contact_callee_username_var (string) 1.3.11. th_callid_loop_protection (int) + 1.3.12. th_state_url (string) + 1.3.13. th_state_ttl (int) + 1.3.14. th_state_ttl_short (int) 1.4. Exported Functions @@ -63,11 +66,14 @@ topology_hiding Module 1.9. Set th_contact_caller_username_var parameter 1.10. Set th_contact_callee_username_var parameter 1.11. Set th_callid_loop_protection parameter - 1.12. topology_hiding usage - 1.13. Calling topology_hiding_match() function for topology + 1.12. Set th_state_url parameter + 1.13. Set th_state_ttl parameter + 1.14. Set th_state_ttl_short parameter + 1.15. topology_hiding usage + 1.16. Calling topology_hiding_match() function for topology hiding sequential requests - 1.14. topology_hiding_match_dialog() usage + 1.17. topology_hiding_match_dialog() usage Chapter 1. Admin Guide @@ -257,6 +263,210 @@ ding_username_var__") modparam("topology_hiding", "th_callid_loop_protection", 1) ... +1.3.12. th_state_url (string) + + The URL of a cachedb backend where the topology hiding state of + dialog-less calls is to be kept. + + When topology hiding runs without a dialog, the whole encoded + state normally travels inside the Contact URI parameter (see + th_contact_encode_param), which makes that URI long. Some user + agents cannot cope with it and truncate the parameter, after + which topology_hiding_match() can no longer decode it and the + sequential requests of those calls are lost. This is most + visible for non-INVITE dialogs, such as SUBSCRIBE, as those can + never be given a dialog to hold the state - the dialog module + only handles INVITE based dialogs. + + With this parameter set, the state is stored in the given + backend under a short key, and only that key travels in the + Contact URI, keeping it short regardless of how large the + hidden topology is. Off the wire the obfuscation and URI-safe + encoding a Contact-borne state carries serve no purpose, so the + stored copy holds neither: it is kept as plain, length-prefixed + text. + + A state is only ever kept on the server side when the message + it belongs to tells how long its dialog is going to live, since + a state which expires from under a dialog which is still up + cannot be matched anymore, and takes down everything that was + still to be routed through it - the BYE of a call, most + notably. A subscription is bounded by its expires, and the + messages which open no dialog only live as long as their + transaction, so those are stored. A call is not bounded by + anything, unless the session timers are in use, so the state of + a call which has none keeps travelling in its Contact, where it + cannot expire - exactly as it does without this parameter. Both + may well be in use at the same time, as this is decided for + each dialog as its Contact is encoded. + + Note that a call which is given a dialog never gets here at + all: the dialog module holds its state and bounds it for + exactly as long as the call lives, and its Contact carries just + the dialog id, so it is already both short and correct. + Engaging a dialog for the calls (see force_dialog) is therefore + a better answer for them than this parameter could ever be, + which is left to serve what a dialog cannot: everything that is + not INVITE based. + + The state must be readable by whichever node receives the + sequential requests, so when the traffic is distributed or + fails over between several OpenSIPS instances, the backend must + be shared between them (e.g. a common cachedb_redis instance or + cluster). For a single instance, a local backend such as + cachedb_local is enough - it is kept in shared memory, so all + the OpenSIPS workers do see it, but its contents are lost on + restart and are not visible to the other nodes. + + Security: the state kept in the backend is stored in the clear. + It is not encrypted, and - unlike the copy that travels inside + a Contact - it is not obfuscated either, so anyone able to read + the backend can see the topology it hides: the real Contact, + the route set and the receiving socket of each call. This is + intentional. The store is meant to be a trusted, internal + backend reachable only by the OpenSIPS nodes that share it: + deploy it on a private or DMZ network, bind it to the internal + interface, enable the backend's own authentication, and keep it + off any untrusted network. Do not point th_state_url at a + backend that is reachable beyond that trust boundary. + + The state is immutable, so re-encoding a Contact on a + sequential request only pushes its expiration further out. To + spare the backend a write on every such request, each node + keeps a small in-memory record of what it last stored and skips + the write while the state still has enough of its lifetime left + - a write that would actually shorten a state (a subscription + being torn down) is never skipped. This is transparent and + needs no configuration. + + A state is only ever removed from the backend when it expires + (see th_state_ttl), therefore the backend must implement the + expiration of the values it stores. All the cachedb backends + do, except for cachedb_mongodb, which silently ignores it - + using it here would pile up the states forever, so it is + rejected at startup. + + By default this parameter is not set, and the state travels in + the Contact URI. + + Example 1.12. Set th_state_url parameter +... +# a single instance - state kept in its own shared memory +loadmodule "cachedb_local.so" +modparam("topology_hiding", "th_state_url", "local://topohiding") + +# several instances sharing a Redis server +loadmodule "cachedb_redis.so" +modparam("topology_hiding", "th_state_url", "redis://127.0.0.1:6379/0") + +# a password protected Redis cluster - one node is enough, +# the rest of the cluster is discovered from it +modparam("topology_hiding", "th_state_url", + "redis://:secretpass@10.0.0.20:6379/0") + +# a Memcached server +loadmodule "cachedb_memcached.so" +modparam("topology_hiding", "th_state_url", + "memcached://127.0.0.1:11211/") +... + +1.3.13. th_state_ttl (int) + + For how long, in seconds, a topology hiding state is kept in + the backend configured through th_state_url. It should be at + least as long as the calls whose topology is hidden are + expected to last, as a call whose state has expired can no + longer have its sequential requests matched. + + Whenever a message which keeps its dialog alive is matched, the + state it was sent to has its expiration pushed further, for as + long as that message says the dialog is going to live. This + matters because the party which sent it only learns of a new + state if the reply it gets back carries a Contact of its own - + otherwise it goes on using the state it already knows, which + therefore has to outlive its original expiration. Only the + messages which do carry the lifetime of their dialog may + refresh it - a SUBSCRIBE or NOTIFY through its expires, an + INVITE or UPDATE through its Session-Expires - so that, say, an + in-dialog OPTIONS cannot cut the state of the call it runs into + down to th_state_ttl_short. + + A state is stored each time a Contact is encoded, and is + otherwise only removed when it expires, so the module tries not + to keep any state for longer than it may be needed: + * for SUBSCRIBE and NOTIFY, the state has to outlive the + subscription, so its duration is taken from the very + message being encoded (plus a small margin, to cover the + refresh). A NOTIFY reports how long the subscription + actually has left in its Subscription-State header (RFC + 6665 4.1.3), which is what the notifier granted rather than + what the subscriber asked for, so it is preferred. Failing + that, it is looked for as an “expires” parameter of the + Contact first and as the Expires header afterwards, the + parameter taking precedence over the header. This matters + because a notifier is free to grant a shorter subscription + than the one asked for, and its NOTIFYs are what bring the + state of a subscription back in line with what was really + granted. Note that a subscription may last much longer than + this parameter, in which case its state is kept for longer + as well. A message which ends a subscription (an expires of + “0”, or a NOTIFY reporting it as “terminated”) drops the + state it was sent to right away, instead of leaving it + behind for as long as the subscription was going to last - + the Contact it gets encoded with still takes + th_state_ttl_short, so that the final NOTIFY may be routed + back; + * for INVITE and UPDATE, when the session timers are in use, + the session gets refreshed every Session-Expires seconds + (RFC 4028) and each refresh encodes the Contact anew, so + the state only has to survive one interval - that value + (plus the margin) is used then. Should a refresh not come, + the session is torn down anyway, so this can never expire + the state of a live call. Note that the Expires header is + deliberately not used here: on an INVITE it limits the + validity of the invitation, not the duration of the call it + may lead to; + * OPTIONS, MESSAGE, INFO and PUBLISH neither open a dialog + nor refresh the target of one, so their Contact is only of + interest while their transaction is running - they get + th_state_ttl_short; + * everything else, and any of the above whose expiry could + not be told (no session timers, no Expires), may last for + any amount of time, so it gets this parameter. + + This parameter is only used when th_state_url is set. + + Default value is “3600” (one hour). + + Example 1.13. Set th_state_ttl parameter +... +# hide the topology of calls which may last up to 12 hours +modparam("topology_hiding", "th_state_ttl", 43200) +... + +1.3.14. th_state_ttl_short (int) + + For how long, in seconds, to keep the topology hiding state of + the messages which do not open a dialog nor refresh the target + of one - see th_state_ttl for the exact list. Their Contact is + only used while their transaction is alive, so keeping their + state for as long as a call's would needlessly fill up the + storage. + + It only needs to cover the lifetime of a transaction. Raise it + to the value of th_state_ttl if you have user agents which + wrongly latch onto the Contact of such a request and keep using + it afterwards. + + This parameter is only used when th_state_url is set. + + Default value is “60”. + + Example 1.14. Set th_state_ttl_short parameter +... +modparam("topology_hiding", "th_state_ttl_short", 120) +... + 1.4. Exported Functions 1.4.1. topology_hiding() @@ -322,7 +532,7 @@ modparam("topology_hiding", "th_callid_loop_protection", 1) is advertised on both legs. If the separator is being used, you can control the username put in contact per leg. - Example 1.12. topology_hiding usage + Example 1.15. topology_hiding usage ... if(!has_totag() && is_method("INVITE")) { topology_hiding(); @@ -355,7 +565,7 @@ if(!has_totag() && is_method("INVITE")) { } ... - Example 1.13. Calling topology_hiding_match() function for + Example 1.16. Calling topology_hiding_match() function for topology hiding sequential requests ... if (has_totag()) @@ -384,7 +594,7 @@ ogy hiding dialog\n"); This function can be used from REQUEST_ROUTE. - Example 1.14. topology_hiding_match_dialog() usage + Example 1.17. topology_hiding_match_dialog() usage ... if (has_totag()) { if (!topology_hiding_match() ) { diff --git a/modules/topology_hiding/doc/topology_hiding_admin.xml b/modules/topology_hiding/doc/topology_hiding_admin.xml index a713c789e1f..48f4d9e72fc 100644 --- a/modules/topology_hiding/doc/topology_hiding_admin.xml +++ b/modules/topology_hiding/doc/topology_hiding_admin.xml @@ -281,6 +281,273 @@ modparam("topology_hiding", "th_callid_loop_protection", 1) +
+ <varname>th_state_url</varname> (string) + + The URL of a cachedb backend where the + topology hiding state of dialog-less calls is to be kept. + + + When topology hiding runs without a dialog, the whole encoded + state normally travels inside the Contact URI parameter (see + ), which makes + that URI long. Some user agents cannot cope with it and truncate + the parameter, after which + can no longer decode + it and the sequential requests of those calls are lost. This is + most visible for non-INVITE dialogs, such as SUBSCRIBE, as those + can never be given a dialog to hold the state - the + dialog module only handles INVITE based + dialogs. + + + With this parameter set, the state is stored in the given backend + under a short key, and only that key travels in the Contact URI, + keeping it short regardless of how large the hidden topology is. + Off the wire the obfuscation and URI-safe encoding a Contact-borne + state carries serve no purpose, so the stored copy holds neither: + it is kept as plain, length-prefixed text. + + + A state is only ever kept on the server side when the message it + belongs to tells how long its dialog is going to live, since a + state which expires from under a dialog which is still up cannot + be matched anymore, and takes down everything that was still to be + routed through it - the BYE of a call, most notably. A + subscription is bounded by its expires, and the messages which + open no dialog only live as long as their transaction, so those + are stored. A call is not bounded by anything, unless the session + timers are in use, so the state of a call which has none keeps + travelling in its Contact, where it cannot expire - exactly as it + does without this parameter. Both may well be in use at the same + time, as this is decided for each dialog as its Contact is + encoded. + + + Note that a call which is given a dialog never gets here at all: + the dialog module holds its state and bounds + it for exactly as long as the call lives, and its Contact carries + just the dialog id, so it is already both short and correct. + Engaging a dialog for the calls (see + ) is therefore a better answer + for them than this parameter could ever be, which is left to serve + what a dialog cannot: everything that is not INVITE based. + + + The state must be readable by whichever node receives the + sequential requests, so when the traffic is distributed or fails + over between several &osips; instances, the backend must be shared + between them (e.g. a common cachedb_redis + instance or cluster). For a single instance, a local backend such + as cachedb_local is enough - it is kept in + shared memory, so all the &osips; workers do see it, but its + contents are lost on restart and are not visible to the other + nodes. + + + Security: the state kept in the + backend is stored in the clear. It is not encrypted, and - unlike + the copy that travels inside a Contact - it is not obfuscated + either, so anyone able to read the backend can see the topology it + hides: the real Contact, the route set and the receiving socket of + each call. This is intentional. The store is meant to be a trusted, + internal backend reachable only by the &osips; nodes that share it: + deploy it on a private or DMZ network, bind it to the internal + interface, enable the backend's own authentication, and keep it off + any untrusted network. Do not point + th_state_url at a backend that is reachable + beyond that trust boundary. + + + The state is immutable, so re-encoding a Contact on a sequential + request only pushes its expiration further out. To spare the + backend a write on every such request, each node keeps a small + in-memory record of what it last stored and skips the write while + the state still has enough of its lifetime left - a write that + would actually shorten a state (a subscription being torn down) + is never skipped. This is transparent and needs no configuration. + + + A state is only ever removed from the backend when it expires + (see ), therefore the backend + must implement the expiration of the values it stores. All the + cachedb backends do, except for + cachedb_mongodb, which silently ignores it - + using it here would pile up the states forever, so it is rejected + at startup. + + + + By default this parameter is not set, and the state travels + in the Contact URI. + + + + Set <varname>th_state_url</varname> parameter + +... +# a single instance - state kept in its own shared memory +loadmodule "cachedb_local.so" +modparam("topology_hiding", "th_state_url", "local://topohiding") + +# several instances sharing a Redis server +loadmodule "cachedb_redis.so" +modparam("topology_hiding", "th_state_url", "redis://127.0.0.1:6379/0") + +# a password protected Redis cluster - one node is enough, +# the rest of the cluster is discovered from it +modparam("topology_hiding", "th_state_url", + "redis://:secretpass@10.0.0.20:6379/0") + +# a Memcached server +loadmodule "cachedb_memcached.so" +modparam("topology_hiding", "th_state_url", + "memcached://127.0.0.1:11211/") +... + + +
+ +
+ <varname>th_state_ttl</varname> (int) + + For how long, in seconds, a topology hiding state is kept in the + backend configured through . + It should be at least as long as the calls whose topology is + hidden are expected to last, as a call whose state has expired can + no longer have its sequential requests matched. + + + Whenever a message which keeps its dialog alive is matched, the + state it was sent to has its expiration pushed further, for as + long as that message says the dialog is going to live. This + matters because the party which sent it only learns of a new state + if the reply it gets back carries a Contact of its own - otherwise + it goes on using the state it already knows, which therefore has + to outlive its original expiration. Only the messages which do + carry the lifetime of their dialog may refresh it - a + SUBSCRIBE or NOTIFY + through its expires, an INVITE or + UPDATE through its + Session-Expires - so that, say, an in-dialog + OPTIONS cannot cut the state of the call it runs into down to + . + + + A state is stored each time a Contact is encoded, and is otherwise + only removed when it expires, so the module tries not to keep any + state for longer than it may be needed: + + + + for SUBSCRIBE and + NOTIFY, the state has to outlive the + subscription, so its duration is taken from the very message + being encoded (plus a small margin, to cover the refresh). A + NOTIFY reports how long the subscription actually has left in + its Subscription-State header (RFC 6665 + 4.1.3), which is what the notifier granted rather than what + the subscriber asked for, so it is preferred. Failing that, it + is looked for as an expires parameter of the + Contact first and as the Expires header + afterwards, the parameter taking precedence over the header. + This matters because a notifier is free to grant a shorter + subscription than the one asked for, and its NOTIFYs are what + bring the state of a subscription back in line with what was + really granted. + Note that a subscription may last much longer than this + parameter, in which case its state is kept for longer as + well. A message which ends a subscription (an expires of + 0, or a NOTIFY reporting it as + terminated) drops the state it was sent to + right away, + instead of leaving it behind for as long as the subscription + was going to last - the Contact it gets encoded with still + takes , so that the + final NOTIFY may be routed back; + + + for INVITE and + UPDATE, when the session timers are in + use, the session gets refreshed every + Session-Expires seconds (RFC 4028) and + each refresh encodes the Contact anew, so the state only has + to survive one interval - that value (plus the margin) is used + then. Should a refresh not come, the session is torn down + anyway, so this can never expire the state of a live call. + Note that the Expires header is + deliberately not used here: on an INVITE + it limits the validity of the invitation, not the duration of + the call it may lead to; + + + OPTIONS, MESSAGE, + INFO and PUBLISH + neither open a dialog nor refresh the target of one, so their + Contact is only of interest while their transaction is + running - they get + ; + + + everything else, and any of the above whose expiry could not + be told (no session timers, no Expires), may last for any + amount of time, so it gets this parameter. + + + + This parameter is only used when + is set. + + + + Default value is 3600 (one hour). + + + + Set <varname>th_state_ttl</varname> parameter + +... +# hide the topology of calls which may last up to 12 hours +modparam("topology_hiding", "th_state_ttl", 43200) +... + + +
+ +
+ <varname>th_state_ttl_short</varname> (int) + + For how long, in seconds, to keep the topology hiding state of the + messages which do not open a dialog nor refresh the target of one + - see for the exact list. + Their Contact is only used while their transaction is alive, so + keeping their state for as long as a call's would needlessly fill + up the storage. + + + It only needs to cover the lifetime of a transaction. Raise it to + the value of if you have user + agents which wrongly latch onto the Contact of such a request and + keep using it afterwards. + + + This parameter is only used when + is set. + + + + Default value is 60. + + + + Set <varname>th_state_ttl_short</varname> parameter + +... +modparam("topology_hiding", "th_state_ttl_short", 120) +... + + +
diff --git a/modules/topology_hiding/th_store.c b/modules/topology_hiding/th_store.c new file mode 100644 index 00000000000..c637f52d006 --- /dev/null +++ b/modules/topology_hiding/th_store.c @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips 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 + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "../../dprint.h" +#include "../../mem/mem.h" +#include "../../mem/shm_mem.h" +#include "../../locking.h" +#include "../../ut.h" +#include "../../md5utils.h" +#include "../../cachedb/cachedb.h" +#include "th_store.h" + +str th_state_url = {NULL, 0}; +int th_state_ttl = 3600; +int th_state_ttl_short = 60; + +static enum th_store_type th_store_be = TH_STORE_NONE; + +static cachedb_funcs th_cdbf; +static cachedb_con *th_cdbc; + +/* the stored keys are prefixed, so that the storage may be shared with + * other users without clashing over the key names */ +#define TH_KEY_PREFIX "th:" +#define TH_KEY_PREFIX_LEN (sizeof(TH_KEY_PREFIX)-1) + +int th_store_enabled(void) +{ + return th_store_be != TH_STORE_NONE; +} + + +/* + * Write-coalescing cache (shared memory, one per node). + * + * The stored state is immutable, so re-storing it on every sequential + * request of a dialog only ever pushes its expiration further out. This + * cache remembers, per key, the expiry this node last wrote, and lets + * th_store_put()/th_store_refresh() skip the backend write while the state + * still has plenty of life left - turning a busy dialog's stream of + * identical writes into an occasional TTL bump. With anycast/ECMP a + * dialog's requests reach the same node, whose workers share this cache, + * so the repeats are seen and collapsed. + * + * Skipping only ever drops a *redundant* write: a request that asks for a + * shorter life than is stored (a teardown lowering the TTL) still writes, + * and a stale or evicted entry just causes one extra write. The only + * residual effect is that, across a node change, a state might expire + * slightly early - which the user agent recovers from by re-establishing. + * It is a plain best-effort hint, so a single small lock guards it. + */ +#define TH_WC_BITS 12 +#define TH_WC_SIZE (1 << TH_WC_BITS) +#define TH_WC_MASK (TH_WC_SIZE - 1) + +struct th_wc_entry { + char key[TH_KEY_LEN]; + int used; + time_t deadline; /* absolute expiry this node last stored */ +}; + +static struct th_wc_entry *th_wc; /* direct-mapped, in shm */ +static gen_lock_t *th_wc_lock; + +static unsigned int th_wc_slot(const char *s) +{ + unsigned int h = 2166136261u; /* FNV-1a over the wire key */ + int i; + + for (i = 0; i < TH_KEY_LEN; i++) + h = (h ^ (unsigned char)s[i]) * 16777619u; + + return h & TH_WC_MASK; +} + +/* + * Whether the state under @key still has to be written to last @ttl more + * seconds. Returns 1 to write (and records the new expiry), 0 to skip + * because a write this node already made covers it. + */ +static int th_wc_need_write(str *key, int ttl) +{ + struct th_wc_entry *e; + time_t now; + int write; + + if (!th_wc || key->len != TH_KEY_LEN) + return 1; + + now = time(NULL); + e = &th_wc[th_wc_slot(key->s)]; + + lock_get(th_wc_lock); + if (e->used && memcmp(e->key, key->s, TH_KEY_LEN) == 0 && + now + ttl >= e->deadline && /* not asking for a shorter life */ + e->deadline - now >= ttl / 2) { /* still at least half of it left */ + write = 0; + } else { + memcpy(e->key, key->s, TH_KEY_LEN); + e->used = 1; + e->deadline = now + ttl; + write = 1; + } + lock_release(th_wc_lock); + + return write; +} + +static void th_wc_forget(str *key) +{ + struct th_wc_entry *e; + + if (!th_wc || key->len != TH_KEY_LEN) + return; + + e = &th_wc[th_wc_slot(key->s)]; + lock_get(th_wc_lock); + if (e->used && memcmp(e->key, key->s, TH_KEY_LEN) == 0) + e->used = 0; + lock_release(th_wc_lock); +} + + +/* + * The states are only ever removed by the expiration of the stored value, + * so a backend which does not implement it would pile them up forever. + * The cachedb interface has no way of telling whether a backend honours + * the expire argument of its set(), hence the check on the URL scheme. + */ +static int th_store_check_scheme(void) +{ + str scheme; + char *p; + + p = memchr(th_state_url.s, ':', th_state_url.len); + if (!p) { + LM_ERR("cannot extract the backend out of th_state_url %s\n", + db_url_escape(&th_state_url)); + return -1; + } + + scheme.s = th_state_url.s; + scheme.len = p - th_state_url.s; + + if (str_casematch_nt(&scheme, "mongodb")) { + LM_ERR("the mongodb backend ignores the expiration of the values " + "it stores, so the topology hiding states would never be " + "removed from it - use a backend which expires its values, " + "such as redis, memcached or local\n"); + return -1; + } + + return 0; +} + + +int th_store_init(void) +{ + if (!th_state_url.s) + return 0; + + th_state_url.len = strlen(th_state_url.s); + + if (th_store_check_scheme() < 0) + return -1; + + if (cachedb_bind_mod(&th_state_url, &th_cdbf) < 0) { + LM_ERR("cannot bind functions for th_state_url %s\n", + db_url_escape(&th_state_url)); + return -1; + } + + if (!CACHEDB_CAPABILITY(&th_cdbf, + CACHEDB_CAP_GET|CACHEDB_CAP_SET|CACHEDB_CAP_REMOVE)) { + LM_ERR("the cachedb backend of th_state_url does not provide " + "the needed get/set/remove support\n"); + return -1; + } + + if (th_state_ttl <= 0) { + LM_ERR("th_state_ttl must be a positive value\n"); + return -1; + } + if (th_state_ttl_short <= 0) { + LM_ERR("th_state_ttl_short must be a positive value\n"); + return -1; + } + + th_store_be = TH_STORE_CACHEDB; + + /* Best-effort write-coalescing cache. If it cannot be set up, leave it + * off (th_wc == NULL) and simply write on every request - a slower but + * equally correct fallback, so this never fails mod_init. */ + th_wc_lock = lock_alloc(); + if (th_wc_lock && lock_init(th_wc_lock)) { + th_wc = shm_malloc(TH_WC_SIZE * sizeof *th_wc); + if (th_wc) { + memset(th_wc, 0, TH_WC_SIZE * sizeof *th_wc); + } else { + lock_destroy(th_wc_lock); + lock_dealloc(th_wc_lock); + th_wc_lock = NULL; + LM_WARN("no shm for the write-coalescing cache - will store on " + "every request\n"); + } + } else { + if (th_wc_lock) + lock_dealloc(th_wc_lock); + th_wc_lock = NULL; + LM_WARN("cannot init the write-coalescing lock - will store on " + "every request\n"); + } + + LM_INFO("topology hiding state kept in the shared store, " + "ttl %d s (%d s for the dialog-less methods)\n", + th_state_ttl, th_state_ttl_short); + + return 0; +} + + +int th_store_child_init(void) +{ + if (!th_store_enabled()) + return 0; + + th_cdbc = th_cdbf.init(&th_state_url); + if (!th_cdbc) { + LM_ERR("cannot connect to th_state_url %s\n", + db_url_escape(&th_state_url)); + return -1; + } + + return 0; +} + + +void th_store_destroy(void) +{ + if (th_cdbc) { + th_cdbf.destroy(th_cdbc); + th_cdbc = NULL; + } + if (th_wc) { + shm_free(th_wc); + th_wc = NULL; + } + if (th_wc_lock) { + lock_destroy(th_wc_lock); + lock_dealloc(th_wc_lock); + th_wc_lock = NULL; + } +} + + +/* + * Derive the wire key of a state from the given seeds, straight into + * @out (which must hold TH_KEY_LEN bytes). + * + * The key is deterministic on purpose: the seeds identify the dialog leg + * whose state this is (its stable identifiers - the call and the tag of + * the party the state belongs to), so every refresh of that same leg + * derives the very same key. Its stored value is then simply overwritten + * and its expiration pushed further, instead of a fresh random key being + * piled up next to the previous one on each refresh - those would then + * linger in the store until they expired, with no dialog to ever clean + * them up (this mode exists precisely because there is no dialog). + * + * One of the seeds is expected to be a secret (the contact-encoding + * password), which is what keeps the key impossible to guess from one + * dialog to another: without it, knowing a dialog's call-id and tags - + * which travel in the clear - would hand out its hidden topology. + */ +void th_store_make_key(str seeds[], int n, char *out) +{ + char md5[MD5_LEN]; + + MD5StringArray(md5, seeds, n); + /* MD5StringArray emits MD5_LEN(32) hex chars; TH_KEY_LEN of them + * make for a 64-bit key, as wide as the former random one */ + memcpy(out, md5, TH_KEY_LEN); +} + + +/* build the full storage key ("th:" + wire key) into @buf */ +static inline void th_store_key(str *key, char *buf, str *out) +{ + memcpy(buf, TH_KEY_PREFIX, TH_KEY_PREFIX_LEN); + memcpy(buf + TH_KEY_PREFIX_LEN, key->s, key->len); + out->s = buf; + out->len = TH_KEY_PREFIX_LEN + key->len; +} + + +int th_store_put(str *blob, str *key, int ttl) +{ + char buf[TH_KEY_PREFIX_LEN + TH_KEY_LEN]; + str full_key; + + if (!th_store_enabled()) { + LM_BUG("no topology hiding storage configured\n"); + return -1; + } + if (!th_cdbc) { + LM_ERR("not connected to the topology hiding storage\n"); + return -1; + } + if (ttl <= 0) { + LM_BUG("bad ttl %d for the topology hiding state\n", ttl); + return -1; + } + + if (!th_wc_need_write(key, ttl)) { + LM_DBG("topology hiding state under the key is already stored with " + "enough TTL, skipping the write\n"); + return 0; + } + + th_store_key(key, buf, &full_key); + + if (th_cdbf.set(th_cdbc, &full_key, blob, ttl) < 0) { + LM_ERR("failed to store the topology hiding state under <%.*s>\n", + full_key.len, full_key.s); + return -1; + } + + LM_DBG("stored %d bytes of topology hiding state under <%.*s>, " + "expiring in %d s\n", blob->len, full_key.len, full_key.s, ttl); + return 0; +} + + +int th_store_get(str *key, str *blob) +{ + char buf[TH_KEY_PREFIX_LEN + TH_KEY_LEN]; + str full_key; + + if (!th_store_enabled()) { + LM_BUG("no topology hiding storage configured\n"); + return -1; + } + if (!th_cdbc) { + LM_ERR("not connected to the topology hiding storage\n"); + return -1; + } + + if (key->len != TH_KEY_LEN) { + LM_DBG("bad topology hiding key length %d, expected %d - the " + "user agent may have truncated it\n", key->len, TH_KEY_LEN); + return -1; + } + + th_store_key(key, buf, &full_key); + + blob->s = NULL; + blob->len = 0; + + if (th_cdbf.get(th_cdbc, &full_key, blob) < 0) { + LM_ERR("failed to fetch the topology hiding state of <%.*s>\n", + full_key.len, full_key.s); + return -1; + } + if (!blob->s || !blob->len) { + LM_WARN("no topology hiding state found for <%.*s> - it may have " + "expired, check the th_state_ttl* parameters against the " + "lifetime of the hidden calls\n", full_key.len, full_key.s); + if (blob->s) { + pkg_free(blob->s); + blob->s = NULL; + } + return -1; + } + + LM_DBG("fetched %d bytes of topology hiding state for <%.*s>\n", + blob->len, full_key.len, full_key.s); + return 0; +} + + +void th_store_refresh(str *key, str *blob, int ttl) +{ + char buf[TH_KEY_PREFIX_LEN + TH_KEY_LEN]; + str full_key; + + if (!th_store_enabled() || !th_cdbc) + return; + if (key->len != TH_KEY_LEN || !blob->s || !blob->len || ttl <= 0) + return; + + if (!th_wc_need_write(key, ttl)) { + LM_DBG("topology hiding state already refreshed with enough TTL, " + "skipping\n"); + return; + } + + th_store_key(key, buf, &full_key); + + /* there is no way of just pushing the expiration of a value further + * through the cachedb interface, so store it again as it is */ + if (th_cdbf.set(th_cdbc, &full_key, blob, ttl) < 0) + LM_WARN("failed to refresh the topology hiding state of <%.*s>, " + "it may expire while still in use\n", + full_key.len, full_key.s); + else + LM_DBG("refreshed the topology hiding state of <%.*s> for " + "another %d s\n", full_key.len, full_key.s, ttl); +} + + +void th_store_del(str *key) +{ + char buf[TH_KEY_PREFIX_LEN + TH_KEY_LEN]; + str full_key; + + if (!th_store_enabled() || !th_cdbc) + return; + if (key->len != TH_KEY_LEN) + return; + + /* forget any cached expiry, so a later re-create of this key writes */ + th_wc_forget(key); + + th_store_key(key, buf, &full_key); + + /* the state expires on its own anyway, so a failure here is not + * worth failing the request over */ + if (th_cdbf.remove(th_cdbc, &full_key) < 0) + LM_WARN("failed to drop the topology hiding state of <%.*s>, " + "leaving it to expire\n", full_key.len, full_key.s); + else + LM_DBG("dropped the topology hiding state of <%.*s>\n", + full_key.len, full_key.s); +} diff --git a/modules/topology_hiding/th_store.h b/modules/topology_hiding/th_store.h new file mode 100644 index 00000000000..a5aa153ed70 --- /dev/null +++ b/modules/topology_hiding/th_store.h @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips 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 + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef TH_STORE_H +#define TH_STORE_H + +#include "../../str.h" + +/* + * Server-side storage for the topology hiding state of dialog-less calls. + * + * In the default (stateless) mode, the whole encoded state travels in the + * Contact URI parameter, which makes that URI long. Some user agents cannot + * cope with it and truncate the parameter, so the state can no longer be + * decoded when they send a sequential request. + * + * In this mode, the state is kept on the server side under a short key, and + * only that key travels in the Contact URI. Off the wire the obfuscation and + * URI-safe encoding serve no purpose, so the stored copy carries neither: its + * fields are simply length-prefixed in a printable ":" form, + * which keeps it inspectable in the store (only the state that still travels + * inline in a Contact is obfuscated and encoded). The state must be readable + * by whichever node receives the sequential request, so the storage is + * expected to be shared between all the nodes handling the same traffic. + */ + +/* length of the key travelling on the wire (hex chars => 64 bits) */ +#define TH_KEY_LEN 16 + +/* + * A key travels in the Contact URI parameter prefixed by this marker, so + * that it can never be taken for an encoded state travelling in that same + * parameter instead - both are in use at once, as it is decided per dialog + * which of the two it gets (see th_state_storable()). + * + * The marker has to satisfy two things, and '_' is picked because it + * provably does: + * + * 1) No encoded state may ever start with it, or a state would be looked + * up in the storage as if it were a key. The state is emitted by + * word64encode() or word32encode(), whose alphabets are word64digits + * "A-Za-z0-9+." and base32digits "A-Z2-7" (see ut.c), and both pad + * with '-'. So an encoded state is always within [A-Za-z0-9+.-], which + * '_' is not part of. Note this holds whatever its length: telling the + * two apart by length instead would rest on nothing more than the + * minimum size of what gets packed, which no rule keeps true. + * + * Mind that word64 is not the usual base64: it ends in "+." where + * base64 ends in "+/", so '.' - the tempting choice - is one of the + * characters a state can be made of, and would not do here. + * + * 2) It must be legal, unescaped, in the parameter of a SIP URI. Per the + * grammar of RFC 3261 25.1: + * + * pvalue = 1*paramchar + * paramchar = param-unreserved / unreserved / escaped + * unreserved = alphanum / mark + * mark = "-" / "_" / "." / "!" / "~" / "*" / "'" / "(" / ")" + * + * which puts '_' in mark, hence in unreserved, hence in paramchar. + * + * The marker only ever has to be unambiguous inside the value of our own + * parameter: whatever else the URI carries is matched by parameter name + * (see topology_hiding_match()), so no other parameter can shadow it. + */ +#define TH_KEY_MARKER '_' + +/* what a key takes up in the Contact, marker included */ +#define TH_KEY_WIRE_LEN (1 + TH_KEY_LEN) + +/* storage backends */ +enum th_store_type { + TH_STORE_NONE = 0, /* stateless - state travels in the Contact */ + TH_STORE_CACHEDB, /* shared key-value store */ +}; + +extern str th_state_url; +extern int th_state_ttl; +extern int th_state_ttl_short; + +/* added on top of a subscription's Expires, to cover the refresh */ +#define TH_STATE_TTL_MARGIN 30 + +/* is a server-side storage configured? */ +int th_store_enabled(void); + +/* bind the storage backend - to be called from mod_init */ +int th_store_init(void); +/* connect to the storage backend - to be called from child_init */ +int th_store_child_init(void); +void th_store_destroy(void); + +/* + * Derive the wire key of a state from @seeds into @out, which must hold + * TH_KEY_LEN bytes. The key is deterministic, so that every refresh of the + * same dialog leg lands on it again - see the definition for the rationale. + */ +void th_store_make_key(str seeds[], int n, char *out); + +/* + * Store @blob for @ttl seconds under @key. The caller provides the key, + * already filled in (TH_KEY_LEN bytes, e.g. via th_store_make_key), so + * that refreshing a leg overwrites its state in place rather than piling + * up a new one. + */ +int th_store_put(str *blob, str *key, int ttl); + +/* + * Fetch the blob stored under @key. On success, @blob->s is allocated in + * pkg memory and must be freed by the caller. + */ +int th_store_get(str *key, str *blob); + +/* Drop the blob stored under @key, once it is known to be of no use. */ +void th_store_del(str *key); + +/* + * Keep the blob already stored under @key around for another @ttl + * seconds. @blob must be the value it currently holds. + */ +void th_store_refresh(str *key, str *blob, int ttl); + +#endif /* TH_STORE_H */ diff --git a/modules/topology_hiding/topo_hiding_logic.c b/modules/topology_hiding/topo_hiding_logic.c index 5b9d8c58db6..f9f86ccf5e6 100644 --- a/modules/topology_hiding/topo_hiding_logic.c +++ b/modules/topology_hiding/topo_hiding_logic.c @@ -25,7 +25,10 @@ */ #include "../../ut.h" +#include "../../parser/parse_expires.h" +#include "../../parser/parse_sst.h" #include "topo_hiding_logic.h" +#include "th_store.h" extern int force_dialog; extern struct tm_binds tm_api; @@ -63,8 +66,11 @@ static void th_down_onreply(struct cell* t, int type,struct tmcb_params *param); static void th_up_onreply(struct cell* t, int type, struct tmcb_params *param); static void th_no_dlg_onreply(struct cell* t, int type, struct tmcb_params *param); static void th_no_dlg_user_onreply(struct cell* t, int type, struct tmcb_params *param); -static int topo_no_dlg_encode_contact(struct sip_msg *req,int flags,str *routes,str *ct_user); +static int topo_no_dlg_encode_contact(struct sip_msg *req,int flags,str *routes,str *ct_user,int store_state); static int topo_no_dlg_seq_handling(struct sip_msg *msg,str *info); +static int th_state_msg_ttl(struct sip_msg *msg); +static int th_state_storable(struct sip_msg *msg); +static str *th_msg_key_tag(struct sip_msg *msg); static int dlg_th_onreply(struct dlg_cell *dlg, struct sip_msg *rpl, struct sip_msg *req, int init_req, int dir, int dst_leg); @@ -895,7 +901,8 @@ static void _th_no_dlg_onreply(struct cell* t, int type, struct tmcb_params *par if ( !(rpl->REPLY_STATUS>=300 && rpl->REPLY_STATUS<400) ) { if (topo_no_dlg_encode_contact(rpl,flags, - (p?&p->routes:NULL),(p?&p->username:NULL)) < 0) { + (p?&p->routes:NULL),(p?&p->username:NULL), + -1/*decide from the reply*/) < 0) { LM_ERR("Failed to encode contact header \n"); return; } @@ -986,7 +993,8 @@ static int topo_hiding_no_dlg(struct sip_msg *req, return -1; } - if (topo_no_dlg_encode_contact(req,extra_flags,NULL, ¶ms->ct_caller_user) < 0) { + if (topo_no_dlg_encode_contact(req,extra_flags,NULL, ¶ms->ct_caller_user, + -1/*decide from the request*/) < 0) { LM_ERR("Failed to encode contact header \n"); return -1; } @@ -1758,14 +1766,16 @@ int topo_callid_post_raw(str *data, struct sip_msg* foo) /* We encode the RR headers, the actual Contact and the socket str for this leg */ /* Via headers will be restored using the TM module, no need to save anything for them */ -static char* build_encoded_contact_suffix(struct sip_msg* msg, str *routes, int *suffix_len, int flags) +static char* build_encoded_contact_suffix(struct sip_msg* msg, str *routes, int *suffix_len, int flags, int store_state) { short rr_len,ct_len,addr_len,flags_len; - char *suffix_plain,*suffix_enc,*p,*s; + char *suffix_plain = NULL,*suffix_enc = NULL,*p,*s; str rr_set = {NULL, 0}; str contact; str flags_str; - int i,total_len,enc_len; + int i,total_len,enc_len,wire_len; + char *blob_buf = NULL; + str blob, key; struct sip_uri ctu; struct th_ct_params* el; param_t *it; @@ -1817,14 +1827,20 @@ static char* build_encoded_contact_suffix(struct sip_msg* msg, str *routes, int if (topo_ct_short_len(msg->rcv.bind_address->sock_str.len, &addr_len, "bind address") < 0) goto error; - local_len += rr_len + ct_len + flags_len + addr_len; + local_len += rr_len + ct_len + flags_len + addr_len; enc_len = th_ct_enc_scheme == ENC_BASE64 ? calc_word64_encode_len(local_len) : calc_word32_encode_len(local_len); - total_len = enc_len + - 1 /* ; */ + - th_contact_encode_param.len + - 1 /* = */ + - 1 /* > */; + /* a sequential request keeps whatever its dialog is already using, + * anything else is decided upon here */ + if (store_state < 0) + store_state = th_state_storable(msg); + /* with a server-side storage, only the key travels in the Contact */ + wire_len = store_state ? TH_KEY_WIRE_LEN : enc_len; + total_len = wire_len + + 1 /* ; */ + + th_contact_encode_param.len + + 1 /* = */ + + 1 /* > */; if (th_param_list) { if ( parse_contact(msg->contact)<0 || @@ -1894,20 +1910,103 @@ static char* build_encoded_contact_suffix(struct sip_msg* msg, str *routes, int p+= sizeof(short); memcpy(p,msg->rcv.bind_address->sock_str.s,msg->rcv.bind_address->sock_str.len); p+= msg->rcv.bind_address->sock_str.len; - for (i=0;i<(int)(p-suffix_plain);i++) - suffix_plain[i] ^= topo_hiding_ct_encode_pw.s[i%topo_hiding_ct_encode_pw.len]; s = suffix_enc; *s++ = ';'; memcpy(s,th_contact_encode_param.s,th_contact_encode_param.len); s+= th_contact_encode_param.len; *s++ = '='; - if (th_ct_enc_scheme == ENC_BASE64) - word64encode((unsigned char*)s,(unsigned char *)suffix_plain,p-suffix_plain); - else - word32encode((unsigned char*)s,(unsigned char *)suffix_plain,p-suffix_plain); - s = s+enc_len; - + if (store_state) { + /* the state is kept on the server side under a key. It never + * travels on the wire, so it needs neither the XOR obfuscation + * nor the URI-safe word encoding a Contact-borne state does - + * keep it printable and inspectable in the store, framed as + * length-prefixed ":" fields in the same + * order (route set, contact, flags, receiving socket). */ + char *b, *ls; + int ll; + + blob_buf = pkg_malloc(local_len + 4*6); + if (!blob_buf) { + LM_ERR("no more pkg\n"); + goto error; + } + b = blob_buf; + #define __put_ascii_field(_f) \ + do { \ + ls = int2str((unsigned long)(_f).len, &ll); \ + memcpy(b, ls, ll); b += ll; \ + *b++ = ':'; \ + if ((_f).len) { memcpy(b, (_f).s, (_f).len); b += (_f).len; } \ + } while (0) + __put_ascii_field(rr_set); + __put_ascii_field(contact); + __put_ascii_field(flags_str); + __put_ascii_field(msg->rcv.bind_address->sock_str); + #undef __put_ascii_field + blob.s = blob_buf; + blob.len = b - blob_buf; + + /* mark the key, so that it can never be taken for an encoded + * state travelling in the Contact instead */ + *s = TH_KEY_MARKER; + + /* the key is derived straight into the Contact buffer */ + key.s = s + 1; + key.len = TH_KEY_LEN; + + /* + * Key the state on the dialog leg it belongs to, so that a + * refresh of that leg derives the very same key and just + * overwrites its state, instead of leaving a new one behind on + * every request (no dialog holds these, so nothing else would + * ever reclaim them). The leg is the party whose Contact we are + * hiding: its call and its own tag - the From tag of a request + * it sends, the To tag of a reply it sends - both stable for as + * long as the dialog lives. The encoding password is mixed in to + * keep the key from being guessable out of the clear-text call-id + * and tags. + */ + { + str seeds[3]; + str *own_tag = th_msg_key_tag(msg); + + if (!own_tag) { + /* th_state_storable() only lets a message reach here + * once it can be keyed (has both a call-id and its + * leg's tag); the sole caller which forces the state + * to be stored - a sequential request being re-encoded + * - is in-dialog and thus always has both */ + LM_BUG("no call-id or tag to key the topology hiding " + "state on\n"); + goto error; + } + + seeds[0] = topo_hiding_ct_encode_pw; + seeds[1] = msg->callid->body; + seeds[2] = *own_tag; + th_store_make_key(seeds, 3, key.s); + } + + if (th_store_put(&blob, &key, th_state_msg_ttl(msg)) < 0) { + LM_ERR("failed to store the topology hiding state\n"); + goto error; + } + pkg_free(blob_buf); + blob_buf = NULL; + } else { + /* the state travels inline in the Contact - obfuscate it and + * then URI-safe encode it onto the wire */ + for (i=0;i<(int)(p-suffix_plain);i++) + suffix_plain[i] ^= topo_hiding_ct_encode_pw.s[i%topo_hiding_ct_encode_pw.len]; + if (th_ct_enc_scheme == ENC_BASE64) + word64encode((unsigned char*)s,(unsigned char *)suffix_plain,p-suffix_plain); + else + word32encode((unsigned char*)s,(unsigned char *)suffix_plain,p-suffix_plain); + } + s = s+wire_len; + + if (th_param_list) { for (el=th_param_list;el;el=el->next) { /* we just iterate over the unknown params */ @@ -1941,10 +2040,364 @@ static char* build_encoded_contact_suffix(struct sip_msg* msg, str *routes, int error: if (rr_set.s && free_rr_set) pkg_free(rr_set.s); + if (blob_buf) + pkg_free(blob_buf); + if (suffix_plain) + pkg_free(suffix_plain); + if (suffix_enc) + pkg_free(suffix_enc); return NULL; } -static int topo_no_dlg_encode_contact(struct sip_msg *msg,int flags, str *routes, str *ct_user) +/* + * How long the state of @msg has to outlive it. + * + * The states are only removed when they expire, and a new one is stored + * for each request whose Contact gets encoded, so keeping them for longer + * than needed just piles them up in the storage. + */ +/* the method @msg is about, be it a request or the reply to one */ +static int th_msg_method(struct sip_msg *msg) +{ + if (msg->first_line.type == SIP_REQUEST) + return msg->first_line.u.request.method_value; + + return (msg->cseq && get_cseq(msg)) ? + get_cseq(msg)->method_id : METHOD_UNDEF; +} + + +/* + * The expires @msg is subject to, or -1 if it carries none. It may be + * given either as an "expires" Contact parameter or as the Expires + * header, the parameter taking precedence (RFC 3261 10.2.1.1). + */ +static int th_msg_expires(struct sip_msg *msg) +{ + exp_body_t *exp; + contact_t *ct; + unsigned int ct_exp; + + if (msg->contact || + (parse_headers(msg, HDR_CONTACT_F, 0) == 0 && msg->contact)) { + if (msg->contact->parsed || parse_contact(msg->contact) == 0) { + ct = ((contact_body_t *)msg->contact->parsed)->contacts; + if (ct && ct->expires && ct->expires->body.len && + str2int(&ct->expires->body, &ct_exp) == 0) { + LM_DBG("expires in %us, per the Contact\n", ct_exp); + return (int)ct_exp; + } + } + } + + if (parse_headers(msg, HDR_EXPIRES_F, 0) == 0 && msg->expires) { + if (msg->expires->parsed || parse_expires(msg->expires) == 0) { + exp = (exp_body_t *)msg->expires->parsed; + if (exp && exp->valid) { + LM_DBG("expires in %ds, per the Expires header\n", + exp->val); + return exp->val; + } + } + LM_DBG("unparsable Expires header\n"); + } + + return -1; +} + + +#define SUB_STATE_HDR "Subscription-State" +#define SUB_STATE_HDR_LEN (sizeof(SUB_STATE_HDR)-1) + +/* + * What a NOTIFY has to say about the lifetime of its subscription. + * + * A NOTIFY carries it in its Subscription-State header rather than in an + * Expires one (RFC 6665 4.1.3), and the value is the one the notifier + * actually granted, which may well be shorter than what the subscriber + * had asked for. + * + * Returns the seconds the subscription still has, 0 if it is over, or -1 + * if the message says nothing about it. + */ +static int th_notify_sub_expires(struct sip_msg *msg) +{ + struct hdr_field *hf; + param_hooks_t hooks; + param_t *params = NULL, *p; + str body, state; + unsigned int val; + int ret = -1; + char *sep; + + if (parse_headers(msg, HDR_EOH_F, 0) < 0) + return -1; + + /* not one of the headers the parser knows about */ + for (hf = msg->headers; hf; hf = hf->next) + if (hf->type == HDR_OTHER_T && + hf->name.len == SUB_STATE_HDR_LEN && + strncasecmp(hf->name.s, SUB_STATE_HDR, SUB_STATE_HDR_LEN) == 0) + break; + if (!hf) + return -1; + + body = hf->body; + trim(&body); + if (!body.len) + return -1; + + sep = memchr(body.s, ';', body.len); + + state = body; + if (sep) + state.len = sep - body.s; + trim(&state); + + /* a subscription which is over is not going to come back */ + if (state.len == 10 && strncasecmp(state.s, "terminated", 10) == 0) + return 0; + + if (!sep) + return -1; + + /* the parameters start past the separator */ + body.len -= sep - body.s + 1; + body.s = sep + 1; + if (body.len <= 0) + return -1; + + if (parse_params(&body, CLASS_ANY, &hooks, ¶ms) < 0) { + LM_DBG("unparsable " SUB_STATE_HDR " parameters\n"); + return -1; + } + + for (p = params; p; p = p->next) + if (p->name.len == 7 && + strncasecmp(p->name.s, "expires", 7) == 0) { + if (str2int(&p->body, &val) == 0) { + LM_DBG("subscription has %us left, per " SUB_STATE_HDR "\n", + val); + ret = (int)val; + } + break; + } + + free_params(params); + return ret; +} + + +/* + * The lifetime the subscription @msg belongs to still has, or -1 if it + * carries nothing about it. A NOTIFY reports the granted value, so it is + * preferred over anything the subscriber may have merely asked for. + */ +static int th_sub_expires(struct sip_msg *msg, int method) +{ + int expires = -1; + + if (method == METHOD_NOTIFY) + expires = th_notify_sub_expires(msg); + + if (expires < 0) + expires = th_msg_expires(msg); + + return expires; +} + + +/* does @msg tear down the subscription it belongs to? */ +static int th_msg_ends_subscription(struct sip_msg *msg) +{ + int method = th_msg_method(msg); + + if (method != METHOD_SUBSCRIBE && method != METHOD_NOTIFY) + return 0; + + return th_sub_expires(msg, method) == 0; +} + + +/* + * Does @msg push the lifetime of the dialog it belongs to further, and + * by how much? Returns 0 if it does not say anything about it. + * + * Only the messages which do carry the lifetime of their dialog may + * answer here: refreshing a state off, say, an in-dialog OPTIONS would + * cut the state of the call it runs into down to a minute. + */ +static int th_msg_refreshes_state(struct sip_msg *msg) +{ + struct session_expires se; + int expires, method; + + method = th_msg_method(msg); + + switch (method) { + case METHOD_SUBSCRIBE: + case METHOD_NOTIFY: + expires = th_sub_expires(msg, method); + /* an ending subscription drops its state instead */ + if (expires <= 0) + return 0; + return expires + TH_STATE_TTL_MARGIN; + + case METHOD_INVITE: + case METHOD_UPDATE: + if (parse_session_expires(msg, &se) == parse_sst_success && + se.interval > 0) + return (int)se.interval + TH_STATE_TTL_MARGIN; + return 0; + + default: + return 0; + } +} + + +/* + * May the state of @msg be kept on the server side? + * + * Only if @msg tells us how long its dialog is going to live: a state + * which expires from under a dialog which is still up cannot be matched + * anymore, and takes down everything that was still to be routed through + * it - the BYE of a call, most notably. Whatever cannot be bounded keeps + * travelling in the Contact instead, where it never expires. + * + * Note that a call which does get a dialog never reaches here: the + * dialog holds its state and bounds it for exactly as long as it lives, + * which is a better answer than this mode could ever give it. + */ +/* + * The stable identifier of the dialog leg whose Contact @msg carries - + * the tag the party owns: its From tag when it sends a request, its To + * tag when it sends a reply. Both last as long as the dialog, so every + * message of that leg derives the same storage key off it. + * + * Returns NULL when the state cannot be keyed - either the call-id or the + * tag is missing. The latter is most notably the reply of an + * out-of-dialog transaction (a 200 OK to an OPTIONS keepalive, say), + * which carries a Contact but never a To tag. Such a state is not stored: + * it travels in the Contact instead (there is no sequential traffic to + * match it against anyway). + */ +static str *th_msg_key_tag(struct sip_msg *msg) +{ + str *tag; + + /* the key is derived from the call-id and the leg's tag, so both + * have to be there - the call-id of any message, and the tag its + * leg owns */ + if ((!msg->callid && parse_headers(msg, HDR_CALLID_F, 0) < 0) || + !msg->callid) + return NULL; + + if (msg->first_line.type == SIP_REQUEST) { + if (parse_from_header(msg) < 0 || !msg->from || !get_from(msg)) + return NULL; + tag = &get_from(msg)->tag_value; + } else { + if (parse_to_header(msg) < 0 || !msg->to || !get_to(msg)) + return NULL; + tag = &get_to(msg)->tag_value; + } + + return tag->len ? tag : NULL; +} + + +static int th_state_storable(struct sip_msg *msg) +{ + struct session_expires se; + + if (!th_store_enabled()) + return 0; + + /* the state is keyed on the call-id and the leg's tag; with no way + * to key it, it cannot be stored and stays in the Contact instead */ + if (!th_msg_key_tag(msg)) + return 0; + + switch (th_msg_method(msg)) { + case METHOD_INVITE: + case METHOD_UPDATE: + /* a call lasts for as long as it pleases, unless the session + * timers bound it */ + if (parse_session_expires(msg, &se) == parse_sst_success && + se.interval > 0) + return 1; + LM_DBG("no session timers on this call - keeping its state in " + "the Contact, as it may outlive any expiration\n"); + return 0; + + default: + /* a subscription is bounded by its expires, and the rest only + * lives as long as its transaction */ + return 1; + } +} + + +static int th_state_msg_ttl(struct sip_msg *msg) +{ + struct session_expires se; + int method, expires; + + method = th_msg_method(msg); + + switch (method) { + case METHOD_SUBSCRIBE: + case METHOD_NOTIFY: + /* The state is needed for as long as the subscription lives, + * and a subscription may very well outlive the generic + * th_state_ttl. */ + expires = th_sub_expires(msg, method); + if (expires < 0) + return th_state_ttl; + /* a subscription being torn down only needs its transaction to + * complete, and the final NOTIFY to be routed back */ + if (expires == 0) + return th_state_ttl_short; + return expires + TH_STATE_TTL_MARGIN; + + case METHOD_INVITE: + case METHOD_UPDATE: + /* When the session timers are in use, the session is refreshed + * every Session-Expires seconds (RFC 4028), and each refresh + * encodes the Contact anew, so the state only has to survive + * one interval. Should a refresh not come, the session is torn + * down anyway, hence the state of a live call can never be + * expired from under it. Without the header, the call may last + * for as long as it wants to. + * + * Note the value is negotiated, but each message carries the + * one it is subject to, and both the request and the reply are + * encoded from their own message. */ + if (parse_session_expires(msg, &se) == parse_sst_success && + se.interval > 0) { + LM_DBG("session refreshed every %us, per Session-Expires\n", + se.interval); + return (int)se.interval + TH_STATE_TTL_MARGIN; + } + return th_state_ttl; + + case METHOD_OPTIONS: + case METHOD_MESSAGE: + case METHOD_INFO: + case METHOD_PUBLISH: + /* these neither open a dialog nor refresh the target of one + * (RFC 3261 12.2.1.2), so their Contact is only of interest + * while their transaction is running */ + return th_state_ttl_short; + + default: + return th_state_ttl; + } +} + + +static int topo_no_dlg_encode_contact(struct sip_msg *msg,int flags, str *routes, str *ct_user, int store_state) { struct lump* lump; char *prefix=NULL,*suffix=NULL,*ct_username=NULL; @@ -2011,7 +2464,8 @@ static int topo_no_dlg_encode_contact(struct sip_msg *msg,int flags, str *routes /* make sure we do not free this string in case of a further error */ prefix = NULL; - if (!(suffix = build_encoded_contact_suffix(msg, routes, &suffix_len, flags))) { + if (!(suffix = build_encoded_contact_suffix(msg, routes, &suffix_len, flags, + store_state))) { LM_ERR("Failed to build suffix \n"); goto error; } @@ -2057,6 +2511,10 @@ static int topo_no_dlg_seq_handling(struct sip_msg *msg,str *info) str route_buf = {0, 0}; struct th_no_dlg_param *param = NULL; transaction_cb* used_cb; + str stored_blob = {NULL, 0}; + str consumed_key = {NULL, 0}; + int ttl; + int from_storage = 0; /* parse all headers to be sure that all RR and Contact hdrs are found */ if (parse_headers(msg, HDR_EOH_F, 0)< 0) { @@ -2064,48 +2522,115 @@ static int topo_no_dlg_seq_handling(struct sip_msg *msg,str *info) return -1; } + /* + * The state either travels in the Contact or was left in the + * storage, and both may be in use at the same time, as it is + * decided per dialog when its Contact is encoded. A key is told + * apart by its marker, which none of the encodings of an actual + * state can produce. + */ + if (th_store_enabled() && info->len == TH_KEY_WIRE_LEN && + info->s[0] == TH_KEY_MARKER) { + /* the Contact only carried the key of the state - fetch the + * actual encoded state and go on decoding it as usual. Keep + * the key around, it still points into the request */ + consumed_key.s = info->s + 1; + consumed_key.len = TH_KEY_LEN; + if (th_store_get(&consumed_key, &stored_blob) < 0) + return -1; + info = &stored_blob; + from_storage = 1; + } + /* delete vias */ if(topo_delete_vias(msg) < 0) { LM_ERR("Failed to remove via headers\n"); - return -1; + goto err_free_blob; } /* delete record route */ for (it=msg->record_route;it;it=it->sibling) { if (del_lump(msg, it->name.s - buf, it->len, 0) == 0) { LM_ERR("del_lump failed\n"); - return -1; + goto err_free_blob; } } - max_size = th_ct_enc_scheme == ENC_BASE64 ? - calc_max_word64_decode_len(info->len) : - calc_max_word32_decode_len(info->len); - dec_buf = pkg_malloc(max_size); - if (dec_buf==NULL) { - LM_ERR("No more pkg\n"); - return -1; - } - - if (th_ct_enc_scheme == ENC_BASE64) - dec_len = word64decode((unsigned char *)dec_buf, - (unsigned char *)info->s,info->len); - else - dec_len = word32decode((unsigned char *)dec_buf, - (unsigned char *)info->s,info->len); - for (i=0;ilen) : + calc_max_word32_decode_len(info->len); + dec_buf = pkg_malloc(max_size); + if (dec_buf==NULL) { + LM_ERR("No more pkg\n"); + goto err_free_blob; + } + + if (th_ct_enc_scheme == ENC_BASE64) + dec_len = word64decode((unsigned char *)dec_buf, + (unsigned char *)info->s,info->len); + else + dec_len = word32decode((unsigned char *)dec_buf, + (unsigned char *)info->s,info->len); + for (i=0;i 0) + th_store_refresh(&consumed_key, &stored_blob, ttl); + + /* dec_buf owns this buffer now - hand it over instead of + * freeing it, and drop our reference so the error path (which + * frees dec_buf and then stored_blob.s) cannot free it twice. */ + stored_blob.s = NULL; + info = NULL; + } + + /* A field is length-prefixed: a raw 2-byte short for a state that + * came inline in the Contact, or a printable ":" for one + * read back from the store (see build_encoded_contact_suffix). Parse + * in place either way - the field just points into the decode buffer. */ #define __extract_len_and_buf(_p, _len, _s) \ do { \ - (_s).len = *(short *)p;\ - if ((_s).len<0 || (_s).len>_len) {\ + if (from_storage) { \ + int _n = 0; \ + while ((_len) > 0 && *(_p) >= '0' && *(_p) <= '9') { \ + _n = _n*10 + (*(_p) - '0'); (_p)++; (_len)--; \ + } \ + if ((_len) <= 0 || *(_p) != ':') { \ + LM_ERR("bad length framing in stored contact\n"); \ + goto err_free_buf; \ + } \ + (_p)++; (_len)--; \ + (_s).len = _n; \ + } else { \ + (_s).len = *(short *)(_p); \ + (_p) += sizeof(short); \ + (_len) -= sizeof(short); \ + } \ + if ((_s).len<0 || (_s).len>(_len)) {\ LM_ERR("bad length %d in encoded contact\n", (_s).len);\ goto err_free_buf;\ }\ - (_s).s = _p + sizeof(short);\ - _p += sizeof(short) + (_s).len;\ - _len -= sizeof(short) + (_s).len;\ + (_s).s = (_p);\ + (_p) += (_s).len;\ + (_len) -= (_s).len;\ } while(0) p = dec_buf; @@ -2342,7 +2867,19 @@ static int topo_no_dlg_seq_handling(struct sip_msg *msg,str *info) free_rr(&head); pkg_free(dec_buf); - if (topo_no_dlg_encode_contact(msg,flags,NULL,NULL) < 0) { + /* + * A subscription being torn down will not be sending anything to + * this state again, so drop it now rather than leaving it behind + * for as long as the subscription it belonged to was going to last. + * The Contact encoded below gets a state of its own, which is what + * routes the final NOTIFY back. + */ + if (consumed_key.s && th_msg_ends_subscription(msg)) + th_store_del(&consumed_key); + + /* keep this dialog on whichever of the two it came in with */ + if (topo_no_dlg_encode_contact(msg,flags,NULL,NULL, + consumed_key.s ? 1 : 0) < 0) { LM_ERR("Failed to encode contact header \n"); return -1; } @@ -2357,5 +2894,8 @@ static int topo_no_dlg_seq_handling(struct sip_msg *msg,str *info) free_rr(&head); err_free_buf: pkg_free(dec_buf); +err_free_blob: + if (stored_blob.s) + pkg_free(stored_blob.s); return -1; } diff --git a/modules/topology_hiding/topology_hiding.c b/modules/topology_hiding/topology_hiding.c index 5a540fc7c14..f50c3496e42 100644 --- a/modules/topology_hiding/topology_hiding.c +++ b/modules/topology_hiding/topology_hiding.c @@ -29,6 +29,7 @@ #include "topo_hiding_logic.h" +#include "th_store.h" struct tm_binds tm_api; struct dlg_binds dlg_api; @@ -48,6 +49,7 @@ str th_contact_callee_var = str_init("_th_contact_callee_username_var_"); int th_ct_enc_scheme; static int mod_init(void); +static int child_init(int rank); static void mod_destroy(void); static int fixup_mmode(void **param); static int fixup_th_params(void **param); @@ -79,6 +81,9 @@ static const param_export_t params[] = { { "th_contact_caller_username_var", STR_PARAM, &th_contact_caller_var.s }, { "th_contact_callee_username_var", STR_PARAM, &th_contact_callee_var.s }, { "th_callid_loop_protection", INT_PARAM, &th_loop_protection }, + { "th_state_url", STR_PARAM, &th_state_url.s }, + { "th_state_ttl", INT_PARAM, &th_state_ttl }, + { "th_state_ttl_short", INT_PARAM, &th_state_ttl_short }, {0, 0, 0} }; @@ -106,6 +111,7 @@ static const dep_export_t deps = { }, { /* modparam dependencies */ { "force_dialog", get_deps_dialog }, + { "th_state_url", get_deps_cachedb_url }, { NULL, NULL }, }, }; @@ -129,7 +135,7 @@ struct module_exports exports= { mod_init, /* module initialization function */ (response_function) 0, mod_destroy, - 0, /* per-child init function */ + child_init, /* per-child init function */ 0 /* reload confirm function */ }; @@ -164,6 +170,11 @@ static int mod_init(void) } + if (th_store_init() < 0) { + LM_ERR("failed to initialize the topology hiding state storage\n"); + goto error; + } + /* loading dependencies */ if (load_tm_api(&tm_api)!=0) { LM_ERR("can't load TM API\n"); @@ -202,9 +213,19 @@ static int mod_init(void) return -1; } +static int child_init(int rank) +{ + if (th_store_child_init() < 0) { + LM_ERR("failed to connect to the topology hiding state storage\n"); + return -1; + } + + return 0; +} + static void mod_destroy(void) { - return; + th_store_destroy(); } static int fixup_mmode(void **param)