diff --git a/modules/httpd/README.md b/modules/httpd/README.md index 13fc2378263..86356957568 100644 --- a/modules/httpd/README.md +++ b/modules/httpd/README.md @@ -24,6 +24,25 @@ and `tls_key_file` parameters. If this is enabled, support for plain http is disabled. +### Multiple HTTP Servers + + +The module can run several named HTTP listeners, each with its own +worker pool, so requests are handled in parallel instead of serially. +Prefix a modparam value with a listener name in brackets (e.g. +`[critical]port`) to configure that listener; unprefixed values +apply to the `default` listener. + + +```opensips title="Example: two listeners" +... +modparam("httpd", "port", "[default]8888") +modparam("httpd", "port", "[critical]8889") +modparam("httpd", "workers", "[critical]4") +... +``` + + ### Dependencies @@ -66,6 +85,12 @@ lasting until **0.9.71** (May 2020) ### Exported Parameters +> [!NOTE] +> Except for the `auth_*` parameters, all parameters below accept the +> `[name]`-prefixed syntax from [Multiple HTTP Servers](#multiple-http-servers). +> The `auth_*` parameters are global and apply to all listeners. + + #### ip(string) @@ -239,6 +264,23 @@ modparam("httpd", "tls_ciphers", "SECURE256:+SECURE192:-VERS-ALL:+VERS-TLS1.2") ``` +#### workers (integer) + + +The number of worker processes handling HTTP requests for this +listener, in parallel. + + +*The default value is 1.* + + +```opensips title="Set workers parameter" +... +modparam("httpd", "workers", 4) +... +``` + + #### auth_realm (string) diff --git a/modules/httpd/httpd.c b/modules/httpd/httpd.c index 1f9615de5fb..380be044144 100644 --- a/modules/httpd/httpd.c +++ b/modules/httpd/httpd.c @@ -48,6 +48,8 @@ #define MIN_POST_BUF_SIZE 256 #define DEFAULT_POST_BUF_SIZE 1024 +#define DEFAULT_TLS_CIPHERS "SECURE256:+SECURE192:-VERS-ALL:+VERS-TLS1.2" +#define DEFAULT_CONN_TIMEOUT 30 /* module functions */ static int mod_init(); @@ -59,10 +61,10 @@ static mi_response_t *mi_list_root_path(const mi_params_t *params, int port = 8888; str ip = {"127.0.0.1", 9}; str buffer = {NULL, 0}; -unsigned int hd_conn_timeout_s = 30; +unsigned int hd_conn_timeout_s = DEFAULT_CONN_TIMEOUT; str tls_cert_file = {NULL, 0}; str tls_key_file = {NULL, 0}; -str tls_ciphers = {"SECURE256:+SECURE192:-VERS-ALL:+VERS-TLS1.2", 45}; +str tls_ciphers = {DEFAULT_TLS_CIPHERS, sizeof(DEFAULT_TLS_CIPHERS) - 1}; int post_buf_size = DEFAULT_POST_BUF_SIZE; int receive_buf_size = DEFAULT_POST_BUF_SIZE; str auth_realm = {"OpenSIPS MI", 11}; @@ -73,24 +75,150 @@ struct httpd_cb *httpd_cb_list = NULL; char *httpd_receive_buff = NULL; int httpd_receive_buff_pos=0; -static const proc_export_t mi_procs[] = { - {"HTTPD", 0, 0, httpd_proc, 1, +#define HTTPD_DEFAULT_SRV "default" + +struct httpd_server *httpd_servers = NULL; +int httpd_n_servers = 0; + +static proc_export_t mi_procs[] = { + {"HTTPD", httpd_pre_fork, httpd_post_fork, httpd_proc, 1, PROC_FLAG_INITCHILD|PROC_FLAG_HAS_IPC|PROC_FLAG_NEEDS_SCRIPT }, {NULL, 0, 0, NULL, 0, 0} }; +static void httpd_split(char *val, str *name, str *rest) +{ + char *p; + str v; + + v.s = val; + v.len = val ? strlen(val) : 0; + while (v.len && v.s[0] == ' ') { + v.s++; + v.len--; + } + + if (v.len && v.s[0] == '[' && (p = memchr(v.s, ']', v.len))) { + name->s = v.s + 1; + name->len = (int)(p - v.s - 1); + while (name->len && name->s[0] == ' ') { + name->s++; + name->len--; + } + while (name->len && name->s[name->len - 1] == ' ') + name->len--; + + rest->s = p + 1; + rest->len = v.len - (int)(p - v.s) - 1; + } else { + name->s = HTTPD_DEFAULT_SRV; + name->len = sizeof(HTTPD_DEFAULT_SRV) - 1; + rest->s = v.s; + rest->len = v.len; + } + + while (rest->len && rest->s[0] == ' ') { + rest->s++; + rest->len--; + } + while (rest->len && rest->s[rest->len - 1] == ' ') + rest->len--; +} + +static struct httpd_server *httpd_get_server(str *name) +{ + struct httpd_server *arr; + int i; + + for (i = 0; i < httpd_n_servers; i++) + if (httpd_servers[i].name.len == name->len && + memcmp(httpd_servers[i].name.s, name->s, name->len) == 0) + return &httpd_servers[i]; + + arr = realloc(httpd_servers, (httpd_n_servers + 1) * sizeof *arr); + if (!arr) { + LM_ERR("no more memory for HTTP servers\n"); + return NULL; + } + httpd_servers = arr; + arr = &httpd_servers[httpd_n_servers]; + memset(arr, 0, sizeof *arr); + arr->name.s = malloc(name->len + 1); + if (!arr->name.s) { + LM_ERR("no more memory for HTTP server name\n"); + return NULL; + } + memcpy(arr->name.s, name->s, name->len); + arr->name.s[name->len] = '\0'; + arr->name.len = name->len; + arr->listen_fd = -1; + httpd_n_servers++; + return arr; +} + +static struct httpd_server *httpd_srv_str(void *val, str *rest) +{ + str name; + + httpd_split((char *)val, &name, rest); + return httpd_get_server(&name); +} + +static int httpd_srv_int(modparam_t type, void *val, struct httpd_server **s, + int *out) +{ + str name, rest; + + if (type & STR_PARAM) { + httpd_split((char *)val, &name, &rest); + if (str2sint(&rest, out) < 0) { + LM_ERR("invalid integer modparam value '%.*s'\n", + rest.len, rest.s); + return -1; + } + } else { + name.s = HTTPD_DEFAULT_SRV; + name.len = sizeof(HTTPD_DEFAULT_SRV) - 1; + *out = (int)(long)val; + } + *s = httpd_get_server(&name); + return *s ? 0 : -1; +} + +static int set_ip(modparam_t t, void *v) +{ struct httpd_server *s; str r; s = httpd_srv_str(v, &r); if (!s) return -1; s->ip = r; return 0; } +static int set_tls_cert(modparam_t t, void *v) +{ struct httpd_server *s; str r; s = httpd_srv_str(v, &r); if (!s) return -1; s->tls_cert_file = r; return 0; } +static int set_tls_key(modparam_t t, void *v) +{ struct httpd_server *s; str r; s = httpd_srv_str(v, &r); if (!s) return -1; s->tls_key_file = r; return 0; } +static int set_tls_ciphers(modparam_t t, void *v) +{ struct httpd_server *s; str r; s = httpd_srv_str(v, &r); if (!s) return -1; s->tls_ciphers = r; return 0; } +static int set_port(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->port = n; return 0; } +static int set_buf_size(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->buf_size = n; return 0; } +static int set_conn_timeout(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->conn_timeout = n; return 0; } +static int set_post_buf_size(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->post_buf_size = n; return 0; } +static int set_receive_buf_size(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->receive_buf_size = n; return 0; } +static int set_workers(modparam_t t, void *v) +{ struct httpd_server *s; int n; if (httpd_srv_int(t, v, &s, &n) < 0) return -1; s->workers = n; return 0; } + /** Module parameters */ static const param_export_t params[] = { - {"port", INT_PARAM, &port}, - {"ip", STR_PARAM, &ip.s}, - {"buf_size", INT_PARAM, &buffer.len}, - {"conn_timeout", INT_PARAM, &hd_conn_timeout_s}, - {"post_buf_size", INT_PARAM, &post_buf_size}, - {"receive_buf_size", INT_PARAM, &receive_buf_size}, - {"tls_cert_file", STR_PARAM, &tls_cert_file.s}, - {"tls_key_file", STR_PARAM, &tls_key_file.s}, - {"tls_ciphers", STR_PARAM, &tls_ciphers.s}, + {"ip", STR_PARAM|USE_FUNC_PARAM, (void*)set_ip}, + {"port", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_port}, + {"buf_size", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_buf_size}, + {"conn_timeout", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_conn_timeout}, + {"post_buf_size", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_post_buf_size}, + {"receive_buf_size", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_receive_buf_size}, + {"tls_cert_file", STR_PARAM|USE_FUNC_PARAM, (void*)set_tls_cert}, + {"tls_key_file", STR_PARAM|USE_FUNC_PARAM, (void*)set_tls_key}, + {"tls_ciphers", STR_PARAM|USE_FUNC_PARAM, (void*)set_tls_ciphers}, + {"workers", STR_PARAM|INT_PARAM|USE_FUNC_PARAM, (void*)set_workers}, {"auth_realm", STR_PARAM, &auth_realm.s}, {"auth_username", STR_PARAM, &auth_username.s}, {"auth_password", STR_PARAM, &auth_password.s}, @@ -165,6 +293,8 @@ static long httpd_get_runtime_version(void) static int mod_init(void) { struct ip_addr *_ip; + struct httpd_server *s; + int i, total_workers = 0; #if defined MHD_VERSION && MHD_VERSION >= 0x00093500 /* Get whether epoll() is supported. If supported then @@ -178,12 +308,63 @@ static int mod_init(void) "running %s\n",MHD_get_version()); return -1; } - if (ip.s) { - ip.len = strlen(ip.s); - if ( strcmp(ip.s, "*") && !(_ip=str2ip(&ip)) && !(_ip=str2ip6(&ip))) { - LM_ERR("invalid IP [%.*s]\n", ip.len, ip.s); + + if (httpd_n_servers == 0) { + str def = {HTTPD_DEFAULT_SRV, sizeof(HTTPD_DEFAULT_SRV) - 1}; + if (!httpd_get_server(&def)) return -1; + } + + for (i = 0; i < httpd_n_servers; i++) { + s = &httpd_servers[i]; + + if (s->port == 0) { + if (s->name.len == sizeof(HTTPD_DEFAULT_SRV) - 1 && + memcmp(s->name.s, HTTPD_DEFAULT_SRV, s->name.len) == 0) + s->port = 8888; + else { + LM_ERR("missing 'port' for HTTP server '%.*s'\n", + s->name.len, s->name.s); + return -1; + } + } + + if (s->ip.s) { + s->ip.len = strlen(s->ip.s); + if (strcmp(s->ip.s, "*") && !(_ip=str2ip(&s->ip)) + && !(_ip=str2ip6(&s->ip))) { + LM_ERR("invalid IP [%.*s] for HTTP server '%.*s'\n", + s->ip.len, s->ip.s, s->name.len, s->name.s); + return -1; + } } + + if (s->workers < 1) + s->workers = 1; + if (s->post_buf_size == 0) + s->post_buf_size = DEFAULT_POST_BUF_SIZE; + if (s->post_buf_size < MIN_POST_BUF_SIZE) { + LM_ERR("post_buf_size should be bigger than %d (HTTP server '%.*s')\n", + MIN_POST_BUF_SIZE, s->name.len, s->name.s); + return -1; + } + if (s->receive_buf_size == 0) + s->receive_buf_size = DEFAULT_POST_BUF_SIZE; + if (s->conn_timeout == 0) + s->conn_timeout = DEFAULT_CONN_TIMEOUT; + if (!s->tls_ciphers.s) + s->tls_ciphers.s = DEFAULT_TLS_CIPHERS; + if ((s->tls_cert_file.s && !s->tls_key_file.s) || + (!s->tls_cert_file.s && s->tls_key_file.s)) { + LM_ERR("both tls_cert_file and tls_key_file are required for a " + "TLS HTTP server ('%.*s')\n", s->name.len, s->name.s); + return -1; + } + + total_workers += s->workers; + LM_INFO("HTTP server '%.*s' on %s:%d with %d worker(s)%s\n", + s->name.len, s->name.s, s->ip.s ? s->ip.s : "*", s->port, + s->workers, s->tls_cert_file.s ? " (TLS)" : ""); } if (auth_realm.s) @@ -203,20 +384,7 @@ static int mod_init(void) return -1; } - if (post_buf_size < MIN_POST_BUF_SIZE) { - LM_ERR("post_buf_size should be bigger then %d\n", - MIN_POST_BUF_SIZE); - return -1; - } - if (buffer.len == 0) - buffer.len = (pkg_mem_size/4); - LM_DBG("buf_size=[%d]\n", buffer.len); - - httpd_receive_buff = pkg_malloc(receive_buf_size); - if (httpd_receive_buff == NULL) { - LM_ERR("No more pkg\n"); - return -1; - } + mi_procs[0].no = total_workers; return 0; } diff --git a/modules/httpd/httpd_proc.c b/modules/httpd/httpd_proc.c index 72e42fac140..1925dc12c36 100644 --- a/modules/httpd/httpd_proc.c +++ b/modules/httpd/httpd_proc.c @@ -41,6 +41,7 @@ #include #endif +#include "../../globals.h" #include "../../pt.h" #include "../../sr_module.h" #include "../../str.h" @@ -71,6 +72,119 @@ extern int receive_buf_size; extern char *httpd_receive_buff; extern int httpd_receive_buff_pos; +int httpd_listen_fd = -1; + +static int httpd_build_sockaddr(str *sip, int sport, struct sockaddr_storage *ss, + int *family, int *dual_stack, char **ip_repr, char *reprbuf) +{ + struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)ss; + struct sockaddr_in *s4 = (struct sockaddr_in *)ss; + + memset(ss, 0, sizeof *ss); + *dual_stack = 0; + + if (sip->s && strcmp(sip->s, "*")) { + if (q_memchr(sip->s, ':', sip->len)) { + if (inet_pton(AF_INET6, sip->s, &s6->sin6_addr) <= 0) { + LM_ERR("failed to parse 'ip' modparam: %s\n", sip->s); + return -1; + } + s6->sin6_family = AF_INET6; + s6->sin6_port = htons(sport); + *family = AF_INET6; + sprintf(reprbuf, "[%s]", !strcmp(sip->s, "::0") ? "::" : sip->s); + *ip_repr = reprbuf; + return sizeof *s6; + } + + if (inet_pton(AF_INET, sip->s, &s4->sin_addr) <= 0) { + LM_ERR("failed to parse 'ip' modparam: %s\n", sip->s); + return -1; + } + s4->sin_family = AF_INET; + s4->sin_port = htons(sport); + *family = AF_INET; + *ip_repr = sip->s; + return sizeof *s4; + } + + s6->sin6_addr = in6addr_any; + s6->sin6_family = AF_INET6; + s6->sin6_port = htons(sport); + *family = AF_INET6; + *dual_stack = 1; + *ip_repr = "*"; + return sizeof *s6; +} + +static int httpd_open_listen_socket(struct httpd_server *s) +{ + struct sockaddr_storage ss; + int family, dual, salen, fd, on = 1, off = 0; + char *ip_repr, reprbuf[1 + IP_ADDR_MAX_STR_SIZE + 1]; + + salen = httpd_build_sockaddr(&s->ip, s->port, &ss, &family, &dual, + &ip_repr, reprbuf); + if (salen < 0) + return -1; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + LM_ERR("failed to create HTTPD socket: %s\n", strerror(errno)); + return -1; + } + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) + LM_WARN("failed to set SO_REUSEADDR: %s\n", strerror(errno)); + if (dual) + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof off); + + if (bind(fd, (struct sockaddr *)&ss, salen) < 0) { + LM_ERR("failed to bind HTTPD socket on %s:%d: %s\n", + ip_repr, s->port, strerror(errno)); + close(fd); + return -1; + } + if (listen(fd, 1024) < 0) { + LM_ERR("failed to listen on HTTPD socket: %s\n", strerror(errno)); + close(fd); + return -1; + } + + return fd; +} + +int httpd_pre_fork(void) +{ + int i; + struct httpd_server *s; + + for (i = 0; i < httpd_n_servers; i++) { + s = &httpd_servers[i]; + s->listen_fd = -1; + if (s->workers <= 1) + continue; + s->listen_fd = httpd_open_listen_socket(s); + if (s->listen_fd < 0) + return -1; + } + return 0; +} + +int httpd_post_fork(void) +{ + int i; + struct httpd_server *s; + + for (i = 0; i < httpd_n_servers; i++) { + s = &httpd_servers[i]; + if (s->listen_fd >= 0) { + close(s->listen_fd); + s->listen_fd = -1; + } + } + return 0; +} + static const str MI_HTTP_U_URL = str_init("" "Unable to parse URL!"); static const str MI_HTTP_U_METHOD = str_init("" @@ -816,6 +930,40 @@ int httpd_callback(int fd, void *dmn, int was_timeout) void httpd_proc(int rank) { struct httpd_cb *cb = httpd_cb_list; + struct httpd_server *s = NULL, *o; + int i, acc = 0; + + for (i = 0; i < httpd_n_servers; i++) { + o = &httpd_servers[i]; + if (rank < acc + o->workers) { + s = o; + break; + } + acc += o->workers; + } + if (!s) { + LM_ERR("cannot map HTTPD worker rank %d to a server\n", rank); + return; + } + + ip = s->ip; + port = s->port; + buffer.len = s->buf_size; + hd_conn_timeout_s = s->conn_timeout; + post_buf_size = s->post_buf_size; + receive_buf_size = s->receive_buf_size; + tls_cert_file = s->tls_cert_file; + tls_key_file = s->tls_key_file; + tls_ciphers = s->tls_ciphers; + httpd_listen_fd = s->listen_fd; + + for (i = 0; i < httpd_n_servers; i++) { + o = &httpd_servers[i]; + if (o != s && o->listen_fd >= 0) { + close(o->listen_fd); + o->listen_fd = -1; + } + } /*child's initial settings*/ if (init_mi_child()!=0) { @@ -823,6 +971,9 @@ void httpd_proc(int rank) return; } + if (buffer.len == 0) + buffer.len = (pkg_mem_size/4); + /* Allocating http response buffer */ buffer.s = (char*)malloc(sizeof(char)*buffer.len); if (buffer.s==NULL) { @@ -830,6 +981,12 @@ void httpd_proc(int rank) return; } + httpd_receive_buff = pkg_malloc(receive_buf_size); + if (httpd_receive_buff == NULL) { + LM_ERR("oom for receive buffer\n"); + return; + } + while(cb) { if (cb->init_proc_callback) cb->init_proc_callback(); @@ -843,7 +1000,7 @@ void httpd_proc(int rank) char *key_pem, *cert_pem, *ip_repr, ip6buf[1+IP_ADDR_MAX_STR_SIZE+1]; void *saddr; struct sockaddr_in saddr4; - struct MHD_OptionItem mhd_opts[4]; + struct MHD_OptionItem mhd_opts[5]; const union MHD_DaemonInfo *dmni; int fd; @@ -880,10 +1037,6 @@ void httpd_proc(int rank) } else mhd_flags = mhd_flags | MHD_NO_FLAG; - mhd_opts[mhd_opt_n].option = MHD_OPTION_END; - mhd_opts[mhd_opt_n].value = 0; - mhd_opts[mhd_opt_n].ptr_value = NULL; - #if (MHD_VERSION <= 0x00095000) mhd_flags = mhd_flags | MHD_USE_EPOLL_LINUX_ONLY; #else @@ -952,12 +1105,28 @@ void httpd_proc(int rank) httpd_server_info.sin.sin_port = port; #endif + if (httpd_listen_fd >= 0) { + mhd_flags &= ~(MHD_USE_IPv6 | MHD_USE_DUAL_STACK); + mhd_opts[mhd_opt_n].option = MHD_OPTION_LISTEN_SOCKET; + mhd_opts[mhd_opt_n].value = httpd_listen_fd; + mhd_opts[mhd_opt_n].ptr_value = NULL; + mhd_opt_n++; + } else { + mhd_opts[mhd_opt_n].option = MHD_OPTION_SOCK_ADDR; + mhd_opts[mhd_opt_n].value = 0; + mhd_opts[mhd_opt_n].ptr_value = saddr; + mhd_opt_n++; + } + + mhd_opts[mhd_opt_n].option = MHD_OPTION_END; + mhd_opts[mhd_opt_n].value = 0; + mhd_opts[mhd_opt_n].ptr_value = NULL; + LM_DBG("init_child [%d] - [%d] HTTP Server init [%s:%d]\n", rank, getpid(), ip_repr, port); set_proc_attrs("HTTPD %s:%d", ip_repr, port); dmn = MHD_start_daemon(mhd_flags, port, NULL, NULL, &(answer_to_connection), NULL, - MHD_OPTION_SOCK_ADDR, saddr, MHD_OPTION_ARRAY, mhd_opts, MHD_OPTION_END); diff --git a/modules/httpd/httpd_proc.h b/modules/httpd/httpd_proc.h index 7ed68a8e778..41fb3b419ed 100644 --- a/modules/httpd/httpd_proc.h +++ b/modules/httpd/httpd_proc.h @@ -26,6 +26,8 @@ #ifndef _MI_HTTP_HTTPD_PROC_H #define _MI_HTTP_HTTPD_PROC_H +#include "../../str.h" + #ifdef LIBMICROHTTPD #include extern struct MHD_Daemon *dmn; @@ -37,8 +39,28 @@ extern struct MHD_Daemon *dmn; #endif #endif +struct httpd_server { + str name; + str ip; + int port; + int buf_size; + int conn_timeout; + int post_buf_size; + int receive_buf_size; + str tls_cert_file; + str tls_key_file; + str tls_ciphers; + int workers; + int listen_fd; +}; + +extern struct httpd_server *httpd_servers; +extern int httpd_n_servers; + void httpd_proc(int rank); void httpd_proc_destroy(void); +int httpd_pre_fork(void); +int httpd_post_fork(void); #endif diff --git a/modules/mid_registrar/README.md b/modules/mid_registrar/README.md index 8c48b0fee29..195a82cbd8d 100644 --- a/modules/mid_registrar/README.md +++ b/modules/mid_registrar/README.md @@ -737,7 +737,7 @@ improving interoperability. ```opensips title="Setting the allow_dup_cseq parameter" ... # strict RFC 3261 compliance: reject REGISTER requests with duplicate CSeq -modparam(" +modparam("mid_registrar", "allow_dup_cseq", true) ``` @@ -763,7 +763,7 @@ than, e.g., every 1800 seconds following the restart. ```opensips title="Setting the expires_max_deviation parameter" ... # add a random +/- 0-100 seconds to each registration lifetime -modparam(" +modparam("mid_registrar", "expires_max_deviation", 100) ``` @@ -784,7 +784,7 @@ That's it - the function parameter overrides this global parameter. ```opensips title="Set max_contacts parameter" ... # Allow no more than 10 contacts per AOR -modparam(" +modparam("mid_registrar", "max_contacts", 10) ``` @@ -798,7 +798,7 @@ Default value is **64**. ```opensips title="Setting the *max_username_len* module parameter" -modparam(" +modparam("mid_registrar", "max_username_len", 128) ``` @@ -812,7 +812,7 @@ Default value is **64**. ```opensips title="Setting the *max_domain_len* module parameter" -modparam(" +modparam("mid_registrar", "max_domain_len", 128) ``` @@ -826,7 +826,7 @@ Default value is **256**. ```opensips title="Setting the *max_aor_len* module parameter" -modparam(" +modparam("mid_registrar", "max_aor_len", 512) ``` @@ -840,7 +840,7 @@ Default value is **255**. ```opensips title="Setting the *max_contact_len* module parameter" -modparam(" +modparam("mid_registrar", "max_contact_len", 512) ``` diff --git a/modules/registrar/README.md b/modules/registrar/README.md index 87e5ea81037..80cbc0747f5 100644 --- a/modules/registrar/README.md +++ b/modules/registrar/README.md @@ -372,7 +372,7 @@ improving interoperability. ```opensips title="Setting the allow_dup_cseq parameter" ... # strict RFC 3261 compliance: reject REGISTER requests with duplicate CSeq -modparam(" +modparam("registrar", "allow_dup_cseq", true) ``` @@ -398,7 +398,7 @@ than, e.g., every 1800 seconds following the restart. ```opensips title="Setting the expires_max_deviation parameter" ... # add a random +/- 0-100 seconds to each registration lifetime -modparam(" +modparam("registrar", "expires_max_deviation", 100) ``` @@ -419,7 +419,7 @@ That's it - the function parameter overrides this global parameter. ```opensips title="Set max_contacts parameter" ... # Allow no more than 10 contacts per AOR -modparam(" +modparam("registrar", "max_contacts", 10) ``` @@ -433,7 +433,7 @@ Default value is **64**. ```opensips title="Setting the *max_username_len* module parameter" -modparam(" +modparam("registrar", "max_username_len", 128) ``` @@ -447,7 +447,7 @@ Default value is **64**. ```opensips title="Setting the *max_domain_len* module parameter" -modparam(" +modparam("registrar", "max_domain_len", 128) ``` @@ -461,7 +461,7 @@ Default value is **256**. ```opensips title="Setting the *max_aor_len* module parameter" -modparam(" +modparam("registrar", "max_aor_len", 512) ``` @@ -475,7 +475,7 @@ Default value is **255**. ```opensips title="Setting the *max_contact_len* module parameter" -modparam(" +modparam("registrar", "max_contact_len", 512) ``` diff --git a/modules/sip_overload/Makefile b/modules/sip_overload/Makefile new file mode 100644 index 00000000000..b7dad3ff1a3 --- /dev/null +++ b/modules/sip_overload/Makefile @@ -0,0 +1,9 @@ +# sip_overload module makefile +# +# WARNING: do not run this directly, it should be run by the master Makefile + +include ../../Makefile.defs +auto_gen= +NAME=sip_overload.so + +include ../../Makefile.modules diff --git a/modules/sip_overload/README.md b/modules/sip_overload/README.md new file mode 100644 index 00000000000..d8ef3a48cad --- /dev/null +++ b/modules/sip_overload/README.md @@ -0,0 +1,184 @@ +--- +title: "sip_overload Module" +description: "RFC 7339 loss-based and RFC 7415 rate-based SIP overload control." +--- + +## Overview + +The `sip_overload` module implements hop-by-hop SIP overload control using the Via parameters defined by RFC 7339 and RFC 7415. + +When loaded, OpenSIPS advertises overload-control support on every locally generated Via: + +``` +;oc;oc-algo="loss,rate" +``` + +The module can consume overload feedback from downstream responses, keep the feedback in shared memory for all workers, enforce the validity interval and sequence ordering, and make admission decisions for both standardized algorithms: + +- `loss`: probabilistic request reduction by the advertised percentage +- `rate`: per-peer maximum request rate, enforced with the RFC 7415 default + leaky bucket (`T = 1/oc`, burst tolerance `TAU = 4*T`) + +It can also act as an overloaded server. Loading the module makes server-side +RFC 7339 support visible immediately: a capable upstream client receives an +idle `oc=0;oc-validity=0` response even when no overload policy has been +activated. `oc_set_local()` configures active feedback, which is automatically +written only into the top Via of locally generated responses (e.g. +`sl_send_reply()`, `t_reply()` or the automatic `100 Trying`) and only when +the upstream client advertised `oc`. The client's `oc` and `oc-algo` +parameters are replaced by the server values, as required by RFC 7339. +Responses relayed from downstream are not modified, and the upstream +client's `oc` parameters are not stripped from requests forwarded +downstream (RFC 7339 recommends it). + +The server never selects an algorithm the client did not advertise. Because +`loss` is mandatory in RFC 7339, a local `rate` policy presented to a +loss-only neighbor falls back to an idle loss response instead of sending an +incompatible rate value. + +The overload scope is an explicit peer key. A deployment should use a stable key representing the downstream IP/port combination, as required by RFC 7339. + +## Parameters + +### advertise + +Enable automatic Via capability advertisement. Default: `1`. + +### advertise_algorithms + +Comma-separated algorithms advertised in `oc-algo`. Default: + +``` +loss,rate +``` + +The order expresses preference. RFC 7339 requires support for `loss`; RFC 7415 defines `rate`. + +Configure this value as an unquoted comma-separated list. Each token must +contain ASCII letters or digits only, matching the RFC 7339 `oc-algo` grammar. +Whitespace, empty tokens, quotes, semicolons and control characters are rejected +at startup rather than copied into a Via header. + +When `advertise=1`, startup fails if `loss` is omitted, because an RFC 7339 +client must support and advertise the loss-based algorithm. + +### max_peers + +Maximum number of downstream peer feedback records kept in shared memory. +Default: `4096`. + +Expired or explicitly cleared (`oc-validity=0`) peer records are garbage +collected before the module refuses a new peer. This bounds shared-memory use +when peer keys are generated dynamically or a deployment talks to many +destinations. + +## Functions + +### oc_update() + +Use in an `onreply_route`. Parses `oc`, `oc-algo`, `oc-validity` and `oc-seq` from the top Via and stores the state under the response source IP/port. + +The peer key is formatted as `:` (IPv6 addresses are not +enclosed in brackets), e.g. `192.0.2.10:5060`; `oc_check()` must be given +the very same key. + +If `oc-validity` is absent, the RFC 7339 default of 500 ms is used. + +### oc_update_peer(peer) + +Same as `oc_update()`, but stores the feedback under an explicit peer key. This is recommended when routing logic already has a canonical destination identifier. + +Example: + +```opensips +onreply_route[OC_FEEDBACK] { + oc_update_peer("$avp(dst_key)"); +} +``` + +### oc_check(peer) + +Returns `1` when a request may be sent and `-1` when the current overload state says it should be throttled. + +For `loss`, the `oc` value is interpreted as the percentage of traffic to reduce. For `rate`, it is interpreted as the maximum requests per second, allowing bursts of up to five back-to-back requests (RFC 7415 default +algorithm, `TAU = 4*T`). + +Example: + +```opensips +if (oc_check("$avp(dst_key)") < 0) { + send_reply(503, "Downstream overload"); + exit; +} +t_on_reply("OC_FEEDBACK"); +route(relay); +``` + +The script remains responsible for SIP request prioritization. This allows deployments to protect ACK/BYE/emergency or other locally important traffic according to policy. + +### oc_set_local(algorithm, value, validity_ms) + +Enable server-side overload feedback. Supported algorithms are `loss` and `rate`. + +Examples: + +```opensips +# Ask each supporting upstream peer to reduce offered traffic by 35%. +oc_set_local("loss", 35, 1000); + +# Or cap a supporting upstream peer at 500 requests/second. +oc_set_local("rate", 500, 1000); +``` + +The module maintains an increasing `oc-seq` value and automatically writes the feedback into locally generated SIP responses. + +### oc_clear_local() + +Signals the end of overload control by sending `oc-validity=0` with a new sequence number. + +## Events + +### E_SIP_OVERLOAD_UPDATE + +Raised when a newer valid feedback record is accepted. + +Parameters: + +- `peer` +- `algorithm` +- `value` +- `validity_ms` + +### E_SIP_OVERLOAD_THROTTLE + +Raised when `oc_check()` rejects a request due to active feedback. + +## Statistics + +- `sip_overload:feedback_updates` +- `sip_overload:feedback_stale` +- `sip_overload:feedback_malformed` +- `sip_overload:requests_allowed` +- `sip_overload:requests_throttled` +- `sip_overload:feedback_expired` +- `sip_overload:peer_states` +- `sip_overload:peer_limit_drops` +- `sip_overload:server_updates` + +These can be exported by the existing Prometheus module. + +## Standards behavior + +- Clients always advertise at least `loss`. +- Servers acknowledge overload-control support even while idle with `oc=0` + and `oc-validity=0`. +- A response algorithm is selected only from algorithms advertised by that + upstream client. +- `oc-validity` defaults to 500 ms when omitted. +- `oc-validity=0` immediately ends overload control. +- Older or duplicate `oc-seq` feedback does not restart the validity period. +- Loss values are constrained to 0..100. +- Under rate control, `oc=0` with non-zero validity blocks all controlled requests. +- Overload information is scoped per downstream peer rather than globally. +- Per-peer feedback state is bounded by `max_peers`; expired/cleared state is + reclaimed before new feedback is rejected. diff --git a/modules/sip_overload/sip_overload.c b/modules/sip_overload/sip_overload.c new file mode 100644 index 00000000000..4a80270e93d --- /dev/null +++ b/modules/sip_overload/sip_overload.c @@ -0,0 +1,971 @@ +/* + * RFC 7339 / RFC 7415 SIP overload control + * + * Copyright (C) 2026 OpenSIPS Project + * + * 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 +#include +#include + +#include "../../sr_module.h" +#include "../../dprint.h" +#include "../../locking.h" +#include "../../mem/shm_mem.h" +#include "../../parser/msg_parser.h" +#include "../../parser/parse_via.h" +#include "../../timer.h" +#include "../../ip_addr.h" +#include "../../statistics.h" +#include "../../msg_translator.h" +#include "../../evi/evi_modules.h" +#include "../../evi/evi_params.h" + +#define OC_DEFAULT_VALIDITY_MS 500 +#define OC_KEY_MAX 128 +#define OC_SEQ_MAX 32 +#define OC_ADV_MAX 160 +#define OC_REPLY_MAX 192 +#define OC_RATE_TAU_STEPS 4 + +enum oc_algorithm { + OC_ALGO_LOSS = 0, + OC_ALGO_RATE = 1 +}; + +struct oc_seq { + unsigned long long major; + unsigned int minor; +}; + +struct oc_peer { + unsigned short key_len; + char key[OC_KEY_MAX]; + enum oc_algorithm algo; + unsigned int value; + unsigned int validity_ms; + struct oc_seq seq; + utime_t expires_us; + utime_t next_allowed_us; + struct oc_peer *next; +}; + +struct oc_local_feedback { + int enabled; + enum oc_algorithm algo; + unsigned int value; + unsigned int validity_ms; + struct oc_seq seq; +}; + +static char *advertise_algorithms = "loss,rate"; +static int advertise = 1; +static int max_peers = 4096; + +static gen_lock_t *oc_lock; +static struct oc_peer **oc_peers; +static unsigned int *oc_peer_count; +static struct oc_local_feedback *local_feedback; + +static stat_var *st_feedback_updates; +static stat_var *st_feedback_stale; +static stat_var *st_feedback_malformed; +static stat_var *st_requests_allowed; +static stat_var *st_requests_throttled; +static stat_var *st_feedback_expired; +static stat_var *st_peer_states; +static stat_var *st_peer_limit_drops; +static stat_var *st_server_updates; + +static event_id_t ev_update = EVI_ERROR; +static event_id_t ev_throttle = EVI_ERROR; +static str ev_update_name = str_init("E_SIP_OVERLOAD_UPDATE"); +static str ev_throttle_name = str_init("E_SIP_OVERLOAD_THROTTLE"); +static str p_peer = str_init("peer"); +static str p_algo = str_init("algorithm"); +static str p_value = str_init("value"); +static str p_validity = str_init("validity_ms"); + +static char oc_advertise_buf[OC_ADV_MAX]; + +static int mod_init(void); +static void mod_destroy(void); +static int oc_via_params_provider(struct sip_msg *msg, int context, str *out); +static int w_oc_update(struct sip_msg *msg); +static int w_oc_update_peer(struct sip_msg *msg, str *peer); +static int w_oc_check(struct sip_msg *msg, str *peer); +static int w_oc_set_local(struct sip_msg *msg, str *algo, int *value, int *validity); +static int w_oc_clear_local(struct sip_msg *msg); + +static const cmd_export_t cmds[] = { + {"oc_update", (cmd_function)w_oc_update, {{0,0,0}}, ONREPLY_ROUTE}, + {"oc_update_peer", (cmd_function)w_oc_update_peer, + {{CMD_PARAM_STR,0,0},{0,0,0}}, ONREPLY_ROUTE}, + {"oc_check", (cmd_function)w_oc_check, + {{CMD_PARAM_STR,0,0},{0,0,0}}, REQUEST_ROUTE|BRANCH_ROUTE|FAILURE_ROUTE|LOCAL_ROUTE}, + {"oc_set_local", (cmd_function)w_oc_set_local, { + {CMD_PARAM_STR,0,0}, + {CMD_PARAM_INT,0,0}, + {CMD_PARAM_INT,0,0}, + {0,0,0}}, ALL_ROUTES}, + {"oc_clear_local", (cmd_function)w_oc_clear_local, {{0,0,0}}, ALL_ROUTES}, + {0,0,{{0,0,0}},0} +}; + +static const param_export_t params[] = { + {"advertise_algorithms", STR_PARAM, &advertise_algorithms}, + {"advertise", INT_PARAM, &advertise}, + {"max_peers", INT_PARAM, &max_peers}, + {0,0,0} +}; + +static const stat_export_t stats[] = { + {"feedback_updates", STAT_NO_RESET, &st_feedback_updates}, + {"feedback_stale", STAT_NO_RESET, &st_feedback_stale}, + {"feedback_malformed", STAT_NO_RESET, &st_feedback_malformed}, + {"requests_allowed", STAT_NO_RESET, &st_requests_allowed}, + {"requests_throttled", STAT_NO_RESET, &st_requests_throttled}, + {"feedback_expired", STAT_NO_RESET, &st_feedback_expired}, + {"peer_states", STAT_NO_RESET, &st_peer_states}, + {"peer_limit_drops", STAT_NO_RESET, &st_peer_limit_drops}, + {"server_updates", STAT_NO_RESET, &st_server_updates}, + {0,0,0} +}; + +struct module_exports exports = { + "sip_overload", + MOD_TYPE_DEFAULT, + MODULE_VERSION, + DEFAULT_DLFLAGS, + 0, + 0, + cmds, + 0, + params, + stats, + 0, + 0, + 0, + 0, + 0, + mod_init, + 0, + mod_destroy, + 0, + 0 +}; + +/* + * Microsecond monotonic clock. The core get_uticks() only advances in + * UTIMER_TICK (100 ms) steps, which is far too coarse for rate control. + */ +static utime_t oc_now_us(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) + return get_uticks(); + return (utime_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000; +} + +static int str_eq_ci(const str *s, const char *lit) +{ + int i, l; + const char *p; + if (!s || !s->s || !lit) + return 0; + p = s->s; + l = s->len; + if (l >= 2 && p[0] == '"' && p[l-1] == '"') { + p++; + l -= 2; + } + if ((int)strlen(lit) != l) + return 0; + for (i = 0; i < l; i++) { + char a = p[i], b = lit[i]; + if (a >= 'A' && a <= 'Z') a += 'a' - 'A'; + if (b >= 'A' && b <= 'Z') b += 'a' - 'A'; + if (a != b) + return 0; + } + return 1; +} + +static int parse_uint(const str *s, unsigned int *out) +{ + unsigned long v = 0; + int i; + if (!s || !s->s || s->len <= 0) + return -1; + for (i = 0; i < s->len; i++) { + if (s->s[i] < '0' || s->s[i] > '9') + return -1; + v = v * 10 + (unsigned long)(s->s[i] - '0'); + if (v > 0xffffffffUL) + return -1; + } + *out = (unsigned int)v; + return 0; +} + +static int parse_seq(const str *s, struct oc_seq *seq) +{ + unsigned long long major = 0; + unsigned int minor = 0; + int i, dot = -1, minor_digits = 0; + if (!s || !s->s || s->len < 3) + return -1; + for (i = 0; i < s->len; i++) { + if (s->s[i] == '.') { + if (dot >= 0 || i == 0 || i == s->len - 1) + return -1; + dot = i; + continue; + } + if (s->s[i] < '0' || s->s[i] > '9') + return -1; + if (dot < 0) { + major = major * 10ULL + (unsigned long long)(s->s[i] - '0'); + } else { + if (++minor_digits > 5) + return -1; + minor = minor * 10U + (unsigned int)(s->s[i] - '0'); + } + } + if (dot < 0 || dot > 12 || minor_digits <= 0) + return -1; + seq->major = major; + seq->minor = minor; + return 0; +} + +static int seq_cmp(const struct oc_seq *a, const struct oc_seq *b) +{ + if (a->major < b->major) return -1; + if (a->major > b->major) return 1; + if (a->minor < b->minor) return -1; + if (a->minor > b->minor) return 1; + return 0; +} + +static struct via_param *find_via_param(struct via_body *via, const char *name) +{ + struct via_param *p; + int nlen = strlen(name); + if (!via) + return NULL; + for (p = via->param_lst; p; p = p->next) + if (p->name.len == nlen && strncasecmp(p->name.s, name, nlen) == 0) + return p; + return NULL; +} + +static struct oc_peer *find_peer_locked(const str *key) +{ + struct oc_peer *p; + if (!key || !key->s || key->len <= 0 || key->len >= OC_KEY_MAX) + return NULL; + for (p = *oc_peers; p; p = p->next) + if (p->key_len == key->len && memcmp(p->key, key->s, key->len) == 0) + return p; + return NULL; +} + +static void purge_expired_peers_locked(utime_t now) +{ + struct oc_peer *p, *prev = NULL, *next; + + if (!oc_peers || !oc_peer_count) + return; + + for (p = *oc_peers; p; p = next) { + next = p->next; + if (!p->validity_ms || now >= p->expires_us) { + if (prev) + prev->next = next; + else + *oc_peers = next; + shm_free(p); + if (*oc_peer_count) + (*oc_peer_count)--; + update_stat(st_peer_states, -1); + continue; + } + prev = p; + } +} + +static struct oc_peer *get_peer_locked(const str *key, utime_t now) +{ + struct oc_peer *p = find_peer_locked(key); + + if (p) + return p; + + if (*oc_peer_count >= (unsigned int)max_peers) + purge_expired_peers_locked(now); + if (*oc_peer_count >= (unsigned int)max_peers) + return NULL; + + p = shm_malloc(sizeof(*p)); + if (!p) + return NULL; + memset(p, 0, sizeof(*p)); + p->key_len = key->len; + memcpy(p->key, key->s, key->len); + p->next = *oc_peers; + *oc_peers = p; + (*oc_peer_count)++; + update_stat(st_peer_states, 1); + return p; +} + +static void raise_oc_event(event_id_t event, const str *peer, + enum oc_algorithm algo, unsigned int value, unsigned int validity) +{ + evi_params_p ep; + str algo_s = algo == OC_ALGO_RATE ? str_init("rate") : str_init("loss"); + int ivalue = (int)value, ivalidity = (int)validity; + if (event == EVI_ERROR || !evi_probe_event(event)) + return; + ep = evi_get_params(); + if (!ep) + return; + if (evi_param_add_str(ep, &p_peer, peer) < 0 || + evi_param_add_str(ep, &p_algo, &algo_s) < 0 || + evi_param_add_int(ep, &p_value, &ivalue) < 0 || + evi_param_add_int(ep, &p_validity, &ivalidity) < 0) { + evi_free_params(ep); + return; + } + if (evi_raise_event(event, ep) < 0) + LM_ERR("failed to raise overload-control event\n"); +} + +static int source_peer(struct sip_msg *msg, str *peer, char *buf, int size) +{ + const char *ip; + int len; + if (!msg) + return -1; + ip = ip_addr2a(&msg->rcv.src_ip); + if (!ip) + return -1; + len = snprintf(buf, size, "%s:%u", ip, msg->rcv.src_port); + if (len <= 0 || len >= size) + return -1; + peer->s = buf; + peer->len = len; + return 0; +} + +static int update_from_via(struct sip_msg *msg, const str *peer) +{ + struct via_param *p_oc, *p_algo, *p_validity, *p_seq; + struct oc_peer *state; + enum oc_algorithm algo; + struct oc_seq seq; + unsigned int value, validity = OC_DEFAULT_VALIDITY_MS; + utime_t now; + int cmp; + + if (!msg || !peer || peer->len <= 0) + return -1; + if (parse_headers(msg, HDR_VIA_F, 0) < 0 || !msg->via1) + goto malformed; + + p_oc = find_via_param(msg->via1, "oc"); + p_algo = find_via_param(msg->via1, "oc-algo"); + p_validity = find_via_param(msg->via1, "oc-validity"); + p_seq = find_via_param(msg->via1, "oc-seq"); + + /* No feedback means the downstream server did not update our OC state. */ + if (!p_oc) + return 1; + if (!p_oc->value.s || parse_uint(&p_oc->value, &value) < 0 || !p_seq || + parse_seq(&p_seq->value, &seq) < 0) + goto malformed; + + if (p_algo && str_eq_ci(&p_algo->value, "rate")) + algo = OC_ALGO_RATE; + else if (!p_algo || str_eq_ci(&p_algo->value, "loss")) + algo = OC_ALGO_LOSS; + else + goto malformed; + + if (p_validity && parse_uint(&p_validity->value, &validity) < 0) + goto malformed; + if (algo == OC_ALGO_LOSS && value > 100) + goto malformed; + + now = oc_now_us(); + lock_get(oc_lock); + state = get_peer_locked(peer, now); + if (!state) { + int at_limit = oc_peer_count && + *oc_peer_count >= (unsigned int)max_peers; + lock_release(oc_lock); + if (at_limit) + update_stat(st_peer_limit_drops, 1); + return -1; + } + + if (state->seq.major || state->seq.minor) { + cmp = seq_cmp(&seq, &state->seq); + if (cmp <= 0) { + lock_release(oc_lock); + update_stat(st_feedback_stale, 1); + return 1; + } + } + + state->algo = algo; + state->value = value; + state->validity_ms = validity; + state->seq = seq; + state->next_allowed_us = now; + state->expires_us = validity ? now + (utime_t)validity * 1000 : now; + lock_release(oc_lock); + + update_stat(st_feedback_updates, 1); + raise_oc_event(ev_update, peer, algo, value, validity); + return 1; + +malformed: + update_stat(st_feedback_malformed, 1); + return -1; +} + +static int w_oc_update(struct sip_msg *msg) +{ + char buf[OC_KEY_MAX]; + str peer; + if (source_peer(msg, &peer, buf, sizeof(buf)) < 0) + return -1; + return update_from_via(msg, &peer); +} + +static int w_oc_update_peer(struct sip_msg *msg, str *peer) +{ + if (!peer || !peer->s || peer->len <= 0 || peer->len >= OC_KEY_MAX) + return -1; + return update_from_via(msg, peer); +} + +static int w_oc_check(struct sip_msg *msg, str *peer) +{ + struct oc_peer *state; + enum oc_algorithm algo; + unsigned int value, validity; + utime_t now, step; + int allow = 1; + + if (!peer || !peer->s || peer->len <= 0 || peer->len >= OC_KEY_MAX) + return -1; + now = oc_now_us(); + + lock_get(oc_lock); + state = find_peer_locked(peer); + if (!state) { + lock_release(oc_lock); + update_stat(st_requests_allowed, 1); + return 1; + } + + if (!state->validity_ms || now >= state->expires_us) { + if (state->validity_ms && now >= state->expires_us) + update_stat(st_feedback_expired, 1); + state->validity_ms = 0; + lock_release(oc_lock); + update_stat(st_requests_allowed, 1); + return 1; + } + + algo = state->algo; + value = state->value; + validity = state->validity_ms; + + if (algo == OC_ALGO_RATE) { + /* + * RFC 7415 default leaky bucket, T = 1/oc and + * TAU = 4*T; next_allowed_us holds the bucket's theoretical + * arrival time (LCT + X). + */ + if (value == 0) { + allow = 0; + } else { + step = 1000000ULL / value; + if (!step) step = 1; + if (state->next_allowed_us > now && + state->next_allowed_us - now > OC_RATE_TAU_STEPS * step) { + allow = 0; + } else { + if (state->next_allowed_us < now) + state->next_allowed_us = now; + state->next_allowed_us += step; + } + } + } else { + if (value >= 100) + allow = 0; + else if (value > 0 && ((unsigned int)(rand() % 100) + 1) <= value) + allow = 0; + } + lock_release(oc_lock); + + if (allow) { + update_stat(st_requests_allowed, 1); + return 1; + } + + update_stat(st_requests_throttled, 1); + raise_oc_event(ev_throttle, peer, algo, value, validity); + return -1; +} + +static void next_local_seq_locked(struct oc_seq *seq) +{ + unsigned long long sec = (unsigned long long)time(NULL); + if (local_feedback->seq.major < sec) { + seq->major = sec; + seq->minor = 0; + } else { + *seq = local_feedback->seq; + seq->minor++; + if (seq->minor > 99999) { + seq->major++; + seq->minor = 0; + } + } +} + +static int parse_algo_name(const str *algo, enum oc_algorithm *out) +{ + if (str_eq_ci(algo, "loss")) { + *out = OC_ALGO_LOSS; + return 0; + } + if (str_eq_ci(algo, "rate")) { + *out = OC_ALGO_RATE; + return 0; + } + return -1; +} + +static int w_oc_set_local(struct sip_msg *msg, str *algo_s, int *value_p, int *validity_p) +{ + enum oc_algorithm algo; + struct oc_seq seq; + unsigned int value, validity; + + if (!value_p || !validity_p || *value_p < 0 || *validity_p < 0 || + parse_algo_name(algo_s, &algo) < 0) + return -1; + value = (unsigned int)*value_p; + validity = (unsigned int)*validity_p; + if (algo == OC_ALGO_LOSS && value > 100) + return -1; + + lock_get(oc_lock); + next_local_seq_locked(&seq); + local_feedback->enabled = 1; + local_feedback->algo = algo; + local_feedback->value = value; + local_feedback->validity_ms = validity; + local_feedback->seq = seq; + lock_release(oc_lock); + update_stat(st_server_updates, 1); + return 1; +} + +static int w_oc_clear_local(struct sip_msg *msg) +{ + struct oc_seq seq; + lock_get(oc_lock); + next_local_seq_locked(&seq); + local_feedback->enabled = 1; + local_feedback->value = 0; + local_feedback->validity_ms = 0; + local_feedback->seq = seq; + lock_release(oc_lock); + update_stat(st_server_updates, 1); + return 1; +} + +static int advertised_algo_list_valid(const char *value) +{ + const unsigned char *p; + int token_len = 0; + + if (!value || !*value) + return 0; + + for (p = (const unsigned char *)value; ; p++) { + if (*p == ',' || *p == '\0') { + if (!token_len) + return 0; + token_len = 0; + if (*p == '\0') + break; + continue; + } + if (!((*p >= 'A' && *p <= 'Z') || + (*p >= 'a' && *p <= 'z') || + (*p >= '0' && *p <= '9'))) + return 0; + token_len++; + } + return 1; +} + +static int algo_list_has(const str *value, const char *algo) +{ + int start, end, alen; + + if (!value || !value->s || !algo) + return 0; + alen = strlen(algo); + start = 0; + end = value->len; + if (end >= 2 && value->s[0] == '"' && value->s[end - 1] == '"') { + start++; + end--; + } + + while (start < end) { + int tok_start, tok_end, j; + + while (start < end && + (value->s[start] == ' ' || value->s[start] == '\t' || + value->s[start] == ',')) + start++; + tok_start = start; + while (start < end && value->s[start] != ',') + start++; + tok_end = start; + while (tok_end > tok_start && + (value->s[tok_end - 1] == ' ' || value->s[tok_end - 1] == '\t')) + tok_end--; + + if (tok_end - tok_start == alen) { + for (j = 0; j < alen; j++) { + char a = value->s[tok_start + j], b = algo[j]; + if (a >= 'A' && a <= 'Z') a += 'a' - 'A'; + if (b >= 'A' && b <= 'Z') b += 'a' - 'A'; + if (a != b) + break; + } + if (j == alen) + return 1; + } + } + return 0; +} + +static int request_supports_oc(struct sip_msg *msg) +{ + if (!msg) + return 0; + if (parse_headers(msg, HDR_VIA_F, 0) < 0 || !msg->via1) + return 0; + return find_via_param(msg->via1, "oc") != NULL; +} + +static int request_supports_algo(struct sip_msg *msg, enum oc_algorithm algo) +{ + struct via_param *p_algo; + + if (!request_supports_oc(msg)) + return 0; + + p_algo = find_via_param(msg->via1, "oc-algo"); + if (!p_algo) + return algo == OC_ALGO_LOSS; + + return algo_list_has(&p_algo->value, + algo == OC_ALGO_RATE ? "rate" : "loss"); +} + +static int oc_via_params_provider(struct sip_msg *msg, int context, str *out) +{ + static char reply_buf[OC_REPLY_MAX]; + struct oc_local_feedback snap; + enum oc_algorithm selected_algo; + unsigned int selected_value, selected_validity; + const char *algo; + int len; + + out->s = NULL; + out->len = 0; + + if (context == VIA_PARAM_CTX_REQUEST) { + if (!advertise || !oc_advertise_buf[0]) + return 0; + out->s = oc_advertise_buf; + out->len = strlen(oc_advertise_buf); + return 1; + } + + if (context != VIA_PARAM_CTX_REPLY || !msg || !request_supports_oc(msg)) + return 0; + + lock_get(oc_lock); + snap = *local_feedback; + lock_release(oc_lock); + if (!snap.enabled) + return 0; + + selected_algo = snap.algo; + selected_value = snap.value; + selected_validity = snap.validity_ms; + + /* + * A server must choose an algorithm advertised by the client. Every + * RFC 7339 client supports loss. If local policy is rate-based but the + * neighbor did not advertise rate, acknowledge overload-control support + * using the mandatory loss algorithm without imposing an incompatible + * reduction value. + */ + if (!request_supports_algo(msg, selected_algo)) { + selected_algo = OC_ALGO_LOSS; + selected_value = 0; + selected_validity = 0; + } + + algo = selected_algo == OC_ALGO_RATE ? "rate" : "loss"; + len = snprintf(reply_buf, sizeof(reply_buf), + ";oc=%u;oc-algo=\"%s\";oc-validity=%u;oc-seq=%llu.%05u", + selected_value, algo, selected_validity, + snap.seq.major, snap.seq.minor); + if (len <= 0 || len >= (int)sizeof(reply_buf)) + return -1; + out->s = reply_buf; + out->len = len; + return 1; +} + +#ifdef UNIT_TESTS +int sip_overload_test_parse_uint(const char *text, unsigned int *out) +{ + str value = STR_NULL; + + if (!text) + return -1; + value.s = (char *)text; + value.len = strlen(text); + return parse_uint(&value, out); +} + +int sip_overload_test_parse_seq(const char *text, + unsigned long long *major, unsigned int *minor) +{ + str value = STR_NULL; + struct oc_seq seq; + int rc; + + if (!text || !major || !minor) + return -1; + value.s = (char *)text; + value.len = strlen(text); + rc = parse_seq(&value, &seq); + if (rc < 0) + return rc; + *major = seq.major; + *minor = seq.minor; + return 0; +} + +int sip_overload_test_seq_cmp(const char *left, const char *right) +{ + str a_s = STR_NULL, b_s = STR_NULL; + struct oc_seq a, b; + + if (!left || !right) + return 99; + a_s.s = (char *)left; + a_s.len = strlen(left); + b_s.s = (char *)right; + b_s.len = strlen(right); + if (parse_seq(&a_s, &a) < 0 || parse_seq(&b_s, &b) < 0) + return 99; + return seq_cmp(&a, &b); +} + +int sip_overload_test_advertised_algo_list_valid(const char *list) +{ + return advertised_algo_list_valid(list); +} + +int sip_overload_test_algo_list_has(const char *list, const char *algo) +{ + str value = STR_NULL; + + if (!list || !algo) + return 0; + value.s = (char *)list; + value.len = strlen(list); + return algo_list_has(&value, algo); +} +#endif + +static int mod_init(void) +{ + str advertised; + int len; + + if (!advertise_algorithms || !*advertise_algorithms) { + LM_ERR("advertise_algorithms cannot be empty\n"); + return -1; + } + if (!advertised_algo_list_valid(advertise_algorithms)) { + LM_ERR("advertise_algorithms must be a comma-separated list of " + "non-empty ASCII alphanumeric tokens\n"); + return -1; + } + if (max_peers <= 0) { + LM_ERR("max_peers must be greater than zero\n"); + return -1; + } + + + advertised.s = advertise_algorithms; + advertised.len = strlen(advertise_algorithms); + if (advertise && !algo_list_has(&advertised, "loss")) { + LM_ERR("RFC 7339 clients must advertise the mandatory 'loss' algorithm\n"); + return -1; + } + + len = snprintf(oc_advertise_buf, sizeof(oc_advertise_buf), + ";oc;oc-algo=\"%s\"", advertise_algorithms); + if (len <= 0 || len >= (int)sizeof(oc_advertise_buf)) { + LM_ERR("advertise_algorithms is too long\n"); + return -1; + } + + oc_lock = lock_alloc(); + if (!oc_lock) { + LM_ERR("failed to allocate overload-control lock\n"); + return -1; + } + if (!lock_init(oc_lock)) { + LM_ERR("failed to initialize overload-control lock\n"); + lock_dealloc(oc_lock); + oc_lock = NULL; + return -1; + } + + oc_peers = shm_malloc(sizeof(*oc_peers)); + if (!oc_peers) { + LM_ERR("no shared memory for overload-control peer state\n"); + goto error; + } + *oc_peers = NULL; + + oc_peer_count = shm_malloc(sizeof(*oc_peer_count)); + if (!oc_peer_count) { + LM_ERR("no shared memory for overload-control peer count\n"); + goto error; + } + *oc_peer_count = 0; + + local_feedback = shm_malloc(sizeof(*local_feedback)); + if (!local_feedback) { + LM_ERR("no shared memory for overload-control local state\n"); + goto error; + } + memset(local_feedback, 0, sizeof(*local_feedback)); + + /* + * Loading the module means this server supports RFC 7339. Even when + * not overloaded, capable upstream clients receive oc=0 so support can + * be negotiated without inventing overload state. + */ + local_feedback->enabled = 1; + local_feedback->algo = OC_ALGO_LOSS; + local_feedback->value = 0; + local_feedback->validity_ms = 0; + local_feedback->seq.major = (unsigned long long)time(NULL); + local_feedback->seq.minor = 0; + + ev_update = evi_publish_event(ev_update_name); + ev_throttle = evi_publish_event(ev_throttle_name); + if (ev_update == EVI_ERROR || ev_throttle == EVI_ERROR) { + LM_ERR("failed to publish overload-control events\n"); + goto error; + } + + if (register_via_param_provider(oc_via_params_provider) < 0) { + LM_ERR("failed to register overload-control Via parameter provider\n"); + goto error; + } + + return 0; + +error: + if (local_feedback) { + shm_free(local_feedback); + local_feedback = NULL; + } + if (oc_peer_count) { + shm_free(oc_peer_count); + oc_peer_count = NULL; + } + if (oc_peers) { + shm_free(oc_peers); + oc_peers = NULL; + } + if (oc_lock) { + lock_destroy(oc_lock); + lock_dealloc(oc_lock); + oc_lock = NULL; + } + return -1; +} + +static void mod_destroy(void) +{ + struct oc_peer *p, *next; + + if (oc_lock) + lock_get(oc_lock); + if (oc_peers) { + for (p = *oc_peers; p; p = next) { + next = p->next; + shm_free(p); + } + shm_free(oc_peers); + oc_peers = NULL; + } + if (oc_peer_count) { + shm_free(oc_peer_count); + oc_peer_count = NULL; + } + if (local_feedback) { + shm_free(local_feedback); + local_feedback = NULL; + } + if (oc_lock) { + lock_release(oc_lock); + lock_destroy(oc_lock); + lock_dealloc(oc_lock); + oc_lock = NULL; + } +} diff --git a/modules/sip_overload/test/opensips.cfg b/modules/sip_overload/test/opensips.cfg new file mode 100644 index 00000000000..8d0ddf3cbc8 --- /dev/null +++ b/modules/sip_overload/test/opensips.cfg @@ -0,0 +1,16 @@ +log_level = 2 +stderror_enabled = yes +syslog_enabled = no + +udp_workers = 1 +auto_aliases = no +socket = udp:localhost:5059 + +mpath = "modules/" + +loadmodule "proto_udp.so" +loadmodule "sip_overload.so" + +route { + exit; +} diff --git a/modules/sip_overload/test/test.c b/modules/sip_overload/test/test.c new file mode 100644 index 00000000000..503fcdd5000 --- /dev/null +++ b/modules/sip_overload/test/test.c @@ -0,0 +1,130 @@ +/* + * Unit tests for RFC 7339 / RFC 7415 parsing helpers. + * + * Copyright (C) 2026 OpenSIPS Project + * + * 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 + +int sip_overload_test_parse_uint(const char *text, unsigned int *out); +int sip_overload_test_parse_seq(const char *text, + unsigned long long *major, unsigned int *minor); +int sip_overload_test_seq_cmp(const char *left, const char *right); +int sip_overload_test_algo_list_has(const char *list, const char *algo); +int sip_overload_test_advertised_algo_list_valid(const char *list); + +static void test_uint_parser(void) +{ + unsigned int value = 0; + + ok(sip_overload_test_parse_uint("0", &value) == 0 && value == 0, + "sip-overload-uint-zero"); + ok(sip_overload_test_parse_uint("100", &value) == 0 && value == 100, + "sip-overload-uint-valid"); + ok(sip_overload_test_parse_uint("4294967295", &value) == 0 && + value == UINT_MAX, "sip-overload-uint-max"); + ok(sip_overload_test_parse_uint("4294967296", &value) < 0, + "sip-overload-uint-overflow"); + ok(sip_overload_test_parse_uint("-1", &value) < 0, + "sip-overload-uint-negative"); + ok(sip_overload_test_parse_uint("1x", &value) < 0, + "sip-overload-uint-garbage"); + ok(sip_overload_test_parse_uint("", &value) < 0, + "sip-overload-uint-empty"); +} + +static void test_sequence_parser(void) +{ + unsigned long long major = 0; + unsigned int minor = 0; + + ok(sip_overload_test_parse_seq("1.00001", &major, &minor) == 0 && + major == 1 && minor == 1, "sip-overload-seq-valid"); + ok(sip_overload_test_parse_seq("123456789012.99999", &major, &minor) == 0 && + major == 123456789012ULL && minor == 99999, + "sip-overload-seq-max-shape"); + ok(sip_overload_test_parse_seq("1.000001", &major, &minor) < 0, + "sip-overload-seq-minor-too-long"); + ok(sip_overload_test_parse_seq("1234567890123.1", &major, &minor) < 0, + "sip-overload-seq-major-too-long"); + ok(sip_overload_test_parse_seq("1", &major, &minor) < 0, + "sip-overload-seq-missing-dot"); + ok(sip_overload_test_parse_seq(".1", &major, &minor) < 0, + "sip-overload-seq-missing-major"); + ok(sip_overload_test_parse_seq("1.", &major, &minor) < 0, + "sip-overload-seq-missing-minor"); + ok(sip_overload_test_parse_seq("1.2.3", &major, &minor) < 0, + "sip-overload-seq-multiple-dots"); + + ok(sip_overload_test_seq_cmp("10.00001", "10.00001") == 0, + "sip-overload-seq-equal"); + ok(sip_overload_test_seq_cmp("10.00002", "10.00001") > 0, + "sip-overload-seq-newer-minor"); + ok(sip_overload_test_seq_cmp("11.00000", "10.99999") > 0, + "sip-overload-seq-newer-major"); + ok(sip_overload_test_seq_cmp("9.99999", "10.00000") < 0, + "sip-overload-seq-older-major"); +} + +static void test_advertised_algorithm_grammar(void) +{ + ok(sip_overload_test_advertised_algo_list_valid("loss"), + "sip-overload-advertised-algo-single"); + ok(sip_overload_test_advertised_algo_list_valid("loss,rate"), + "sip-overload-advertised-algo-list"); + ok(sip_overload_test_advertised_algo_list_valid("loss,abc123"), + "sip-overload-advertised-algo-extension-token"); + ok(!sip_overload_test_advertised_algo_list_valid("loss,"), + "sip-overload-advertised-algo-no-trailing-comma"); + ok(!sip_overload_test_advertised_algo_list_valid("loss,,rate"), + "sip-overload-advertised-algo-no-empty-token"); + ok(!sip_overload_test_advertised_algo_list_valid("loss, rate"), + "sip-overload-advertised-algo-no-whitespace"); + ok(!sip_overload_test_advertised_algo_list_valid("\"loss,rate\""), + "sip-overload-advertised-algo-no-config-quotes"); + ok(!sip_overload_test_advertised_algo_list_valid("loss;oc=100"), + "sip-overload-advertised-algo-no-via-injection"); + ok(!sip_overload_test_advertised_algo_list_valid("loss\r\nVia"), + "sip-overload-advertised-algo-no-header-injection"); +} + +static void test_algorithm_list(void) +{ + ok(sip_overload_test_algo_list_has("loss,rate", "loss"), + "sip-overload-algo-loss"); + ok(sip_overload_test_algo_list_has("loss,rate", "rate"), + "sip-overload-algo-rate"); + ok(sip_overload_test_algo_list_has("\"loss, rate\"", "rate"), + "sip-overload-algo-quoted-whitespace"); + ok(sip_overload_test_algo_list_has("LOSS,RATE", "loss"), + "sip-overload-algo-case-insensitive"); + ok(!sip_overload_test_algo_list_has("rate", "loss"), + "sip-overload-algo-missing-loss"); + ok(!sip_overload_test_algo_list_has("lossy,rate", "loss"), + "sip-overload-algo-token-boundary"); +} + +void mod_tests(void) +{ + test_uint_parser(); + test_sequence_parser(); + test_advertised_algorithm_grammar(); + test_algorithm_list(); +} diff --git a/msg_translator.c b/msg_translator.c index deb29be718f..dc89e615479 100644 --- a/msg_translator.c +++ b/msg_translator.c @@ -2496,6 +2496,198 @@ char * build_res_buf_from_sip_res( struct sip_msg* msg, } +#define VIA_PARAM_PROVIDER_MAX_COUNT 8 +#define VIA_PARAM_PROVIDER_MAX_BYTES 1024 + +static via_param_provider_f via_param_providers[VIA_PARAM_PROVIDER_MAX_COUNT]; +static unsigned int via_param_provider_count; + +int register_via_param_provider(via_param_provider_f provider) +{ + unsigned int i; + + if (!provider) + return -1; + + for (i = 0; i < via_param_provider_count; i++) + if (via_param_providers[i] == provider) + return 0; + + if (via_param_provider_count >= VIA_PARAM_PROVIDER_MAX_COUNT) { + LM_ERR("too many Via parameter providers (max %d)\n", + VIA_PARAM_PROVIDER_MAX_COUNT); + return -1; + } + + via_param_providers[via_param_provider_count++] = provider; + return 0; +} + +static int collect_via_param_provider_params(struct sip_msg *msg, int context, + char *buf, int buf_len, str *out) +{ + unsigned int i; + int used = 0, rc; + str part = STR_NULL; + + if (!out || !buf || buf_len <= 0) + return -1; + + out->s = NULL; + out->len = 0; + + for (i = 0; i < via_param_provider_count; i++) { + part = STR_NULL; + rc = via_param_providers[i](msg, context, &part); + if (rc < 0) + return -1; + if (rc == 0 || !part.s || part.len <= 0) + continue; + if (part.len > buf_len - used) { + LM_ERR("combined Via provider parameters exceed %d bytes\n", + buf_len); + return -1; + } + memcpy(buf + used, part.s, part.len); + used += part.len; + } + + if (used) { + out->s = buf; + out->len = used; + } + return 0; +} + + +/* + * Edits applied while copying the top Via of a locally generated reply: + * the provider parameters are inserted right after the last parameter of + * the first Via body (not at the end of the header, which may hold several + * comma-separated Via bodies), and any parameter of that body having the + * same name as a provider parameter is dropped, so that the provider value + * replaces it (e.g. RFC 7339 turns ";oc" into ";oc="). + */ +#define VIA_PARAM_PROVIDER_MAX_CUTS 16 + +struct via1_edit { + str params; /* provider parameters to insert */ + char *ins; /* insertion point, inside the original Via header */ + int inserted; + int ncuts; + int cut_len; /* total length of the removed parameters */ + str cuts[VIA_PARAM_PROVIDER_MAX_CUTS]; +}; + +/* returns the end of a Via param (or host:port) starting at @s, taking + * quoted-string values into account */ +static char *via1_token_end(char *s, char *end) +{ + int quoted = 0; + + for (; s < end; s++) { + if (quoted) { + if (*s == '\\' && s + 1 < end) + s++; + else if (*s == '"') + quoted = 0; + continue; + } + switch (*s) { + case '"': + quoted = 1; + break; + case ';': case ',': case '(': + case ' ': case '\t': case '\r': case '\n': + return s; + } + } + return s; +} + +static int via_params_have_name(const str *params, const str *name) +{ + char *p = params->s, *end = params->s + params->len, *n; + + while (p < end) { + while (p < end && (*p == ';' || *p == ' ' || *p == '\t')) + p++; + n = p; + while (p < end && *p != '=' && *p != ';' && *p != ' ' && *p != '\t') + p++; + if (p - n == name->len && !strncasecmp(n, name->s, name->len)) + return 1; + p = via1_token_end(p, end); + while (p < end && *p != ';') + p++; + } + return 0; +} + +static void via1_edit_init(struct sip_msg *msg, const str *params, + struct via1_edit *ed) +{ + struct via_body *vb = msg->via1; + struct via_param *vp; + char *end = msg->h_via1->body.s + msg->h_via1->body.len, *e; + + memset(ed, 0, sizeof *ed); + if (!params->s || params->len <= 0) + return; + + ed->params = *params; + ed->ins = via1_token_end(vb->last_param ? vb->last_param->start : + vb->host.s, end); + + for (vp = vb->param_lst; vp; vp = vp->next) { + if (vp == vb->branch || vp == vb->rport || vp == vb->received || + !via_params_have_name(params, &vp->name)) + continue; + if (ed->ncuts == VIA_PARAM_PROVIDER_MAX_CUTS) { + LM_WARN("too many Via params to replace, keeping the rest\n"); + break; + } + e = via1_token_end(vp->start, end); + /* also drop the preceding ';' */ + ed->cuts[ed->ncuts].s = vp->start - 1; + ed->cuts[ed->ncuts].len = e - (vp->start - 1); + ed->cut_len += ed->cuts[ed->ncuts].len; + ed->ncuts++; + } +} + +static inline char *via1_copy_plain(char *p, char *from, char *to, + struct via1_edit *ed) +{ + if (!ed->inserted && ed->ins && ed->ins >= from && ed->ins <= to) { + append_str(p, from, ed->ins - from); + append_str(p, ed->params.s, ed->params.len); + ed->inserted = 1; + from = ed->ins; + } + append_str(p, from, to - from); + return p; +} + +/* copies the [from, to) chunk of the original top Via, applying @ed */ +static char *via1_copy(char *p, char *from, char *to, struct via1_edit *ed) +{ + int k; + char *c_s, *c_e; + + for (k = 0; k < ed->ncuts && from < to; k++) { + c_s = ed->cuts[k].s; + c_e = c_s + ed->cuts[k].len; + if (c_e <= from || c_s >= to) + continue; + if (c_s > from) + p = via1_copy_plain(p, from, c_s, ed); + from = c_e < to ? c_e : to; + } + return via1_copy_plain(p, from, to, ed); +} + + char * build_res_buf_from_sip_req( unsigned int code, const str *text ,str *new_tag, struct sip_msg* msg, unsigned int *returned_len, struct bookmark *bmark) { @@ -2507,7 +2699,11 @@ char * build_res_buf_from_sip_req( unsigned int code, const str *text ,str *new_ struct lump_rpl *lump, *body; int i; str to_tag; + str via_provider_params = STR_NULL; + char via_provider_buf[VIA_PARAM_PROVIDER_MAX_BYTES]; + struct via1_edit via1_ed; + memset(&via1_ed, 0, sizeof via1_ed); body = 0; buf=0; to_tag.s = 0; @@ -2524,6 +2720,14 @@ char * build_res_buf_from_sip_req( unsigned int code, const str *text ,str *new_ goto error00; } + if (via_param_provider_count && msg->h_via1 && msg->via1) { + if (collect_via_param_provider_params(msg, VIA_PARAM_CTX_REPLY, + via_provider_buf, sizeof(via_provider_buf), + &via_provider_params) < 0) + via_provider_params = STR_NULL; + via1_edit_init(msg, &via_provider_params, &via1_ed); + } + /*computes the length of the new response buffer*/ len = 0; @@ -2565,7 +2769,9 @@ char * build_res_buf_from_sip_req( unsigned int code, const str *text ,str *new_ for( hdr=msg->h_via1 ; hdr ; hdr=hdr->sibling) { /* we always add CRLF to via*/ len+=(hdr->body.s+hdr->body.len)-hdr->name.s+CRLF_LEN; - if (hdr==msg->h_via1) len += received_len+rport_len; + if (hdr==msg->h_via1) + len += received_len + rport_len + + via1_ed.params.len - via1_ed.cut_len; } /* copy all Record-Route hdrs */ for( hdr=msg->record_route ; hdr ; hdr=hdr->sibling) { @@ -2640,33 +2846,34 @@ char * build_res_buf_from_sip_req( unsigned int code, const str *text ,str *new_ msg->via1->host.len + (msg->via1->port? msg->via1->port_str.len + 1 : 0); /* copy via1 up to params */ - append_str( p, hdr->name.s, i); + p = via1_copy(p, hdr->name.s, hdr->name.s + i, &via1_ed); /* copy received param */ append_str( p, received_buf, received_len); } if (rport_buf){ if (msg->via1->rport){ /* delete the old one */ /* copy until rport */ - append_str( p, hdr->name.s+i , - msg->via1->rport->start-hdr->name.s-1-i); + p = via1_copy(p, hdr->name.s + i, + msg->via1->rport->start - 1, &via1_ed); /* copy new rport */ append_str(p, rport_buf, rport_len); /* copy the rest of the via */ - append_str(p, msg->via1->rport->start+ - msg->via1->rport->size, - hdr->body.s+hdr->body.len- - msg->via1->rport->start- - msg->via1->rport->size); + p = via1_copy(p, msg->via1->rport->start + + msg->via1->rport->size, + hdr->body.s + hdr->body.len, &via1_ed); }else{ /* just copy rport and rest of hdr */ append_str(p, rport_buf, rport_len); - append_str( p, hdr->name.s+i , - (hdr->body.s+hdr->body.len)-hdr->name.s-i); + p = via1_copy(p, hdr->name.s + i, + hdr->body.s + hdr->body.len, &via1_ed); } }else{ /* normal whole via copy */ - append_str( p, hdr->name.s+i , - (hdr->body.s+hdr->body.len)-hdr->name.s-i); + p = via1_copy(p, hdr->name.s + i, + hdr->body.s + hdr->body.len, &via1_ed); } + /* safety net, should not happen */ + if (!via1_ed.inserted && via1_ed.params.len > 0) + append_str(p, via1_ed.params.s, via1_ed.params.len); append_str( p, CRLF,CRLF_LEN); /* and now the rest of the VIA hdrs */ for( hdr=hdr->sibling ; hdr ; hdr=hdr->sibling) { @@ -2844,6 +3051,12 @@ char* via_builder( unsigned int *len, int max_len, local_via_len=MY_VIA_LEN; const str* address_str; /* address displayed in via */ const str* port_str; /* port no displayed in via */ + str provider_params = STR_NULL; + char provider_buf[VIA_PARAM_PROVIDER_MAX_BYTES]; + + if (collect_via_param_provider_params(NULL, VIA_PARAM_CTX_REQUEST, + provider_buf, sizeof(provider_buf), &provider_params) < 0) + provider_params = STR_NULL; /* use pre-set address in via or the outbound socket one */ if (hp && hp->host && hp->host->len) @@ -2861,6 +3074,7 @@ char* via_builder( unsigned int *len, +1 /*':'*/+port_str->len +(branch?(MY_BRANCH_LEN+branch->len):0) +(extra_params?extra_params->len:0) + +provider_params.len +CRLF_LEN+1; line_buf=pkg_malloc( max_len ); if (line_buf==0){ @@ -2910,6 +3124,10 @@ char* via_builder( unsigned int *len, memcpy(line_buf+via_len, extra_params->s, extra_params->len); via_len+=extra_params->len; } + if (provider_params.s && provider_params.len > 0) { + memcpy(line_buf+via_len, provider_params.s, provider_params.len); + via_len+=provider_params.len; + } memcpy(line_buf+via_len, CRLF, CRLF_LEN); via_len+=CRLF_LEN; diff --git a/msg_translator.h b/msg_translator.h index db06fc61224..b9bd3fd0815 100644 --- a/msg_translator.h +++ b/msg_translator.h @@ -134,6 +134,20 @@ static inline const str *_get_adv_port(const struct socket_info *send_sock, return &send_sock->port_no_str; } +#define VIA_PARAM_CTX_REQUEST 0 +#define VIA_PARAM_CTX_REPLY 1 + +typedef int (*via_param_provider_f)(struct sip_msg *msg, int context, str *params); + +/* + * Register a provider for extra parameters placed on locally generated Via + * headers and on the top Via of locally generated replies. Multiple modules + * may register independent providers; their returned parameter strings are + * concatenated in registration order. Providers must be registered from + * mod_init(), before the worker processes are forked. + */ +int register_via_param_provider(via_param_provider_f provider); + char * build_req_buf_from_sip_req ( struct sip_msg* msg, unsigned int *returned_len, const struct socket_info* send_sock, int proto, str *via_params, unsigned int flags); diff --git a/packaging/redhat_fedora/opensips.spec b/packaging/redhat_fedora/opensips.spec index 5d3fc90ae2a..553ed8582a3 100644 --- a/packaging/redhat_fedora/opensips.spec +++ b/packaging/redhat_fedora/opensips.spec @@ -1161,6 +1161,7 @@ fi %{_libdir}/opensips/modules/alias_db.so %{_libdir}/opensips/modules/auth_aaa.so %{_libdir}/opensips/modules/auth_db.so +%{_libdir}/opensips/modules/sip_overload.so %{_libdir}/opensips/modules/sqlops.so %{_libdir}/opensips/modules/b2b_entities.so %{_libdir}/opensips/modules/b2b_logic.so @@ -1265,6 +1266,7 @@ fi %doc docdir/README.alias_db %doc docdir/README.auth_aaa %doc docdir/README.auth_db +%doc docdir/README.sip_overload %doc docdir/README.sqlops %doc docdir/README.b2b_entities %doc docdir/README.b2b_logic diff --git a/parser/parse_via.c b/parser/parse_via.c index 07b2924532a..edacadbf141 100644 --- a/parser/parse_via.c +++ b/parser/parse_via.c @@ -873,6 +873,7 @@ static /*inline*/ char* parse_via_param(char* p, char* end, state=F_VIA; goto endofvalue; case P_STRING: + break; /* a quoted-string may contain commas */ case F_LF: case F_CR: case F_CRLF: diff --git a/parser/test/test_parser.c b/parser/test/test_parser.c index 4de58818190..c357b8688ba 100644 --- a/parser/test/test_parser.c +++ b/parser/test/test_parser.c @@ -21,6 +21,8 @@ #include #include "../parse_uri.h" +#include "../parse_via.h" +#include "../../mem/mem.h" #include "test_parse_qop.h" #include "test_parse_fcaps.h" @@ -320,11 +322,44 @@ void test_parse_msg(void) } +/* a quoted-string Via param value (e.g. RFC 7339 oc-algo) may contain ',' */ +void test_parse_via_quoted_comma(void) +{ + char buf[] = "SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bKx;oc;" + "oc-algo=\"loss,rate\";x=1, SIP/2.0/UDP 10.0.0.2\r\n" + "Max-Forwards: 70\r\n"; + struct via_body *vb; + struct via_param *p; + + vb = pkg_malloc(sizeof *vb); + if (!vb) { + fail("pvia-quoted-comma-alloc"); + return; + } + memset(vb, 0, sizeof *vb); + + ok(parse_via(buf, buf + sizeof buf - 1, vb) != NULL && + vb->error == PARSE_OK, "pvia-quoted-comma-1"); + for (p = vb->param_lst; p; p = p->next) + if (str_match(&p->name, const_str("oc-algo"))) + break; + ok(p && str_match(&p->value, const_str("loss,rate")), + "pvia-quoted-comma-2"); + ok(vb->last_param && str_match(&vb->last_param->name, const_str("x")), + "pvia-quoted-comma-3"); + ok(vb->next && str_match(&vb->next->host, const_str("10.0.0.2")), + "pvia-quoted-comma-4"); + + free_via_list(vb); +} + + void test_parser(void) { test_parse_uri(); test_trim_user_params(); test_parse_msg(); + test_parse_via_quoted_comma(); test_parse_qop_val(); test_parse_fcaps(); test_parse_authenticate_body();