SIP Bridge

Last modified by Félix Olart on 2024/06/20 18:49

 

Available since Flexisip 2.2, configuration changed in Flexisip 2.4

The sip-bridge mode for the B2BUA allows bridging calls to other SIP domains and proxies (a.k.a. SIP trunking).

Enabling the SIP bridge mode

To use the B2BUA in SIP bridge mode, you must first set the application type to sip-bridge in the [b2bua-server] section. Then set the providers file in the [b2bua-server::sip-bridge] section.

[b2bua-server]
application=sip-bridge

# Note that the following field is ignored while operating in sip-bridge mode
#outbound-proxy=ignored

# ...

[b2bua-server::sip-bridge]
providers=/path/to/your/providers-file.json

Dedicated JSON configuration file

The sip-bridge mode uses a separate configuration file in JSON format to define options more deeply nested than what is (currently) possible in the main INI-like configuration file.

Full config example

Let's start with an example configuration file for an example use-case. It's alright if everything is not obvious at first glance, this example is only meant as an overview and quick reference. The next sections of this page will provide you with detailed information on all available fields.

In this example we configure bi-directional bridging between a domain managed by a Flexisip proxy ("flexisip.example.org") and another domain managed by an external imaginary proxy(ies) that we'll call "Jabiru" ("jabiru.example.org"). Each account on the Flexisip proxy will have one (and only one) corresponding account on the Jabiru proxy.

We configure 2 "Providers", one for each direction, such that a user registered to the Flexisip proxy can call a user registered to the Jabiru proxy (Outbound), and vice-versa (Inbound, i.e. a user registered to the Jabiru proxy can call a user registered to the Flexisip proxy).

For each user mapped between the 2 domains, we'll need to provide the B2BUA with 2 URIs: The URI it will use to authenticate on the external Jabiru domain ("uri" field of the Account type), and its identity URI on the Flexisip domain ("alias" field of the Account type). This is configured in the AccountPool object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
 "schemaVersion": 2,
 "providers": [
    {
     "name": "Flexisip -> Jabiru (Outbound)",
     "triggerCondition": { "strategy": "Always" },
     "accountToUse": {
       "strategy": "FindInPool",
       "source": "{from}",
       "by": "alias"
      },
     "onAccountNotFound": "nextProvider",
     "outgoingInvite": {
       "to": "sip:{incoming.to.user}@{account.uri.hostport}{incoming.to.uriParameters}",
       "from": "{account.uri}"
      },
     "accountPool": "JabiruAccountPool"
    }, {
     "name": "Jabiru -> Flexisip (Inbound)",
     "triggerCondition": { "strategy": "Always" },
     "accountToUse": {
       "strategy": "FindInPool",
       "source": "{to}",
       "by": "uri"
      },
     "onAccountNotFound": "nextProvider",
     "outgoingInvite": {
       "to": "{account.alias}",
       "from": "sip:{incoming.from.user}@{account.alias.hostport}{incoming.from.uriParameters}",
       "outboundProxy": "<sip:flexisip.example.org;transport=tcp>"
      },
     "accountPool": "JabiruAccountPool"
    }
  ],
 "accountPools": {
   "JabiruAccountPool": {
     "outboundProxy": "<sip:jabiru.example.org;transport=tls>",
     "registrationRequired": true,
     "maxCallsPerLine": 20,
     "loader": {
       "dbBackend": "sqlite3",
       "initQuery": "SELECT username, domain AS hostport, \"\" AS user_id, \"clrtxt\" AS secret_type, password AS secret, username AS alias_username, \"flexisip.example.org\" AS alias_hostport, \"\" AS outbound_proxy FROM users",
       "updateQuery": "SELECT username, domain AS hostport, \"\" AS user_id, \"clrtxt\" AS secret_type, password AS secret, username AS alias_username, \"flexisip.example.org\" AS alias_hostport, \"\" AS outbound_proxy FROM users WHERE pk = :identifier",
       "connection": "db=sip_accounts user='flexisip-b2bua' password='Mellon' host=db.jabiru.example.org"
      }
    }
  }
}

Schema definition

Below is a comprehensive description of the JSON schema of this configuration file, using TypeScript syntax.

type Root = {
 // Used to indicate which fields the config parser should expect in this file.
 schemaVersion: 2;
 // Providers will be attempted, in order, until one handles the call (bridging or declining it),
 // or there is none left
 providers: Array<Provider>;
 // Accounts are gathered in pools, which can be shared between providers
 accountPools: {
    [key: string]: AccountPool;
  };
};

type Provider = {
 // Human-friendly name for printing. This is a free field, you may put anything you like in it.
 name: string;
 // Name of the pool to use. Must match one of the pools configured in `accountPools`.
 accountPool: string;
 // Whether or not this provider should handle a call.
 // If this evaluates to false, the next provider in the array will be attempted.
 triggerCondition: MatchRegex | Always;
 // How to choose the account that will be used to bridge the call
 accountToUse: Random | FindInPool;
 // What to do if the previous step couldn't find a suitable account.
 // "nextProvider" will resume the search in the provider array
 // Note that if a suitable account is found, but is not available
 // (failed to register, or currently used in too many calls), the B2BUA will decline the call,
 // regardless of the value set here.
 onAccountNotFound: "nextProvider" | "decline";
 // Configure the invite that the B2BUA will send to build the other leg of the bridge (legB).
 outgoingInvite: OutgoingInvite;
};

// Triggers if the incoming call matches the given regex.
type MatchRegex = {
  strategy: "MatchRegex";
  pattern: RegularExpression;
 // Currently ignored, the regex will always be applied on the request URI of the incoming INVITE
 source: string;
};

// An ECMAScript regular expression as provided by C++'s `std::regex` type
// https://en.cppreference.com/w/cpp/regex/ecmascript
type RegularExpression = string;

// This provider will always handle incoming calls.
// This is useful in combination with the FindInPool strategy for the `accountToUse`
// and "nextProvider" behaviour for `onAccountNotFound`
type Always = {
  strategy: "Always";
};

// Pick an account at random in the pool to handle the call.
// This strategy only picks available accounts, so the returned account (if any) is guaranteed to be available
type Random = {
  strategy: "Random";
};

// Given all the accounts in the pool, will choose the first one for which the field indicated by `by` matches the
// `source` string
type FindInPool = {
  strategy: "FindInPool";
 // See the Account type.
 by: "uri" | "alias";
 // Available fields are those of the incoming call.
 // See the "Call" sub-section of the Template Strings section below.
 source: TemplateString;
};

// A template string with variable substitution. See the "Template Strings" section below.
type TemplateString = string;

type OutgoingInvite = {
 // Available fields are "incoming" or "account".
 // See the "OutgoingInvite" sub-section of the Template Strings section below.
 to: TemplateString;
 // Same as above
 from?: TemplateString;
 // Override the outbound proxy configured in the account.
 outboundProxy?: SipUri;
 // Override the AVPF toggle of the B2BUA
 enableAvpf?: boolean;
 // Override the media encryption setting of the B2BUA
 mediaEncryption?: MediaEncryption;
};

// A SIP URI as defined in RFC 3261. E.g. "sip:Sita@example.org"
type SipUri = string;

type MediaEncryption = "zrtp" | "sdes" | "dtls-srtp" | "none";

type AccountPool = {
 // The proxy to which all the accounts of the pool will attempt to connect and send INVITEs.
 // This can be overridden on a per-account, or per-INVITE basis.
 outboundProxy: SipUri;
 // Whether the accounts should attempt to register to their outbound proxy(ies).
 registrationRequired: boolean;
 // New in version 2.4.0-beta-28-g6444ab02.
 // The number of milliseconds to wait between two REGISTER requests when registering the accounts of the pool.
 // This can help in case the external proxy implements some kind of rate-limiting.
 // This is a lower bound: The actual delay observed between two requests may be higher due to processing time.
 // Also note that this rate-limiting only applies to the initial registration process.
 // If, for example, the B2BUA notices that the connexion has been severed (which can happen over TCP or TLS),
 // then this rate-limiting will NOT be respected when it attempts to re-register its accounts.
 registrationThrottlingRateMs?: number;
 // New in version 2.4.0-beta-36-gdb9584b8.
 // Whether the accounts should unregister to their outbound proxy(ies) on server shutdown. Default value is true.
 unregisterOnServerShutdown?: boolean;
 // Maximum number of calls a given account will be allowed to bridge concurrently.
 // Any new INVITE received while the account is currently used in that many calls, will be declined by the B2BUA.
 maxCallsPerLine: number;
 // The loader provides the accounts to the pool.
 // It may either be a static array provided directly in this configuration file, or a configuration to load
 // (and update) those accounts from an SQL database.
 loader: SqlLoader | Array<Account>;
};

type SqlLoader = {
  dbBackend: "mysql" | "sqlite3";
  initQuery: SqlQuery;
 // This query must also make use of an ":identifier" placeholder which will be replaced with the identifier
 // of the account to update, as provided in the Redis notification.
 updateQuery: SqlQuery;
 // Backend-dependent connection string. E.g. for "mysql", it will look like
 // "db=accounts user='b2bua' password='correct horse' host=database.example.org"
 connection: string;
};

// See the "SQL Query" section below
type SqlQuery = string;

type Account = {
  uri: SipUri;
 // This field is useful when the id used for authentication should be different from the username.
 userid?: string;
 // This indicates how to interpret the payload in the "secret" field below. Default is "ha1".
 secretType?: "ha1" | "clrtxt";
 // This can either be a clear-text password or an HA1 digest, as indicated in the "secretType" above,
 // and will be used for authentication.
 secret?: string;
  alias?: SipUri;
 // Override the outbound proxy defined by the pool
 outboundProxy?: SipUri;
};

Template Strings

Template strings provide you with a syntax for variable substitution to let you compose a SIP URI in a flexible way. Variables are enclosed in curly brackets ('{}') and fields are accessed with dots ('.'). This lets you do simple things like the following:

"{incoming.to}"

Which will simply be replaced by the value of the "To" header from the incoming call.

Or more complex things:

"sip:{incoming.from.user}@{account.alias.hostport}{incoming.from.uriParameters}"

(That is equivalent to replacing the host and port of the "From" header of the incoming call)

Following are the variables available for substitution, along with their sub-fields. Unless otherwise mentioned, a variable/field is not representable on its own, and you should access its sub-fields.

OutgoingInvite

Name

Type
accountAccount
incomingCall

Call

NameType
toSIP URI
fromSIP URI
requestAddressSIP URI

Account

NameType
sipIdentitySIP URI
alias

SIP URI

SIP URI

A SIP URI is itself representable as a string (and looks like e.g. "sip:user@example.org:3125;transport=tcp") so you can use it as-is in a template string (e.g. "{incoming.to}" is valid), but you may also access its parts as fields (e.g. "sip:{incoming.from.user}@example.org")

NameType
user

string

hostportstring
uriParametersstring

SQL Query

The queries configured in the SqlLoader let you plug the B2BUA to your own database to retrieve and update the necessary info to populate its account pool(s). Access will only ever be read-only, under no circumstances will the B2BUA perform any write operation.

The query may be arbitrarily complex but MUST return a table with the following columns:

All columns are strings and MUST be present in the returned table. Required fields are highlighted in bold, the rest can be stubbed with empty strings ("").

NameDescription
usernameThose two fields will be used to build the "uri" field of the Account object. The naming follows that of the RFC 3261 on SIP URIs, but in common scenarii "hostport" will only ever be mapped to the domain, without port.
hostport

user_id

This field is useful when the id used for authentication should be different from the username. An empty string ("") indicates that the username should be used for authentication.
secret_typeEither "ha1" or "clrtxt". This indicates how to interpret the payload in the "secret" field below. Default is "ha1".
secretThis can either be a clear-text password or an HA1 digest, as indicated in the "secret_type" above, and will be used for authentication.
alias_usernameThose two fields will be used to build the "alias" field of the Account object
alias_hostport
outbound_proxyOverride the outbound proxy defined by the pool

Additional configuration examples

PSTN fallback service

This configuration (not exactly, but something close to it) could be used to provide one-way call bridging to a PSTN gateway as a fallback way to contact your users. Given the appropriate configuration on the Flexisip proxy, your users could register their phone number as a secondary contact to receive calls on that number if they do not answer on their SIP client (app), or if it's unavailable.

This example assumes you purchased (or otherwise obtained) two accounts from a cloud service provider offering a virtual telephone service to which you can register via SIP. Those two accounts are statically setup in the "FrenchPSTN" account pool, and each will be used at random to bridge calls received by the B2BUA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
 "schemaVersion": 2,
 "providers": [
    {
     "name": "French telephone numbers",
     "triggerCondition": {
       "strategy": "MatchRegex",
       "source": "{requestUri}",
       "pattern": "+33.*"
      },
     "accountToUse": { "strategy": "Random" },
     "onAccountNotFound": "decline",
     "outgoingInvite": {
       "to": "sip:{incoming.requestUri.user}@{account.uri.hostport}{incoming.requestUri.uriParameters}"
      },
     "accountPool": "FrenchPSTN"
    }
  ],
 "accountPools": {
   "FrenchPSTN": {
     "outboundProxy": "<sip:some.provider.example.org;transport=tls>",
     "registrationRequired": true,
     "maxCallsPerLine": 682,
     "loader": [{
       "uri": "sip:account1@some.provider.example.com",
       "userid": "titdkaLSjWAoYPCt",
       "secretType": "clrtxt",
       "secret": "|e_Zn+@Oo,LI8W3@4b'd"
      },{
       "uri": "sip:account2@some.provider.example.com",
       "userid": "hrVdbsADrCUkTjdE",
       "secretType": "clrtxt",
       "secret": "%AMC%Vxn8_7/y\5!k!oM"
      }]
    }
  }
}

Additionally, the proxy server must route the calls to a b2bua server to enable call interception. This can be done using the routes configuration of the Forward module of the proxy server. For example:

[module::Forward]
enabled=true
routes-config-path=/etc/flexisip/routes.conf

In routes.conf:

<sip:127.0.0.1:6667;transport=tcp>     (request.uri.domain == 'example.net' && request.uri.params contains 'user=phone')

This example will cause all requests targeted to a domain example.net and containing a user=phone parameter to be forwarded to sip:127.0.0.1:6067 (which is the configured listening address of the b2bua server; see below).

See the filter syntax page for more details about the way to write matching expressions.

Configuring fallback timeout

If you're using the B2BUA SIP trunking feature as a fallback mechanism (using low-priority contacts with q<1), then you might want to configure how long it takes before un-answered calls are forwarded to the B2BUA for bridging.

This delay can be configured by adjusting the call-fork-current-branches-timeout setting in the [module::Router] section. (The priority-based forking mechanisms are unrelated to the B2BUA feature)

Runtime information with the CLI

 When configured in sip-bridge mode, the B2BUA can provide information on the state of the SIP bridge via the following command:

$ flexisip_cli.py --server b2bua SIP_BRIDGE INFO
{
       "providers" :
       [
               {
                       "accounts" :
                       [
                               {
                                       "address" : "sip:account1@some.provider.example.com",
                                       "freeSlots" : 173,
                                       "registerEnabled" : true,
                                       "status" : "OK"
                               }
                       ],
                       "name" : "Example provider 1"
               },
               {
                       "accounts" :
                       [
                               {
                                       "address" : "sip:account1@other.provider.example.com",
                                       "freeSlots" : 96,
                                       "registerEnabled" : false,
                                       "status" : "OK"
                               }
                       ],
                       "name" : "Example provider 2"
               }
       ]
}