Manipulating SIP headers

This week’s #FridayFun is a deep dive into SIP headers in TangoPBX. This is something I’ve been meaning to write up for years, but have been putting it off due to all the different permutations of fpbx and Asterisk versions. With FPBX 17, we no longer have to think about macros or chan_sip, so the task is simplified by focusing on TangoPBX with pjsip channels only. There are references here to dialplan hooks, where are explained in detail in another thread: Hooking Like a Pro

What’s a SIP header

Starting with just a pjsip extension registered to your system, you make a call. For the first leg, the phone sends a SIP INVITE to the PBX, and the PBX responds. The two sides exchange SIP packets until the call is finally established. Each of of these SIP packets has headers of course, but the focus today is only on the SIP headers in the SIP INVITE. So if I launch sngrep and dial the speaking clock feature code from a local extension, the INVITE looks something like this (lines removed for clarity):

INVITE sip:*60@redacted.com SIP/2.0
Max-Forwards: 20
Via: SIP/2.0/UDP 173.X.X.100:5060;rport;branch=z9hG4bK325014650
From: <sip:4002@redacted.com>;tag=2084277858
To: <sip:*60@redacted.com>
Call-ID: 766301168@redacted.com
CSeq: 21 INVITE
User-Agent: YATE/5.5.0
Contact: <sip:4002@173.X.X.100:5060>
Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO
Content-Type: application/sdp
Content-Length: 506

The first line is the request line or request URI, and the lines that follow are the SIP headers. Usually we don’t have to touch these, but there are times when you need to send specifically formatted headers to a SIP provider for billing, or you may have SIP clients that need custom headers to perform actions such as auto-answer or for push notifications, etc.

How channels work in Asterisk

Above I said “first leg” of a call. As Asterisk is a back to back user agent (as opposed to a SIP proxy), when I dial another party from my desk phone, the first leg of the call is a PJSIP channel between my desk phone and the PBX, the second leg is setup by Asterisk between the PBX and the destination, possibly another local extension, possibly an external number through a SIP provider. Ultimately Asterisk bridges the two channels so we can talk. When you have sngrep open, you can see the individual channels that make up the call, and browse the headers for each. We are concerned with making the SIP INVITE headers for the B leg of the call and that is the focus of this post

Adding headers

If you have need to add a SIP header that is not supported by the GUI, the best way is to use a dialplan hook and the native fpbx subroutine. In fpbx 17, the header subroutine looks like this

TangoPBX*CLI> dialplan show func-set-sipheader
[ Context 'func-set-sipheader' created by 'pbx_config' ]
  's' =>            1. Noop(Sip Add Header function called. Adding ${ARG1} = ${ARG2}) [extensions_additional.conf:5198]
                    2. Set(HASH(__SIPHEADERS,${ARG1})=${ARG2})    [extensions_additional.conf:5199]
                    3. Return()                                   [extensions_additional.conf:5200]

It’s very simple. This is a subroutine that accepts two arguments. ARG1 is the header name, ARG2 is the header value. Very conveniently, this sub can be called at any time in the call flow. The two new arguments passed to the subroutine are added the SIPHEADERS channel variable along with any headers that may have been set previously. When it finally comes time to apply all the headers to the SIP INVITE in the B leg, another subroutine is called, func-apply-sipheaders.

As it happens my friend Rick runs a SIP provider that has a unique requirement. I am required to agree to their TOS, not just once on account setup, but every single time I make a call. So I have this dialplan hook defined on my system:

[macro-dialout-trunk-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
exten => s,n,GoSub(func-set-sipheader,s,1(X-Give-you-up,Never))
exten => s,n,GoSub(func-set-sipheader,s,1(X-Let-you-down,Never))
exten => s,n,Return

Note that func-set-sipheader is an Asterisk subroutine, so args must be passed separated by a comma. If you’re trying to adapt Asterisk documentation to this method, a pitfall I’ve seen more than once is putting = sign between the header name and value. Don’t do this!

With the above dialplan hook in place, whenever I make an outbound call, I can see the INVITE in sngrep with the two headers defined as above:

INVITE sip:1920xxxxxx5@sip.astley.com:5060 SIP/2.0
Via: SIP/2.0/UDP 137.X.X.34:5060;rport;branch=z9hG4bKPj571d1cc9-3746-4c7c-9f77-8681a69238e9
From: <sip:920yyyyyy9@137.X.X.34>;tag=9d4a3f58-0df8-45b2-a7e7-89041ca53aad
To: <sip:1920xxxxxx5@sip.astley.com.com>
Contact: <sip:asterisk@137.X.X.34:5060>
Call-ID: c7079c7a-7971-4409-8495-a416ad87085c
CSeq: 28316 INVITE
Allow: OPTIONS, INVITE, ACK, BYE, CANCEL, UPDATE, PRACK, REGISTER, SUBSCRIBE, NOTIFY, PUBLISH, MESSAGE, INFO, REFER
Supported: 100rel, timer, replaces, norefersub, histinfo
Session-Expires: 1800
Min-SE: 90

X-Let-you-down: Never                     <----- Custom Header
X-Give-you-up: Never                      <----- Custom Header

P-Asserted-Identity: <sip:920yyyyyy9@137.X.X.34>
Remote-Party-ID: <sip:920yyyyyy9@137.X.X.34>;party=calling;privacy=off;screen=no
Max-Forwards: 70
Content-Type: application/sdp
Content-Length:   341

Removing headers

There are use cases for removing SIP headers as well. You might be using custom headers to track things internally, but may need to remove them before a call goes to a trunk. The subroutine that does the header manipulation before the B leg channel is set up, has a check for ‘unset’ (all lower case, no quotes) which will ensure the header is not set. If I modify my custom dialplan hook from above to look like this:

[macro-dialout-trunk-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
exten => s,n,GoSub(func-set-sipheader,s,1(X-Give-you-up,Never))
exten => s,n,GoSub(func-set-sipheader,s,1(X-Let-you-down,Never))
exten => s,n,GoSub(func-set-sipheader,s,1(X-Let-you-down,unset))
exten => s,n,Return

The above unsets the “X-Let-you-down” header that was set in the previous line. When I make an outbound call, I can watch the asterisk full log and see what’s happening (lines removed for clarity):

root@TangoPBX:~# tail -f /var/log/asterisk/full | grep PJSIP_HEADER

[2025-06-20 11:28:51] VERBOSE[2836545][C-00000068] pbx.c: Executing [s@func-apply-sipheaders:11] ExecIf("PJSIP/1.us-central.clearlyip.com-00000138", "1?Set(PJSIP_HEADER(remove,X-Let-you-down)=)") in new stack
[2025-06-20 11:28:51] VERBOSE[2836545][C-00000068] pbx.c: Executing [s@func-apply-sipheaders:14] ExecIf("PJSIP/1.us-central.clearlyip.com-00000138", "1?Set(PJSIP_HEADER(add,X-Give-you-up)=Never)") in new stack

which ultimately resuts in an INVITE to the provider with only a single custom header defined

INVITE sip:1920xxxxxx5@sip.astley.com:5060 SIP/2.0
Via: SIP/2.0/UDP 137.X.X.34:5060;rport;branch=z9hG4bKPjb76b4e8f-e41a-4c36-be23-bd1fe4a0bf31
From: <sip:920yyyyyy9@137.X.X.34>;tag=01f05e2f-8d94-4d6f-b58c-84d932611e5d
To: <sip:1920xxxxxx5@sip.astley.com.com>
Contact: <sip:asterisk@137.X.X.34:5060>
Call-ID: ed8c09fe-63e3-4334-918c-d792fccb3c7d
CSeq: 29535 INVITE
Allow: OPTIONS, INVITE, ACK, BYE, CANCEL, UPDATE, PRACK, REGISTER, SUBSCRIBE, NOTIFY, PUBLISH, MESSAGE, INFO, REFER
Supported: 100rel, timer, replaces, norefersub, histinfo
Session-Expires: 1800
Min-SE: 90

X-Give-you-up: Never                     <----- Custom Header

P-Asserted-Identity: <sip:920yyyyyy9@137.X.X.34>
Remote-Party-ID: <sip:920yyyyyy9@137.X.X.34>;party=calling;privacy=off;screen=no
Max-Forwards: 70
User-Agent: FPBX-17.0.19.28(20.13.0)
Content-Type: application/sdp
Content-Length:   341

That’s all for this week. Happy weekend all!

8 Likes