Here's the analysis and fix plan:
Root Cause Analysis
Problem: External inbound call → Forward All enabled → transfer() to external destination → zero RTP both directions.
Why it happens: The dialplan 515_call-forward-all.xml uses transfer() which reuses the same SIP channel and sends it back through CS_ROUTING. On an external carrier SIP dialog, this resets the media state machine. The A-leg (carrier) had its SDP consumed during the first dialplan pass, but after transfer(), the channel never properly transitions back to CS_EXCHANGE_MEDIA with RTP flowing. The B-leg (gateway) negotiates fine, the bridge "looks" connected (both sides answer), but the kernel-level RTP sessions inside FreeSWITCH never activate.
Why internal calls work: When both legs are local SIP registrations (FS controls both endpoints), the media bridge initialization after transfer() works fine because FS has complete control over both endpoints' media sessions.
Why Follow Me works: Follow Me uses bridge() in Lua (520_follow-me-destinations.xml → app.lua follow_me → session:execute("bridge", ...)). The A-leg stays in CS_EXCHANGE_MEDIA throughout — no channel state machine reset.
Fix Plan
Phase 1: Verify the pcap (quick check)
Recapture with tcpdump -i any -s 0 -w /tmp/forward_all_test.pcap port 5060 or port 5065 or port 27792 or port 22872 to confirm RTP truly isn't flowing (the existing pcap may use a port filter).
Phase 2: Apply fix to 515_call-forward-all.xml
Primary fix: Enable answer before transfer for the inbound case. This establishes the media path (sends 200 OK to carrier) before the channel re-enters routing.
File: /var/www/fusionpbx/app/dialplans/resources/switch/conf/dialplan/515_call-forward-all.xml
Change line 16 from:
<action application="answer" data="" enabled="false"/>
to:
<action application="answer" data="" enabled="true"/>
Secondary change: Enable the sip_h_Diversion header (line 20) so the carrier sees proper diversion info:
<action application="set" data="sip_h_Diversion=<sip:${caller_id_number}@${domain_name}>;reason=unconditional" enabled="true"/>
Phase 3: Update DB + reload
1. Save the changes (they're in the FusionPBX source tree)
2. Update the DB record: v_dialplans.dialplan_xml must be synced OR the change needs to be written through v_dialplan_details
3. Clear cache: rm -rf /var/cache/fusionpbx/*
4. Reload: event_socket api reloadxml
5. Test
Fallback (if answer+transfer doesn't work)
Replace transfer() with a bridge() to loopback/${forward_all_destination}/${domain_name}, keeping the A-leg alive (same pattern as Follow Me's enterprise strategy). This is architecturally cleaner but requires constructing the correct bridge dial string with proper hangup_after_bridge=true / continue_on_fail=true.
Thanks for taking the time to analyse this. I went through your suggestions and did some more testing.
answer() before transfer()
I modified the inbound Call Forward All dialplan to answer the call before the transfer:
<condition field="${call_direction}" expression="^inbound$" break="on-true">
<action application="answer"/>
<action application="set" data="outbound_caller_id_name=${caller_id_name}" inline="true"/>
<action application="set" data="outbound_caller_id_number=${caller_id_number}" inline="true"/>
<action application="transfer" data="${forward_all_destination} XML ${domain_name}"/>
</condition>
Unfortunately, this did not change the behaviour. The call still connects successfully but there is no audio.
Additional investigation
I spent some more time comparing the working and non-working call flows.
The following scenarios work:
- Internal extension → Call Forward All → External number
- External → Extension (0 second timeout) → Forward → External number
- External → Follow Me with confirmation enabled
The following do
not work:
- External → Call Forward All → External number
- External → Call Forward No Answer → External number
- External → Follow Me without confirmation
So the issue only seems to affect
immediate forwarding of an inbound external call.
Follow Me implementation
I also looked at how Follow Me is implemented. The XML simply hands the call to Lua:
<action application="lua" data="app.lua follow_me"/>
The Lua implementation does
not use transfer(). For external destinations it builds a loopback route:
route_bridge = 'loopback/' .. destination_number
and later executes:
session:execute("bridge", app_data)
where app_data includes variables such as caller ID, accountcode, domain information, etc. It also sets:
hangup_after_bridge=true
continue_on_fail=true
before executing the bridge.
Channel comparison
Comparing the active channels was also interesting. A failing
Call Forward All call creates only two channels:
Carrier -> FreeSWITCH -> Gateway
A working
Follow Me call creates four channels:
Carrier -> A-leg -> loopback/xxxx-a -> loopback/xxxx-b -> Gateway
This makes me think the important difference may not be bridge() vs transfer() alone, but the fact that Follow Me creates a new loopback channel before routing the call back out.
Based on this, I think your fallback suggestion may actually be the more interesting direction to investigate.
Have you implemented a loopback-based Call Forward All before, or do you have an example of the correct bridge string to replace the current transfer() implementation while preserving the normal outbound routing and channel variables?