From 9da537a19f881b67f35bd98895f5939983ccff73 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 19:33:23 +0500 Subject: [PATCH] [mod_sofia] Add SIP 603+ detection and passthrough control. Add unit-tests. (#3035) Implement SIP 603+ (ATIS-1000099) support for FCC analytics-based call blocking compliance. Detection: - Detect incoming 603+ responses by checking "Network Blocked" phrase and "v=analytics1;" in the `Reason` header text - Set `sip_603plus_reason` channel variable on both legs for CDR visibility Passthrough control: - `sip_603plus_passthrough=true`: forward 603+ phrase and Reason header - `sip_603plus_passthrough=false`: strip `Reason` header, send clean `603 Decline` - Not set: existing behavior preserved - Works independently of `disable_q850_reason` for selective forwarding --- conf/vanilla/vars.xml | 7 + src/mod/endpoints/mod_sofia/Makefile.am | 10 +- src/mod/endpoints/mod_sofia/mod_sofia.c | 23 + src/mod/endpoints/mod_sofia/sofia.c | 14 + .../mod_sofia/test/conf/freeswitch.xml | 84 +++ .../endpoints/mod_sofia/test/test_603plus.c | 490 ++++++++++++++++++ 6 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 src/mod/endpoints/mod_sofia/test/test_603plus.c diff --git a/conf/vanilla/vars.xml b/conf/vanilla/vars.xml index 54a15a2534..ae7c355f99 100644 --- a/conf/vanilla/vars.xml +++ b/conf/vanilla/vars.xml @@ -430,4 +430,11 @@ + + + diff --git a/src/mod/endpoints/mod_sofia/Makefile.am b/src/mod/endpoints/mod_sofia/Makefile.am index 3a5d295ddb..378ba06c73 100644 --- a/src/mod/endpoints/mod_sofia/Makefile.am +++ b/src/mod/endpoints/mod_sofia/Makefile.am @@ -15,7 +15,7 @@ mod_sofia_la_SOURCES = mod_sofia_la_LIBADD = $(switch_builddir)/libfreeswitch.la libsofiamod.la mod_sofia_la_LDFLAGS = -avoid-version -module -no-undefined -shared $(SOFIA_SIP_LIBS) $(STIRSHAKEN_LIBS) -noinst_PROGRAMS = test/test_sofia_funcs test/test_nuafail test/sipp-based-tests +noinst_PROGRAMS = test/test_sofia_funcs test/test_nuafail test/sipp-based-tests test/test_603plus test_test_sofia_funcs_SOURCES = test/test_sofia_funcs.c test_test_sofia_funcs_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) $(STIRSHAKEN_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" @@ -25,6 +25,11 @@ endif test_test_sofia_funcs_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) $(STIRSHAKEN_LIBS) test_test_sofia_funcs_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) $(STIRSHAKEN_LIBS) +test_test_603plus_SOURCES = test/test_603plus.c +test_test_603plus_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" +test_test_603plus_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) +test_test_603plus_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) + test_test_nuafail_SOURCES = test/test_nuafail.c test_test_nuafail_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) $(STIRSHAKEN_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" if HAVE_STIRSHAKEN @@ -38,13 +43,14 @@ test_sipp_based_tests_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) -DSWITCH_TEST_BA test_sipp_based_tests_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) test_sipp_based_tests_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) -TESTS = test/test_sofia_funcs.sh test/test_nuafail +TESTS = test/test_sofia_funcs.sh test/test_nuafail test/test_603plus #TESTS += test/test_run_sipp.sh if ISMAC mod_sofia_la_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_test_sofia_funcs_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_test_nuafail_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration +test_test_603plus_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_sipp_based_tests_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration endif diff --git a/src/mod/endpoints/mod_sofia/mod_sofia.c b/src/mod/endpoints/mod_sofia/mod_sofia.c index 0bf07b57be..47b2d0681e 100644 --- a/src/mod/endpoints/mod_sofia/mod_sofia.c +++ b/src/mod/endpoints/mod_sofia/mod_sofia.c @@ -493,6 +493,7 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) const char *val = NULL; const char *max_forwards = switch_channel_get_variable(channel, SWITCH_MAX_FORWARDS_VARIABLE); const char *call_info = switch_channel_get_variable(channel, "presence_call_info_full"); + const char *passthrough_603plus = switch_channel_get_variable(channel, "sip_603plus_passthrough"); const char *session_id_header = sofia_glue_session_id_header(session, tech_pvt->profile); val = switch_channel_get_variable(tech_pvt->channel, "disable_q850_reason"); @@ -512,6 +513,23 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) } } + /* 603+ (ATIS-1000099) Reason header override — applied after standard reason construction. + * + * passthrough=true: Restore 603+ Reason even if disable_q850_reason suppressed it. + * Allows selective forwarding of 603+ while suppressing other Reason headers. + * passthrough=false: Strip Reason header entirely — send clean 603 Decline with no Reason. */ + if (passthrough_603plus) { + const char *reason_603plus = switch_channel_get_variable(channel, "sip_603plus_reason"); + + if (!zstr(reason_603plus)) { + if (switch_true(passthrough_603plus)) { + reason = switch_core_session_sprintf(session, "%s", reason_603plus); + } else if (switch_false(passthrough_603plus)) { + reason = switch_core_session_sprintf(session, ""); + } + } + } + if (switch_channel_test_flag(channel, CF_INTERCEPT) || cause == SWITCH_CAUSE_PICKED_OFF || cause == SWITCH_CAUSE_LOSE_RACE) { switch_channel_set_variable(channel, "call_completed_elsewhere", "true"); } @@ -557,6 +575,11 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) if (tech_pvt->respond_phrase) { //phrase = su_strdup(nua_handle_home(tech_pvt->nh), tech_pvt->respond_phrase); phrase = tech_pvt->respond_phrase; + } else if (sip_cause == 603 + && !zstr(reason) + && switch_true(passthrough_603plus) + && !zstr(switch_channel_get_variable(channel, "sip_603plus_reason"))) { + phrase = "Network Blocked"; } else { phrase = sip_status_phrase(sip_cause); } diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index 62b4963e78..8d82e9d3fd 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -6658,9 +6658,23 @@ static void sofia_handle_sip_r_invite(switch_core_session_t *session, int status switch_channel_set_variable(channel, "sip_reason", reason_header); switch_channel_set_variable_partner(channel, "sip_reason", reason_header); } + + /* 603+ (ATIS-1000099) detection: clear stale state from serial forking, then check */ + switch_channel_set_variable(channel, "sip_603plus_reason", NULL); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", NULL); + + if (status == 603 && phrase && !strcasecmp(phrase, "Network Blocked") + && sip->sip_reason && sip->sip_reason->re_text + && !strncmp(sip->sip_reason->re_text, "\"v=analytics1;", 14) + && !zstr(reason_header)) { + + switch_channel_set_variable(channel, "sip_603plus_reason", reason_header); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", reason_header); + } } else { switch_channel_set_variable_partner(channel, "sip_invite_failure_status", NULL); switch_channel_set_variable_partner(channel, "sip_invite_failure_phrase", NULL); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", NULL); } if (status >= 400 && sip->sip_reason && sip->sip_reason->re_protocol && (!strcasecmp(sip->sip_reason->re_protocol, "Q.850") diff --git a/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml b/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml index fa626af93e..6a2959918e 100644 --- a/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml +++ b/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml @@ -140,6 +140,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mod/endpoints/mod_sofia/test/test_603plus.c b/src/mod/endpoints/mod_sofia/test/test_603plus.c new file mode 100644 index 0000000000..658881c884 --- /dev/null +++ b/src/mod/endpoints/mod_sofia/test/test_603plus.c @@ -0,0 +1,490 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2026, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Dmitry Verenitsin + * + * + * test_603plus.c -- Tests for SIP 603+ (ATIS-1000099) detection and passthrough + * + * Detection requires BOTH: + * 1. SIP status 603 with phrase "Network Blocked" (case-insensitive) + * 2. Reason header text starts with "v=analytics1;" (ATIS version AVP) + * + * Test approach: originate via loopback gateway (same FS instance). + * The responding extension sends a crafted 603 with/without Reason header. + * We bind to CHANNEL_HANGUP_COMPLETE to capture sip_603plus_reason from + * the outbound leg before it is destroyed. + * + * Passthrough tests use a bridge scenario: originate -> middle extension + * (sets passthrough) -> bridges to 603+ target. The originate leg receives + * the response FROM the middle box, letting us verify what was actually sent. + */ + +#include +#include + +/* Event capture state */ + +static struct { + char sip_603plus_reason[1024]; + char sip_invite_failure_phrase[256]; + char sip_reason[1024]; + switch_bool_t received; +} capture; + +static void reset_capture(void) +{ + memset(capture.sip_603plus_reason, 0, sizeof(capture.sip_603plus_reason)); + memset(capture.sip_invite_failure_phrase, 0, sizeof(capture.sip_invite_failure_phrase)); + memset(capture.sip_reason, 0, sizeof(capture.sip_reason)); + capture.received = SWITCH_FALSE; +} + +static void on_hangup_complete(switch_event_t *event) +{ + const char *direction, *val; + + /* Only capture from outbound legs (the originating call, not the responder). + * In bridge tests, multiple outbound legs hang up (bridge B-leg, then originate O-leg). + * Reset on every outbound event so the last one (O-leg) wins cleanly. */ + direction = switch_event_get_header(event, "Call-Direction"); + if (zstr(direction) || strcmp(direction, "outbound")) return; + + reset_capture(); + + val = switch_event_get_header(event, "variable_sip_603plus_reason"); + if (!zstr(val)) { + switch_snprintf(capture.sip_603plus_reason, sizeof(capture.sip_603plus_reason), "%s", val); + } + + val = switch_event_get_header(event, "variable_sip_invite_failure_phrase"); + if (!zstr(val)) { + switch_snprintf(capture.sip_invite_failure_phrase, sizeof(capture.sip_invite_failure_phrase), "%s", val); + } + + val = switch_event_get_header(event, "variable_sip_reason"); + if (!zstr(val)) { + switch_snprintf(capture.sip_reason, sizeof(capture.sip_reason), "%s", val); + } + + capture.received = SWITCH_TRUE; +} + +static void originate_and_wait(const char *dest, switch_call_cause_t *cause) +{ + switch_core_session_t *session = NULL; + + switch_ivr_originate(NULL, &session, cause, + dest, 2, NULL, NULL, NULL, NULL, NULL, SOF_NONE, NULL, NULL); + + if (session) { + switch_channel_hangup(switch_core_session_get_channel(session), SWITCH_CAUSE_NORMAL_CLEARING); + switch_core_session_rwunlock(session); + } + + /* Let event dispatch thread deliver CHANNEL_HANGUP_COMPLETE */ + switch_yield(1000000); +} + +/* Test suite */ + +FST_CORE_EX_BEGIN("./conf", SCF_VG | SCF_USE_SQL) +{ +FST_MODULE_BEGIN(mod_sofia, sofia) +{ + FST_SETUP_BEGIN() + { + } + FST_SETUP_END() + + FST_TEARDOWN_BEGIN() + { + } + FST_TEARDOWN_END() + + /* Detection: positive cases */ + + FST_TEST_BEGIN(detect_valid_603plus_sip) + { + /* + * Extension +15553336050 sends: + * 603 Network Blocked + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Both conditions met -> sip_603plus_reason MUST be set. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336050", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set for valid 603+"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), "Failure phrase must be 'Network Blocked'"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_valid_603plus_q850) + { + /* + * Extension +15553336051 sends: + * 603 Network Blocked + * Reason: Q.850;cause=21;text="v=analytics1;url=https://example.com/redress";location=LN + * + * Q.850 protocol is equally valid per ATIS-1000099 section 4.1.1. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336051", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set for Q.850 ATIS Reason"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_603plus_after_180) + { + /* + * Extension +15553336056 sends 180 Ringing, waits 500ms, then: + * 603 Network Blocked + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Detection must work after provisional responses. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336056", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set after 180+603"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* Detection: negative cases */ + + FST_TEST_BEGIN(detect_wrong_phrase) + { + /* + * Extension +15553336052 sends: + * 603 Decline <- wrong phrase + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Phrase is "Decline", not "Network Blocked". Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336052", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set when phrase is 'Decline'"); + /* sip_reason should still be set (existing behavior for any Reason header) */ + fst_xcheck(!zstr_buf(capture.sip_reason), "sip_reason should be set regardless of phrase"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_no_analytics_in_reason) + { + /* + * Extension +15553336053 sends: + * 603 Network Blocked + * Reason: Q.850;cause=21;text="Call Rejected" <- no v=analytics1 + * + * Reason header lacks v=analytics1. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336053", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set without v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_no_reason_header) + { + /* + * Extension +15553336054 sends: + * 603 Network Blocked + * (no Reason header -- disable_q850_reason=true suppresses it) + * + * No Reason header -> sip->sip_reason is NULL. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336054", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set without Reason header"); + fst_xcheck(zstr_buf(capture.sip_reason), "sip_reason should not be set when Reason header is suppressed"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_non_603_status) + { + /* + * Extension +15553336055 sends: + * 486 Busy Here <- not 603 + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Status code is 486, not 603. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336055", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_USER_BUSY, "Expected USER_BUSY for 486"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set for non-603 status"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* + * Passthrough behavior. + * + * Bridge scenario: originate -> middle extension (sets passthrough) -> bridges to 603+ target. + * The originate leg receives the response FROM the middle box. We capture its + * sip_invite_failure_phrase and sip_reason to verify what was actually sent. + */ + + FST_TEST_BEGIN(passthrough_true) + { + /* + * Extension +15553336060 sets sip_603plus_passthrough=true, bridges to 603+ target. + * The middle box should forward both "Network Blocked" phrase and ATIS Reason header. + * Our originate leg should see a valid 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336060", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), + "passthrough=true must preserve 'Network Blocked' phrase"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), + "passthrough=true must result in valid 603+ on originate leg"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(passthrough_false) + { + /* + * Extension +15553336061 sets sip_603plus_passthrough=false, bridges to 603+ target. + * The middle box should strip the ATIS Reason and use default phrase "Decline". + * Our originate leg should NOT see a 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336061", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "passthrough=false must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), + "passthrough=false must strip ATIS Reason (no 603+ on originate leg)"); + fst_xcheck(zstr_buf(capture.sip_reason), + "passthrough=false must suppress Reason header entirely"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(passthrough_default) + { + /* + * Extension +15553336062 does NOT set sip_603plus_passthrough, bridges to 603+ target. + * Default: phrase is "Decline" (existing behavior), but ATIS Reason leaks through. + * This is the backward-compatible state. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336062", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "default passthrough must NOT change phrase (stays 'Decline')"); + /* ATIS Reason leaks through via sip_reason -- this is existing behavior */ + fst_xcheck(!zstr_buf(capture.sip_reason), + "default passthrough: sip_reason should still be set (existing behavior)"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* + * disable_q850_reason + passthrough combinations. + * + * Tests that disable_q850_reason and sip_603plus_passthrough work independently. + * disable_q850_reason suppresses standard Reason headers; + * sip_603plus_passthrough controls 603+ ATIS Reason forwarding. + */ + + FST_TEST_BEGIN(disable_reason_passthrough_true) + { + /* + * Extension +15553336063: disable_q850_reason=true + sip_603plus_passthrough=true. + * Standard Reason suppressed, but ATIS 603+ Reason restored. + * The customer use case: suppress all Reason headers except FCC-required 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336063", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), + "disable_q850+passthrough=true must preserve 'Network Blocked' phrase"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), + "disable_q850+passthrough=true must restore ATIS Reason"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(disable_reason_passthrough_false) + { + /* + * Extension +15553336064: disable_q850_reason=true + sip_603plus_passthrough=false. + * Both suppress -- no Reason header at all. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336064", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "disable_q850+passthrough=false must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_reason), + "disable_q850+passthrough=false must suppress Reason header entirely"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(disable_reason_passthrough_default) + { + /* + * Extension +15553336065: disable_q850_reason=true, passthrough not set. + * disable_q850_reason suppresses everything, passthrough not set = no override. + * No Reason header, phrase is "Decline". + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336065", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "disable_q850+default must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_reason), + "disable_q850+default must suppress Reason header"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + +} +FST_MODULE_END() +} +FST_CORE_END()