Commit b3728145b2 for asterisk.org
commit b3728145b2727c018597ddb97f05bbe3a3c35d7a
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Tue Mar 10 14:49:30 2026 +0100
res_pjsip_maintenance: Add PJSIP endpoint maintenance mode
Introduces res_pjsip_maintenance, a loadable module that allows
operators to place individual PJSIP endpoints into maintenance mode
at runtime without unregistering or disabling them.
While an endpoint is in maintenance mode:
* New inbound INVITE and SUBSCRIBE dialogs are rejected with
503 Service Unavailable and a Retry-After: 300 header.
* In-progress dialogs (re-INVITE, UPDATE, BYE, etc.) are
unaffected and complete normally.
* Outbound originations via Dial() or ARI originate are refused
before any SIP session is created.
State is held in-memory only and is cleared on module unload
or Asterisk restart.
This module was developed with AI assistance (Claude). All code
has been reviewed and tested by the author, who takes full
responsibility for the submission.
CLI interface:
pjsip set maintenance <on|off> <endpoint|all>
pjsip show maintenance [endpoint]
AMI interface:
Action: PJSIPSetMaintenance
Endpoint: <name>|all
State: on|off
Action: PJSIPShowMaintenance
Endpoint: <name> (optional; omit to list all)
Emits PJSIPMaintenanceStatus events per result, followed by
PJSIPMaintenanceStatusComplete. State changes also emit an
unsolicited PJSIPMaintenanceStatus event.
To support outbound blocking, a new session_create callback is
added to ast_sip_session_supplement. Supplements that set this
callback are invoked at the start of ast_sip_session_create_outgoing()
in res_pjsip_session, before any dialog or invite session resources
are allocated. res_pjsip_maintenance registers itself as a session
supplement and uses this callback to gate outbound session creation
on a per-endpoint basis.
MODULEINFO:
<depend>pjproject</depend>
<depend>res_pjsip</depend>
<depend>res_pjsip_session</depend>
UserNote: New module res_pjsip_maintenance adds runtime maintenance
mode for PJSIP endpoints. Use "pjsip set maintenance <on|off>
<endpoint|all>" to enable or disable, and "pjsip show maintenance"
to list affected endpoints. AMI actions PJSIPSetMaintenance and
PJSIPShowMaintenance provide programmatic access. No configuration
file changes required.
DeveloperNote: ast_sip_session_supplement gains a new optional
callback - int (*session_create)(struct ast_sip_endpoint *endpoint,
const char *destination). It is called from the global supplement
list (not per-session) at the start of ast_sip_session_create_outgoing()
via ast_sip_session_check_supplement_create(). Returning non-zero
blocks the outgoing session. Modules that need to gate outbound
SIP session creation should register a supplement with this callback
set rather than hooking into chan_pjsip directly.
diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h
index 7279d0d448..f2e741d76f 100644
--- a/include/asterisk/res_pjsip_session.h
+++ b/include/asterisk/res_pjsip_session.h
@@ -364,6 +364,31 @@ struct ast_sip_session_supplement {
* Defaults to AST_SIP_SESSION_BEFORE_MEDIA
*/
enum ast_sip_session_response_priority response_priority;
+ /*!
+ * \brief Called before an outgoing session is created
+ *
+ * This is called before the session dialog is created and can be used to
+ * block the creation of the session entirely. A non-zero return value
+ * prevents the session from being created. The callback is called from
+ * the global supplement list, not per-session, so the session does not
+ * yet exist when this is called.
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ *
+ * \param endpoint The endpoint the outgoing session would be created for
+ * \param contact The contact to use for the outgoing session, or NULL
+ * \param location Name of the location to call, be it named location or explicit URI, or NULL
+ * \param request_user Optional request user to place in the request URI, or NULL
+ * \param req_topology The requested stream capabilities
+ *
+ * \retval non-zero Block session creation
+ * \retval 0 Allow session creation
+ */
+ int (*session_create)(struct ast_sip_endpoint *endpoint,
+ struct ast_sip_contact *contact, const char *location,
+ const char *request_user, struct ast_stream_topology *req_topology);
};
enum ast_sip_session_sdp_stream_defer {
@@ -625,6 +650,33 @@ void ast_sip_session_register_supplement_with_module(struct ast_module *module,
*/
void ast_sip_session_unregister_supplement(struct ast_sip_session_supplement *supplement);
+/*!
+ * \brief Check registered supplements for permission to create an outgoing session
+ *
+ * Iterates the global supplement list and calls any registered \c session_create
+ * callbacks. The first callback to return a non-zero value stops the iteration
+ * and causes this function to return -1, blocking the session creation.
+ *
+ * This is called at the beginning of ast_sip_session_create_outgoing() before
+ * any dialog or invite session resources are allocated.
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ *
+ * \param endpoint The endpoint the outgoing session would be created for
+ * \param contact The contact to use for the outgoing session, or NULL
+ * \param location Name of the location to call, be it named location or explicit URI, or NULL
+ * \param request_user Optional request user to place in the request URI, or NULL
+ * \param req_topology The requested stream capabilities
+ *
+ * \retval 0 Session creation is allowed
+ * \retval -1 Session creation is blocked by a supplement
+ */
+int ast_sip_session_check_supplement_create(struct ast_sip_endpoint *endpoint,
+ struct ast_sip_contact *contact, const char *location,
+ const char *request_user, struct ast_stream_topology *req_topology);
+
/*!
* \brief Add supplements to a SIP session
*
diff --git a/res/res_pjsip/pjsip_session.c b/res/res_pjsip/pjsip_session.c
index 429fc6de25..c40c110a0e 100644
--- a/res/res_pjsip/pjsip_session.c
+++ b/res/res_pjsip/pjsip_session.c
@@ -108,6 +108,23 @@ int ast_sip_session_add_supplements(struct ast_sip_session *session)
return 0;
}
+int ast_sip_session_check_supplement_create(struct ast_sip_endpoint *endpoint,
+ struct ast_sip_contact *contact, const char *location,
+ const char *request_user, struct ast_stream_topology *req_topology)
+{
+ struct ast_sip_session_supplement *iter;
+ SCOPED_LOCK(lock, &session_supplements, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK);
+
+ AST_RWLIST_TRAVERSE(&session_supplements, iter, next) {
+ if (iter->session_create && iter->session_create(endpoint, contact, location,
+ request_user, req_topology)) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
void ast_sip_session_remove_supplements(struct ast_sip_session *session)
{
struct ast_sip_session_supplement *iter;
diff --git a/res/res_pjsip_maintenance.c b/res/res_pjsip_maintenance.c
new file mode 100644
index 0000000000..455aba83f3
--- /dev/null
+++ b/res/res_pjsip_maintenance.c
@@ -0,0 +1,754 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2026, Aurora Innovation
+ *
+ * Daniel Donoghue
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief PJSIP Endpoint Maintenance Mode
+ *
+ * Provides a runtime toggle to place individual PJSIP endpoints into
+ * maintenance mode. While an endpoint is in maintenance mode:
+ *
+ * - New \b inbound out-of-dialog requests are rejected with
+ * "503 Service Unavailable" and a Retry-After: 300 header
+ * (except SUBSCRIBE/REGISTER with Expires: 0).
+ * - \b Outbound originations (Dial, ARI originate) are refused before
+ * any SIP session or Asterisk channel is created.
+ * - Active in-progress dialogs (BYE, re-INVITE, UPDATE, etc.) are
+ * completely unaffected.
+ * - Existing presence/BLF subscriptions are left to expire naturally.
+ *
+ * CLI:
+ * pjsip set maintenance <on|off> <endpoint|all>
+ * pjsip show maintenance [endpoint]
+ *
+ * AMI actions: PJSIPSetMaintenance, PJSIPShowMaintenance
+ *
+ * \ingroup res_pjsip
+ */
+
+/*** MODULEINFO
+ <depend>pjproject</depend>
+ <depend>res_pjsip</depend>
+ <depend>res_pjsip_session</depend>
+ <support_level>extended</support_level>
+ ***/
+
+/*** DOCUMENTATION
+ <manager name="PJSIPSetMaintenance" language="en_US">
+ <since>
+ <version>20.20.0</version>
+ <version>22.10.0</version>
+ <version>23.4.0</version>
+ </since>
+ <synopsis>
+ Enable or disable maintenance mode for a PJSIP endpoint.
+ </synopsis>
+ <syntax>
+ <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+ <parameter name="Endpoint" required="true">
+ <para>The PJSIP endpoint name, or <literal>all</literal> to
+ toggle maintenance mode for every configured endpoint.</para>
+ </parameter>
+ <parameter name="State" required="true">
+ <para>Desired maintenance state.</para>
+ <enumlist>
+ <enum name="on" />
+ <enum name="off" />
+ </enumlist>
+ </parameter>
+ </syntax>
+ <description>
+ <para>Enables or disables maintenance mode for the specified PJSIP
+ endpoint. While in maintenance mode, new inbound out-of-dialog
+ requests are rejected with 503 Service Unavailable (except
+ SUBSCRIBE/REGISTER with Expires: 0), and outbound originations via
+ Dial() or ARI are refused before any SIP session or channel is
+ created. In-progress dialogs are unaffected.</para>
+ <para>A <literal>PJSIPMaintenanceStatus</literal> event is emitted
+ when the state changes.</para>
+ </description>
+ </manager>
+ <manager name="PJSIPShowMaintenance" language="en_US">
+ <since>
+ <version>20.20.0</version>
+ <version>22.10.0</version>
+ <version>23.4.0</version>
+ </since>
+ <synopsis>
+ Show maintenance mode status for PJSIP endpoints.
+ </synopsis>
+ <syntax>
+ <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+ <parameter name="Endpoint" required="false">
+ <para>If specified, show the status for this endpoint only.
+ If omitted, list all endpoints currently in maintenance
+ mode.</para>
+ </parameter>
+ </syntax>
+ <description>
+ <para>Emits one <literal>PJSIPMaintenanceStatus</literal> event
+ per result, followed by a
+ <literal>PJSIPMaintenanceStatusComplete</literal> event.</para>
+ </description>
+ </manager>
+ <managerEvent language="en_US" name="PJSIPMaintenanceStatus">
+ <managerEventInstance class="EVENT_FLAG_SYSTEM">
+ <since>
+ <version>20.20.0</version>
+ <version>22.10.0</version>
+ <version>23.4.0</version>
+ </since>
+ <synopsis>
+ Reports the maintenance mode state of a PJSIP endpoint.
+ </synopsis>
+ <syntax>
+ <parameter name="Endpoint">
+ <para>The PJSIP endpoint name.</para>
+ </parameter>
+ <parameter name="Status">
+ <para>Current maintenance state.</para>
+ <enumlist>
+ <enum name="enabled" />
+ <enum name="disabled" />
+ </enumlist>
+ </parameter>
+ </syntax>
+ <description>
+ <para>Emitted when an endpoint enters or leaves maintenance
+ mode, and as a list entry in response to
+ <literal>PJSIPShowMaintenance</literal>.</para>
+ </description>
+ </managerEventInstance>
+ </managerEvent>
+ ***/
+
+#include "asterisk.h"
+
+#include <pjsip.h>
+
+#include "asterisk/res_pjsip.h"
+#include "asterisk/res_pjsip_session.h"
+#include "asterisk/manager.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+#include "asterisk/cli.h"
+#include "asterisk/sorcery.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/strings.h"
+
+enum {
+ MAINT_HASH_BUCKETS = 53,
+};
+
+/*! Endpoints currently in maintenance mode.
+ * Protected by the container's own internal RWLOCK.
+ * No other locks are ever held simultaneously with this container.
+ */
+static struct ao2_container *maintenance_set;
+
+/*!
+ * \internal
+ * \brief Add an endpoint to the maintenance set.
+ * \retval 1 Added successfully.
+ * \retval 0 Already in maintenance (no-op).
+ * \retval -1 Allocation failure.
+ */
+static int maint_set_add(const char *endpoint_name)
+{
+ char *entry;
+
+ entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY);
+ if (entry) {
+ ao2_ref(entry, -1);
+ return 0; /* already in maintenance */
+ }
+ return ast_str_container_add(maintenance_set, endpoint_name) ? -1 : 1;
+}
+
+/*!
+ * \internal
+ * \brief Remove an endpoint from the maintenance set.
+ * \retval 1 Removed successfully.
+ * \retval 0 Was not in maintenance (no-op).
+ */
+static int maint_set_remove(const char *endpoint_name)
+{
+ char *entry;
+
+ entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY | OBJ_UNLINK);
+ if (!entry) {
+ return 0;
+ }
+ ao2_ref(entry, -1);
+ return 1;
+}
+
+/*!
+ * \internal
+ * \brief Apply a maintenance state change to the maintenance set.
+ *
+ * Does not validate endpoint existence; callers are responsible for that.
+ * Callers are also responsible for emitting log messages and AMI events.
+ *
+ * \retval 1 State changed.
+ * \retval 0 Already in requested state (no-op).
+ * \retval -1 Allocation failure (enable path only).
+ */
+static int apply_maintenance_state(const char *endpoint_name, int enable)
+{
+ return enable ? maint_set_add(endpoint_name) : maint_set_remove(endpoint_name);
+}
+
+/* Session supplement: block outgoing session creation when endpoint is in maintenance. */
+
+/*!
+ * \internal
+ * \brief Session supplement session_create callback: block outgoing sessions to
+ * endpoints currently in maintenance mode.
+ * \retval 1 Endpoint is in maintenance; session creation blocked.
+ * \retval 0 Endpoint is not in maintenance; session creation allowed.
+ */
+static int maint_session_create(struct ast_sip_endpoint *endpoint,
+ struct ast_sip_contact *contact, const char *location,
+ const char *request_user, struct ast_stream_topology *req_topology)
+{
+ const char *endpoint_name = ast_sorcery_object_get_id(endpoint);
+ char *entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY);
+
+ if (entry) {
+ ao2_ref(entry, -1);
+ ast_log(LOG_NOTICE, "PJSIP: Refusing outbound call to endpoint '%s': maintenance mode active\n",
+ endpoint_name);
+ return 1;
+ }
+ return 0;
+}
+
+static struct ast_sip_session_supplement maintenance_session_supplement = {
+ .session_create = maint_session_create,
+ .priority = AST_SIP_SUPPLEMENT_PRIORITY_FIRST,
+};
+
+/* Inbound request hook for maintenance_pjsip_mod.
+ *
+ * For endpoints in maintenance mode, blocks new out-of-dialog requests
+ * with 503 + Retry-After: 300. Any in-dialog request is passed through
+ * unmodified. SUBSCRIBE and REGISTER with Expires: 0 are also passed
+ * through, allowing un-subscribe and de-registration. */
+
+static pj_bool_t maintenance_on_rx_request(pjsip_rx_data *rdata)
+{
+ pjsip_msg *msg = rdata->msg_info.msg;
+ const pjsip_method *method = &msg->line.req.method;
+ pjsip_to_hdr *to;
+ pjsip_expires_hdr *expires_hdr;
+ struct ast_sip_endpoint *endpoint;
+ char *entry;
+ pjsip_hdr hdr_list;
+ pjsip_generic_int_hdr *retry_after;
+ static const pj_str_t str_retry_after = { "Retry-After", 11 };
+ int is_subscribe;
+ int is_register;
+
+ is_subscribe = pjsip_method_cmp(method, pjsip_get_subscribe_method()) == 0;
+ is_register = pjsip_method_cmp(method, pjsip_get_register_method()) == 0;
+
+ /* Any in-dialog request is always allowed through. */
+ to = rdata->msg_info.to;
+ if (to->tag.slen > 0) {
+ return PJ_FALSE;
+ }
+
+ /* SUBSCRIBE or REGISTER with Expires: 0: allow un-subscribe / de-register. */
+ if (is_subscribe || is_register) {
+ expires_hdr = pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL);
+ if (expires_hdr && expires_hdr->ivalue == 0) {
+ return PJ_FALSE;
+ }
+ }
+
+ endpoint = ast_pjsip_rdata_get_endpoint(rdata);
+ if (!endpoint) {
+ return PJ_FALSE;
+ }
+
+ entry = ao2_find(maintenance_set, ast_sorcery_object_get_id(endpoint), OBJ_SEARCH_KEY);
+ if (!entry) {
+ ao2_ref(endpoint, -1);
+ return PJ_FALSE;
+ }
+ ao2_ref(entry, -1);
+
+ ast_log(LOG_NOTICE, "PJSIP: Endpoint '%s' is in maintenance mode; rejecting new %.*s from %s\n",
+ ast_sorcery_object_get_id(endpoint),
+ (int)method->name.slen, method->name.ptr,
+ rdata->pkt_info.src_name);
+
+ ao2_ref(endpoint, -1);
+
+ pj_list_init(&hdr_list);
+ retry_after = pjsip_generic_int_hdr_create(rdata->tp_info.pool,
+ &str_retry_after, 300);
+ if (retry_after) {
+ pj_list_push_back(&hdr_list, retry_after);
+ }
+
+ pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 503, NULL,
+ retry_after ? &hdr_list : NULL, NULL);
+
+ return PJ_TRUE;
+}
+
+static struct pjsip_module maintenance_pjsip_mod = {
+ .name = { "Maintenance Module", 18 },
+ /*
+ * Run after endpoint identification (endpoint_mod,
+ * PJSIP_MOD_PRIORITY_TSX_LAYER - 3) so that
+ * ast_pjsip_rdata_get_endpoint() returns the identified endpoint,
+ * but before the request authenticator
+ * (PJSIP_MOD_PRIORITY_APPLICATION - 2) so that a maintenance
+ * endpoint receives 503 rather than a 401 challenge.
+ */
+ .priority = PJSIP_MOD_PRIORITY_APPLICATION - 3,
+ .on_rx_request = maintenance_on_rx_request,
+};
+
+/* Sorcery observer - clean up stale entries when an endpoint is deleted. */
+
+static void maint_endpoint_deleted(const void *obj)
+{
+ maint_set_remove(ast_sorcery_object_get_id(obj));
+}
+
+static const struct ast_sorcery_observer endpoint_observer = {
+ .deleted = maint_endpoint_deleted,
+};
+
+/* CLI helpers */
+
+/*!
+ * \internal
+ * \brief Tab-complete a PJSIP endpoint name.
+ */
+static char *cli_complete_endpoint(const char *word)
+{
+ int wordlen = strlen(word);
+ struct ao2_container *endpoints;
+ struct ast_sip_endpoint *endpoint;
+ struct ao2_iterator i;
+
+ endpoints = ast_sorcery_retrieve_by_prefix(ast_sip_get_sorcery(),
+ "endpoint", word, wordlen);
+ if (!endpoints) {
+ return NULL;
+ }
+
+ i = ao2_iterator_init(endpoints, 0);
+ while ((endpoint = ao2_iterator_next(&i))) {
+ ast_cli_completion_add(ast_strdup(ast_sorcery_object_get_id(endpoint)));
+ ao2_cleanup(endpoint);
+ }
+ ao2_iterator_destroy(&i);
+ ao2_ref(endpoints, -1);
+
+ return NULL;
+}
+
+/* CLI: pjsip set maintenance <on|off> <endpoint|all> */
+
+static char *handle_cli_pjsip_set_maintenance(struct ast_cli_entry *e, int cmd,
+ struct ast_cli_args *a)
+{
+ struct ast_sip_endpoint *endpoint;
+ struct ao2_container *all_endpoints;
+ struct ao2_iterator it;
+ const char *endpoint_name;
+ int enable;
+ int rc;
+ int count;
+ int failed;
+
+ switch (cmd) {
+ case CLI_INIT:
+ e->command = "pjsip set maintenance";
+ e->usage =
+ "Usage: pjsip set maintenance <on|off> <endpoint|all>\n"
+ " Place a PJSIP endpoint into or out of maintenance mode.\n"
+ " Use 'all' to toggle maintenance mode for every endpoint.\n"
+ " While in maintenance mode new inbound out-of-dialog requests\n"
+ " to that endpoint are rejected with 503 (except SUBSCRIBE/\n"
+ " REGISTER with Expires: 0), and outbound originations are\n"
+ " refused.\n";
+ return NULL;
+ case CLI_GENERATE:
+ if (a->pos == 3) {
+ static const char * const opts[] = { "on", "off", NULL };
+ return ast_cli_complete(a->word, opts, a->n);
+ }
+ if (a->pos == 4) {
+ if (!strncasecmp("all", a->word, strlen(a->word))) {
+ ast_cli_completion_add(ast_strdup("all"));
+ }
+ return cli_complete_endpoint(a->word);
+ }
+ return NULL;
+ }
+
+ if (a->argc != 5) {
+ return CLI_SHOWUSAGE;
+ }
+
+ if (!strcasecmp(a->argv[3], "on")) {
+ enable = 1;
+ } else if (!strcasecmp(a->argv[3], "off")) {
+ enable = 0;
+ } else {
+ return CLI_SHOWUSAGE;
+ }
+
+ endpoint_name = a->argv[4];
+
+ if (!strcasecmp(endpoint_name, "all")) {
+ all_endpoints = ast_sip_get_endpoints();
+ if (!all_endpoints) {
+ ast_cli(a->fd, "Failed to retrieve endpoint list\n");
+ return CLI_SUCCESS;
+ }
+ count = 0;
+ failed = 0;
+ it = ao2_iterator_init(all_endpoints, 0);
+ while ((endpoint = ao2_iterator_next(&it))) {
+ rc = apply_maintenance_state(ast_sorcery_object_get_id(endpoint), enable);
+ if (rc > 0) {
+ count++;
+ } else if (rc < 0) {
+ failed++;
+ }
+ ao2_ref(endpoint, -1);
+ }
+ ao2_iterator_destroy(&it);
+ ao2_ref(all_endpoints, -1);
+ if (count > 0) {
+ manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus",
+ "Endpoint: all\r\n"
+ "Status: %s\r\n",
+ enable ? "enabled" : "disabled");
+ ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for all endpoints "
+ "(%d endpoint%s affected)\n",
+ enable ? "enabled" : "disabled",
+ count, count == 1 ? "" : "s");
+ }
+ ast_cli(a->fd, "Maintenance mode %s for %d endpoint%s%s\n",
+ enable ? "ENABLED" : "DISABLED",
+ count, count == 1 ? "" : "s",
+ failed ? " (some failed)" : "");
+ return CLI_SUCCESS;
+ }
+
+ endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
+ if (!endpoint) {
+ ast_cli(a->fd, "Endpoint '%s' not found\n", endpoint_name);
+ return CLI_SUCCESS;
+ }
+ ao2_ref(endpoint, -1);
+
+ rc = apply_maintenance_state(endpoint_name, enable);
+ if (rc > 0) {
+ manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus",
+ "Endpoint: %s\r\n"
+ "Status: %s\r\n",
+ endpoint_name, enable ? "enabled" : "disabled");
+ ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for endpoint '%s'\n",
+ enable ? "enabled" : "disabled", endpoint_name);
+ ast_cli(a->fd, "Maintenance mode %s for endpoint '%s'\n",
+ enable ? "ENABLED" : "DISABLED", endpoint_name);
+ } else if (rc == 0 && enable) {
+ ast_cli(a->fd, "Endpoint '%s' is already in maintenance mode\n", endpoint_name);
+ } else if (rc == 0) {
+ ast_cli(a->fd, "Endpoint '%s' was not in maintenance mode\n", endpoint_name);
+ } else {
+ ast_cli(a->fd, "Failed to %s maintenance mode for endpoint '%s'\n",
+ enable ? "enable" : "disable", endpoint_name);
+ }
+
+ return CLI_SUCCESS;
+}
+
+/* CLI: pjsip show maintenance [endpoint] */
+
+/*! \brief ao2_callback used to print one maintenance entry to the CLI */
+static int cli_maint_entry_cb(void *obj, void *arg, int flags)
+{
+ const char *name = obj;
+ int fd = *(int *)arg;
+ ast_cli(fd, " %-40s ON\n", name);
+ return 0;
+}
+
+static char *handle_cli_pjsip_show_maintenance(struct ast_cli_entry *e, int cmd,
+ struct ast_cli_args *a)
+{
+ const char *endpoint_name;
+ char *entry;
+ int fd;
+ int count;
+
+ switch (cmd) {
+ case CLI_INIT:
+ e->command = "pjsip show maintenance";
+ e->usage =
+ "Usage: pjsip show maintenance [endpoint]\n"
+ " Display endpoints currently in maintenance mode.\n"
+ " If [endpoint] is given, show the status for that endpoint only.\n";
+ return NULL;
+ case CLI_GENERATE:
+ if (a->pos == 3) {
+ return cli_complete_endpoint(a->word);
+ }
+ return NULL;
+ }
+
+ if (a->argc == 4) {
+ endpoint_name = a->argv[3];
+ entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY);
+ if (entry) {
+ ast_cli(a->fd, "Endpoint '%s' is in maintenance mode\n", endpoint_name);
+ ao2_ref(entry, -1);
+ } else {
+ ast_cli(a->fd, "Endpoint '%s' is NOT in maintenance mode\n", endpoint_name);
+ }
+ return CLI_SUCCESS;
+ }
+
+ if (a->argc != 3) {
+ return CLI_SHOWUSAGE;
+ }
+
+ ast_cli(a->fd, "\n");
+ ast_cli(a->fd, " %-40s %s\n", "Endpoint", "State");
+ ast_cli(a->fd, " %-40s -----\n", "----------------------------------------");
+ fd = a->fd;
+ ao2_callback(maintenance_set, OBJ_NODATA, cli_maint_entry_cb, &fd);
+
+ count = ao2_container_count(maintenance_set);
+ ast_cli(a->fd, "\n %d endpoint%s in maintenance mode\n\n",
+ count, count == 1 ? "" : "s");
+
+ return CLI_SUCCESS;
+}
+
+static struct ast_cli_entry cli_maintenance[] = {
+ AST_CLI_DEFINE(handle_cli_pjsip_set_maintenance, "Set PJSIP endpoint maintenance mode"),
+ AST_CLI_DEFINE(handle_cli_pjsip_show_maintenance, "Show PJSIP endpoint maintenance status"),
+};
+
+/* AMI: PJSIPSetMaintenance, PJSIPShowMaintenance */
+
+static int ami_set_maintenance(struct mansession *s, const struct message *m)
+{
+ const char *endpoint_name;
+ const char *state_str;
+ struct ast_sip_endpoint *endpoint;
+ struct ao2_container *all_endpoints;
+ struct ao2_iterator it;
+ int enable;
+ int rc;
+
+ endpoint_name = astman_get_header(m, "Endpoint");
+ state_str = astman_get_header(m, "State");
+
+ if (ast_strlen_zero(endpoint_name)) {
+ astman_send_error(s, m, "Endpoint parameter missing");
+ return 0;
+ }
+ if (ast_strlen_zero(state_str)) {
+ astman_send_error(s, m, "State parameter missing");
+ return 0;
+ }
+
+ if (!strcasecmp(state_str, "on")) {
+ enable = 1;
+ } else if (!strcasecmp(state_str, "off")) {
+ enable = 0;
+ } else {
+ astman_send_error(s, m, "State must be 'on' or 'off'");
+ return 0;
+ }
+
+ if (!strcasecmp(endpoint_name, "all")) {
+ int count = 0;
+
+ all_endpoints = ast_sip_get_endpoints();
+ if (!all_endpoints) {
+ astman_send_error(s, m, "Failed to retrieve endpoint list");
+ return 0;
+ }
+ it = ao2_iterator_init(all_endpoints, 0);
+ while ((endpoint = ao2_iterator_next(&it))) {
+ if (apply_maintenance_state(ast_sorcery_object_get_id(endpoint), enable) > 0) {
+ count++;
+ }
+ ao2_ref(endpoint, -1);
+ }
+ ao2_iterator_destroy(&it);
+ ao2_ref(all_endpoints, -1);
+ if (count > 0) {
+ manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus",
+ "Endpoint: all\r\n"
+ "Status: %s\r\n",
+ enable ? "enabled" : "disabled");
+ ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for all endpoints "
+ "(%d endpoint%s affected)\n",
+ enable ? "enabled" : "disabled",
+ count, count == 1 ? "" : "s");
+ }
+ astman_send_ack(s, m,
+ enable ? "Maintenance mode enabled for all endpoints"
+ : "Maintenance mode disabled for all endpoints");
+ return 0;
+ }
+
+ endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
+ if (!endpoint) {
+ astman_send_error_va(s, m, "Endpoint '%s' not found", endpoint_name);
+ return 0;
+ }
+ ao2_ref(endpoint, -1);
+
+ rc = apply_maintenance_state(endpoint_name, enable);
+ if (rc < 0) {
+ astman_send_error_va(s, m, "Failed to %s maintenance mode for endpoint '%s'",
+ enable ? "enable" : "disable", endpoint_name);
+ } else {
+ if (rc > 0) {
+ manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus",
+ "Endpoint: %s\r\n"
+ "Status: %s\r\n",
+ endpoint_name, enable ? "enabled" : "disabled");
+ ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for endpoint '%s'\n",
+ enable ? "enabled" : "disabled", endpoint_name);
+ }
+ astman_send_ack(s, m,
+ enable ? "Maintenance mode enabled" : "Maintenance mode disabled");
+ }
+
+ return 0;
+}
+
+/*! \brief ao2_callback used to emit one PJSIPMaintenanceStatus AMI list entry */
+static int ami_maint_entry_cb(void *obj, void *arg, int flags)
+{
+ const char *name = obj;
+ struct ast_sip_ami *ami = arg;
+ struct ast_str *buf;
+
+ buf = ast_sip_create_ami_event("PJSIPMaintenanceStatus", ami);
+ if (!buf) {
+ return 0;
+ }
+ ast_str_append(&buf, 0, "Endpoint: %s\r\nStatus: enabled\r\n", name);
+ astman_append(ami->s, "%s\r\n", ast_str_buffer(buf));
+ ast_free(buf);
+ ++ami->count;
+
+ return 0;
+}
+
+static int ami_show_maintenance(struct mansession *s, const struct message *m)
+{
+ const char *endpoint_name;
+ struct ast_sip_ami ami;
+ char *entry;
+ struct ast_str *buf;
+
+ endpoint_name = astman_get_header(m, "Endpoint");
+
+ ami.s = s;
+ ami.m = m;
+ ami.action_id = astman_get_header(m, "ActionID");
+ ami.arg = NULL;
+ ami.count = 0;
+
+ astman_send_listack(s, m, "Maintenance status events follow", "start");
+
+ if (!ast_strlen_zero(endpoint_name)) {
+ buf = ast_sip_create_ami_event("PJSIPMaintenanceStatus", &ami);
+ if (buf) {
+ entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY);
+ ast_str_append(&buf, 0, "Endpoint: %s\r\nStatus: %s\r\n",
+ endpoint_name, entry ? "enabled" : "disabled");
+ if (entry) {
+ ao2_ref(entry, -1);
+ }
+ astman_append(s, "%s\r\n", ast_str_buffer(buf));
+ ast_free(buf);
+ }
+ ami.count = 1;
+ } else {
+ ao2_callback(maintenance_set, OBJ_NODATA, ami_maint_entry_cb, &ami);
+ }
+
+ astman_send_list_complete_start(s, m, "PJSIPMaintenanceStatusComplete", ami.count);
+ astman_send_list_complete_end(s);
+
+ return 0;
+}
+
+/* Module load / unload */
+
+static int load_module(void)
+{
+ maintenance_set = ast_str_container_alloc_options(AO2_ALLOC_OPT_LOCK_RWLOCK,
+ MAINT_HASH_BUCKETS);
+ if (!maintenance_set) {
+ ast_log(LOG_ERROR, "res_pjsip_maintenance: failed to allocate maintenance set\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ ast_sorcery_observer_add(ast_sip_get_sorcery(), "endpoint", &endpoint_observer);
+ ast_sip_register_service(&maintenance_pjsip_mod);
+ ast_sip_session_register_supplement(&maintenance_session_supplement);
+ ast_manager_register_xml("PJSIPSetMaintenance",
+ EVENT_FLAG_SYSTEM, ami_set_maintenance);
+ ast_manager_register_xml("PJSIPShowMaintenance",
+ EVENT_FLAG_SYSTEM | EVENT_FLAG_REPORTING, ami_show_maintenance);
+ ast_cli_register_multiple(cli_maintenance, ARRAY_LEN(cli_maintenance));
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+ ast_cli_unregister_multiple(cli_maintenance, ARRAY_LEN(cli_maintenance));
+ ast_manager_unregister("PJSIPShowMaintenance");
+ ast_manager_unregister("PJSIPSetMaintenance");
+ ast_sip_session_unregister_supplement(&maintenance_session_supplement);
+ ast_sip_unregister_service(&maintenance_pjsip_mod);
+ ast_sorcery_observer_remove(ast_sip_get_sorcery(), "endpoint", &endpoint_observer);
+ ao2_cleanup(maintenance_set);
+ maintenance_set = NULL;
+ return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP Endpoint Maintenance Mode",
+ .support_level = AST_MODULE_SUPPORT_EXTENDED,
+ .load = load_module,
+ .unload = unload_module,
+ .load_pri = AST_MODPRI_APP_DEPEND,
+ .requires = "res_pjsip,res_pjsip_session",
+);
diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c
index 04e6444689..4a1fb56bfa 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -3248,6 +3248,12 @@ struct ast_sip_session *ast_sip_session_create_outgoing(struct ast_sip_endpoint
SCOPE_ENTER(1, "%s %s Topology: %s\n", ast_sorcery_object_get_id(endpoint), request_user,
ast_str_tmp(256, ast_stream_topology_to_str(req_topology, &STR_TMP)));
+ if (ast_sip_session_check_supplement_create(endpoint, contact, location,
+ request_user, req_topology)) {
+ SCOPE_EXIT_RTN_VALUE(NULL, "%s: Session creation blocked by supplement\n",
+ ast_sorcery_object_get_id(endpoint));
+ }
+
/* If no location has been provided use the AOR list from the endpoint itself */
if (location || !contact) {
location = S_OR(location, endpoint->aors);