One of the great features of the Perimeta Session Border Controller (SBC) is the powerful message manipulation framework (MMF) that it provides. Using these rules you can make almost any change you like to a SIP message as it passes through the SBC, as well as block or reject messages based on certain criteria.
I’ve been working with these rules a lot recently, and to be honest, it’s taken me a while to get my head around how the configuration works. This article is my attempt to create a (relatively) concise guide for people getting started – and possibly for my future self next time I need a refresher!
This article assumes you understand the purpose of an SBC and are familiar with the SIP protocol.
This is unashamedly a technical article – you have been warned!
Big picture: MMFs and adjacencies

Before we get into the details, it’s important to understand where MMFs fit into the big picture. As shown in the diagram above, any SIP request that arrives at the Perimeta is associated with an adjacency on the inbound side (e.g. GenericAccess) and then after call policy routing is complete, the Perimeta routes that SIP request out through an outbound adjacency (e.g. MetasphereCFS).
Each adjacency may have some message manipulation rules associated with the adjacency (for inbound messages, outbound messages or both) – again as shown in the diagram.
Without worrying about the details of the MMF rules for now, let’s suppose that we have included the following in our adjacency configuration.
adjacency sip GenericAccess
interop
message-manipulation
edit-profiles inbound Rule1,Rule2
edit-profiles outbound Rule3
adjacency sip MetasphereCFS
interop
message-manipulation
edit-profiles inbound ""
edit-profiles outbound Rule4,Rule5,Rule6
What then, would be the impact on a new SIP INVITE request arriving at GenericAccess?
- As soon as it arrives at the GenericAccess adjacency, our INVITE message would be modified by Rule1 then Rule2 (which could potentially impact the routing decisions made by the Perimeta).
- The INVITE goes through the SBC routing, is assigned to exit via the MetasphereCFS adjacency, and just before it leaves the SBC Rule4 then Rule5 then Rule6 are applied to the request.
Alternatively, a SIP INVITE flowing in the other direction, arriving at the SBC from the core network, starts with the MetasphereCFS adjacency:
- In this case, no MMF rules are assigned to the MetasphereCFS adjacency on the inbound side, so the INVITE continues unchanged through call policy routing.
- However, just before leaving the SBC from the GenericAccess adjacency Rule3 is applied to the message.

What’s inside these Perimeta MMF rules?
So far, so good, but of course the challenging part of this topic is the rules themselves. How do we configure these rules to modify the SIP messages in the way that we want?
In the rest of this article we’ll look at the MMF rules required for a few different scenarios – gradually increasing in complexity. In the sections below we’ll cover the following situations.
- I want to modify the headers for every message that leaves this adjacency.
- I want to block or reject a message when certain conditions apply.
- I want to modify the headers when certain conditions apply
- I want to modify the headers when certain conditions apply – and my changes depend on the contents of the message.
Modifying headers for all SIP messages
For our first scenario, let’s suppose you want to make a change to all messages that pass through the Perimeta. This change is motivated by the desire to interoperate better with some external device, so we’ll apply the change on the outbound profile of the relevant adjacency.
We do this using a “header-profile”, which looks something like this.
sbc
signaling
sip message-manipulation
header-profile addUserAgent
description "add a user agent string to all messages"
header User-Agent
action add-first-header value "Metaswitch"
adjacency sip Adjacency1
interop
message-manipulation
edit-profiles outbound addUserAgent
Let’s walk through the settings here:
- header-profile is a configuration type that manipulates SIP headers
- addUserAgent is just the name we gave it
- description is purely for our benefit as the reader – must be in quotes
- header User-Agent specifies that we want to modify the User-Agent header. This could have been To or Contact or Require, etc.
- There are a bunch of possible actions we could use. add-first-header is an action that adds a value to a header but only if it doesn’t already have one. So this line adds the line “User-Agent: Metaswitch” to the SIP message if no User-Agent is already present.
Here are a few alternative actions that you could use.
action drop-request
! silently drops the message without responding to the sender
action strip
! removes this header from the SIP request
action pass
! allow the message to pass through
action replace-name value "New-Header-Name"
! changes the name of the header (e.g. from User-Agent to New-Header-Name)
action replace-value value "SIPclient"
! changes the value (e.g. User-Agent: X-Lite becomes User-Agent: SIPclient)
action modify-value sip-uri user "bob"
! if the header contains a SIP URI changes the user part to "bob"
! e.g. To: charlie@mydomain.com:5060 becomes To: bob@mydomain.com:5060
action modify-value sip-uri host "newdomain.com"
! if the header contains a SIP URI changes the hostto newdomain.com
! e.g. To: charlie@mydomain.com:5060 becomes To: charlie@newdomain.com:5060
action modify-value sip-uri port "5063"
! if the header contains a SIP URI changes the port to 5063
! e.g. To: bob@mydomain.com:5060 becomes To: bob@mydomain.com:5063
action store-vars
! Store information in a variable - we'll come back to this later.
The full list of actions can be found in the Perimeta Advanced Message Editing Guide in a section named “Configuring SIP header profiles”. You can find this document on Metaswitch Communities.
Modifying headers for certain methods only
The rules we applied above would modify the SIP headers for every single request that passes through the adjacency. Let’s imagine instead that we want to make the same change but only on REGISTER messages.
This is slightly more complicated, and requires that we use the method-profile as well as the header-profile.
sbc
signaling
sip message-manipulation
header-profile addUserAgent
description "add a user agent string to all messages"
header User-Agent
action add-first-header value "Metaswitch"
method-profile addUAtoRegister
description "add UA to REGISTER requests"
method REGISTER
header-profile addUserAgent
adjacency sip Adjacency1
interop
message-manipulation
edit-profiles outbound addUAtoRegister
If you compare this configuration snippet to what we had before, you’ll notice that two things have changed:
- I’ve added the new method-profile block in the middle.
- I changed the last line so that we apply the addUAtoRegister method-profile to the adjacency (rather than applying the header-profile).
So what’s going on with these changes?
- The method REGISTER line of the method-profile specifies that this rule applies only to REGISTER messages.
- The following line tells the Perimeta that we should apply the addUserAgent header-profile rule to these REGISTER messages.
- In the adjacency, we were previously applying the header-profile to all SIP requests exiting the adjacency, but now we’re referencing the method-profile instead. This has the effect of first filtering on SIP method, to select REGISTER messages, and then for REGISTER messages only the addUserAgent header-profile takes effect.
Quick Tip: If you’re trying to figure out the impact of some existing configuration, it’s a good idea to always start at the adjacency, and then follow the thread from there, to see which profiles are referenced from the adjacency, and then which sub-profiles are referenced from there.
This means it’s often easier to make sense of some configuration by starting at the end and working backwards.
Blocking or rejecting a request under certain conditions
To make things a little more complex, let’s see how we can add conditions to the MMF rule.
For example, let’s say you use two different SIP domains on your switch – one inside your network (e.g. on Calix access gear) and one for public access from outside the SBC – and for security reasons you want to block all REGISTER requests using one of the domains.
Let’s look at the configuration for that.
sip message-manipulation
header-profile BlockDomain
description "block requests with mydomain.com"
header "To"
action terminate-request response-code 403
condition advanced "(msg.first-header(\"To\").value CONTAINS \"mydomain.com\")"
method-profile BlockRegistersByDomain
description "block registrations on mydomain.com"
method REGISTER
header-profile BlockDomain
adjacency sip GenericAccess
interop
message-manipulation
edit-profiles inbound BlockRegistersByDomain
Let’s step through this configuration fragment so we can understand what it means.
- Starting at the bottom, notice that the adjacency refers to the method-profile.
- This means that we start by filtering on the request type, and in this case we only look at REGISTER messages.
- The method-profile then references the header-profile BlockDomain, and that has a couple of new things for us to examine.
- Firstly, we’re using a new action – terminate-request – which rejects the incoming message and sends back the error response 403 (in our case).
- But hang-on – we don’t want to terminate ALL register requests – and that’s where the condition line comes in.
I could potentially write a whole article just about conditions – this is where a lot of the complexity of MMFs comes into play – but I’ve tried to keep this relatively simple. So let’s break it down.
condition advanced "(msg.first-header(\"To\").value CONTAINS \"mydomain.com\")"
- condition advanced is how you start every condition. It basically means “only apply this rule if the following condition is true”. So in our case (in plain English) we want to say “only apply this rule if the To header contains mydomain.com”.
- The condition itself is enclosed in double-quotes, which then (confusingly) means we need to change any double-quotes inside the condition to have a backslash before the quote marks to avoid confusion! If we remove the quotes and the escaped quotes, the condition looks like this.
(msg.first-header("To").value CONTAINS "mydomain.com")
- We can use the msg.first-header(“HeaderName”).value format to inspect the contents of a specific header.
- The text says first-header because potentially there could be more than one header in the SIP message of a certain type (e.g. multiple Via headers) – and so we specify the first line that contains that header. In most cases it doesn’t make a difference because there’s only one To header anyway.
- The CONTAINS key word simply searches through a text string to find a second string. So A CONTAINS B will be true if the string B can be found somewhere in A.
- In our example, the following To header would be a match.
To: <sip:5102345678@mydomain.com>
You can make your condition filters as complex as you like, and Perimeta has a lot of built-in variables and operators (including regular expressions) to help you out.
I like the CONTAINS operator because it’s very simple to use, but you can read about all the alternatives in the Metaswitch Communities article “Condition strings and built-in variables for use in SIP header and action profiles” (part of the “Perimeta Advanced Message Editing Guide – CLI Users”).
One final note before we move on from this section, if you want to drop the message entirely, rather than sending back an error response, you would change the above code to use the drop-request action.
sip message-manipulation
header-profile BlockDomain
description "block requests with mydomain.com"
header "To"
action drop-request
condition advanced "(msg.first-header(\"To\").value CONTAINS \"mydomain.com\")"
method-profile BlockRegistersByDomain
description "block registrations on mydomain.com"
method REGISTER
header-profile BlockDomain
adjacency sip GenericAccess
interop
message-manipulation
edit-profiles inbound BlockRegistersByDomain
Modifying headers when conditions apply
For our next example let’s make things a little harder. We’ll still use a condition to decide when to apply our rule, but rather than simply rejecting it, we will change the message when the conditions apply.
Let’s suppose that a particular remote SIP device sends us messages that appear to come from two different IP address (1.2.3.4 and 5.6.7.8), and we want to modify the contact header so that all messages have the same IP address (1.2.3.4) in the header.
Here’s what the configuration looks like.
sip message-manipulation
header-profile FixContactIP
description "Replace 5.6.7.8 with 1.2.3.4"
header "Contact"
action modify-value sip-uri host "1.2.3.4"
condition advanced "(msg.first-header(\"Contact\").uri.sip_uri.host EQ \"5.6.7.8\")"
adjacency sip GenericAccess
interop
message-manipulation
edit-profiles inbound FixContactIP
Again, let’s step through this:
- Notice that we don’t have a method-profile this time. We’re referencing the header-profile directly in the adjacency, which means this header-profile will apply to all types of request (e.g. REGISTERs, INVITEs, NOTIFYs, etc).
- This time, our condition uses the “EQ” operator rather than “CONTAINS”. As you may guess “EQ” means equals – so this time we want to check if the sip_uri.host in the Contact header exactly matches 5.6.7.8. If we used contains we could make a mistake if we received a message from the IP address 25.6.7.88 (for example).
- Whereas before we just looked at the “.value” of the header, this time we’re using Perimeta’s built-in variables to describe the specific piece of the header we care about. A typical SIP URI looks like this: user@host:port and you can reference these specific parts of the URI through the “.uri.sip_uri.user”, “.uri.sip_uri.host” and “.uri.sip_uri.port” built-in variables.
- We could have looked at the whole header value and used CONTAINS, but that would have run the risk of a bad match (e.g. 25.6.7.88). Alternatively we could have used the REGEX operator to break apart the value ourselves, but the Perimeta has saved us the trouble.
- Finally, take a look at our action.
action modify-value sip-uri host "1.2.3.4"
- Just as Perimeta helps us specify a part of the header to examine, it also helps us if we want to modify just one part of a SIP URI. So in this case we can specify that we want to modify the host part of the SIP URI and simply provide the value for that section.
Modifying headers based on the contents of the message
For our final example, I’m going to take some inspiration from a thread in Communities, where users were discussing how to make the actual IP address of a registered SIP device visible in MetaView Explorer / MetaView Web. I’ve built on configuration shared in that thread for this example.
Typically, when you view registrations for a line they will always appear to come from the internal IP of the Perimeta SBC, which isn’t that useful – so these rules add a parameter to the Contact URI including the actual IP address.
One of the additional features used in this example is the idea of a stored variable. The Perimeta allows us to temporarily store information as we are processing a message, so that we can then use it later in our processing.
I’m going to split the configuration into two parts – in the first part we’ll actually add a temporary variable to our SIP REGISTER message as it enters the Perimeta.
sip message-manipulation
header-profile addRemoteIP
header "X-REMOTE-IP"
description "Add temporary header"
action add-first-header value ${msg.rmt_ip_addr}
method-profile addRegRemoteIP
method REGISTER
action pass
header-profile addRemoteIP
adjacency sip GenericAccess
interop
message-manipulation
edit-profiles inbound addRegRemoteIP
Most of this you’ve seen before – there are just a couple of new pieces.
- Firstly, from a design point of view, we’re adding a new SIP header to the message, named “X-Remote-IP”. This header is added to the message on the inbound adjacency as a way of saving off some information, and (as you’ll see shortly) we’ll actually remove it again when the request leaves the Perimeta.
- In order to add that header we use the action “add-first-header” to create a new header in the SIP request.
- The value of that header is the built-in variable ${msg.rmt_ip_addr}
The net result of this code is that an inbound REGISTER will get a new header that looks something like this.
X-Remote-IP: 1.2.3.4
Next-up, let’s take a look at the rules that use this header on the outbound side of the SBC, and put this IP address into the Contact header.
sip message-manipulation
header-profile addRmtIPtoContact
header "Store-Rule" entry 1
description "save the remote IP value"
action store-vars
condition advanced "(STORE (RemoteIP, msg.first-header(\"X-REMOTE-IP\").value))"
header "Store-Rule" entry 2
description "Split the contact header into two parts"
action store-vars
condition advanced "(REGEX (msg.first-header(\"Contact\").value, '([^>]*)(>.*)', ContactStart, ContactEnd))"
header Contact
description "Add rmt= parameter"
action replace-value value ${ContactStart};rmt=${RemoteIP}${ContactEnd}
condition advanced "((DEFINED (ContactStart)) AND (DEFINED (RemoteIP)))"
header X-REMOTE-IP
action strip
method-profile addIPtoContact
method REGISTER
action pass
header-profile addRmtIPtoContact
adjacency sip MetasphereCFS
interop
message-manipulation
edit-profiles outbound addIPtoContact
As you can see, we’ve saved the ‘best’ for last. There’s a lot going on here, but I’ll help you through it.
- As always, let’s start at the end, and notice that the adjacency is using the addIPtoContact rule, which is a method-profile.
- This method-profile applies to all REGISTER messages, and applies the addRmtIPtoContact header-profile.
- Notice that this header-profile has four different “headers” in it. The Perimeta has rules that control what order these are processed (it’s not the order the appear) – but the main thing to remember is that headers with action store-vars are dealt with first.
- So now we encounter Store-Rules for the first time. These are rules with an action of “store-vars” which don’t act on any specific header (hence the fake header name Store-Rule). These are used to save values from the message into variables that we can use later.
- Confusingly the “condition advanced” line of the rule doesn’t actually impose any conditions in this case – it’s instead used to define what variables are stored.
- In Store-Rule entry 1 we use the following “condition”.
"(STORE (RemoteIP, msg.first-header(\"X-REMOTE-IP\").value))"
- The STORE operator simply creates a variable and then assigns it a value. In this case we create a variable named RemoteIP and give it the value of the X-REMOTE-IP header added on the inbound side of the SBC. So perhaps RemoteIP = 1.2.3.4 (for example).
- Now let’s take a look at the “condition” in the second Store-Rule (note that entry 1 and entry 2 define the order these are processed).
"(REGEX (msg.first-header(\"Contact\").value, '([^>]*)(>.*)', ContactStart, ContactEnd))"
- This time we’re using the REGEX (regular expression) operator to store variables, rather than the more straight-forward STORE operator.
- If you’ve never used REGEX before then this will all look very strange. It’s beyond the scope of this article for me to explain it, but you can start learning at Regular-Expressions.info.
- This particular instruction takes a look at the value of the Contact header and applies this regular expression to it: ([^>]*)(>.*)
- The first part [^>]* matches a string that starts at the beginning of the value, and matches every character until it finds a “>”. This text is stored in a variable named ContactStart.
- The second part >.* matches a string that starts with the first > in the Contact header value, and continues until the end of the value. This text is stored in a variable named ContactEnd.
- In other words, if we take a typical Contact header then it will get split into two parts, based on the > character.
Contact: <sip:5102345678@192.168.5.5:49234>;isup-oli=00
ContactStart = "<sip:5102345678@192.168.5.5:49234"
ContactEnd = ">;isup-oli=00"
RemoteIP = 1.2.3.4
Now that we’ve stored these variables, we’re finally ready to look at the third part of the header-profile.
header Contact
description "Add rmt= parameter"
action replace-value value ${ContactStart};rmt=${RemoteIP}${ContactEnd}
condition advanced "((DEFINED (ContactStart)) AND (DEFINED (RemoteIP)))"
Let’s start with the condition advanced piece:
- It’s structured as a series of tests, which both need to be true: X AND Y.
- These tests use the DEFINED (X) operator to check whether we have assigned any values to ContactStart and RemoteIP. If not then either no remote IP was stored on the inbound side of the SBC or else there’s no contact header – in which case there’s no point doing anything else.
If ContactStart is missing or RemoteIP is missing then the DEFINED test will fail, and nothing will happen to the Contact header. However, if they’re both present then the Perimeta will process the replace-value action.
The replace-value action replaces the entire value of a header. So we scrap everything that was there before and start with a clean slate.
Then the Perimeta joins together our three variables, plus a little extra text in the middle:
Contact: ContactStart + ";rmt=" + RemoteIP + ContactEnd
in other words...
Contact: "<sip:5102345678@192.168.5.5:49234" + ";rmt=" + "1.2.3.4" + ">;isup-oli=00"
in other words...
Contact: <sip:5102345678@192.168.5.5:49234;rmt=1.2.3.4>;isup-oli=00
At this point we have successfully transferred the remote IP address that sent this message into a new rmt parameter in the Contact header. Phew!
But we’re not (quite) done yet. There’s one final header rule to process.
header X-REMOTE-IP
action strip
To finish, we remove the temporary X-REMOTE-IP header we added on the inbound adjacency, so it doesn’t leak out into the core network. Not that it really matters, but if we’re creating the header for internal use it’s good practice to strip it off again so no external device ever sees it.
Recap
At this point I’m sure you’re pretty exhausted, but you should pat yourself on the back, because we’ve covered a lot of ground. As a reminder, you have learned about:
- method-profiles
- header-profiles
- actions that reject requests, drop requests, add new values and modify values
- storing data in headers
- storing data in variables
- how to match all methods of a certain type
- how to match only certain requests based on data in the headers themselves.
Lengthy though this article has been, we also skipped a lot. I’ve focused on header and method profiles, as 95% of the time that’s what you’ll need – but there are also profiles that focus on parameters, options and even message bodies.
If you want to learn more, the Perimeta Advanced Message Editing Guide is a riveting read and with 226 pages you can keep on learning and developing your skills for months before you run out of material.
Alternatively, if you need Perimeta help and would rather just hire us to take care of it for you, please send us a message and we can set up a call.