# chan_sip to PJSIP Migration: sip_to_pjsip.py, Option Mapping, Tests

Source: https://srvscripts.com/guides/chan-sip-to-pjsip-migration/
Updated: 2026-10-06
Publisher: srvScripts (https://srvscripts.com/)

**Short answer:** chan_sip was removed in Asterisk 21, so any `sip.conf` setup must move to `pjsip.conf` before you upgrade. Run the official converter (`contrib/scripts/sip_to_pjsip/sip_to_pjsip.py sip.conf pjsip.conf`) to get a first draft, then fix what it misses: `qualify`, inbound auth on trunks and anything in its “Non mapped elements” block. On FreePBX use `fwconsole convert2pjsip` and `fwconsole trunks --convert2pjsip` instead.

We ran these commands on our lab server (Debian 12, FreePBX 17.0.33, Asterisk 22.11) on 6 October 2026: the upstream converter on a sample sip.conf, and the fwconsole help for FreePBX’s converters. The conversion output below is from that run; we did not convert a production system.

## Why you have to migrate

chan_sip was deprecated for years and the Asterisk project removed it in Asterisk 21. The current LTS releases (Asterisk 22, and Asterisk 24 when it ships) have only `chan_pjsip`. Our lab’s Asterisk 22.11 lists `chan_pjsip.so` but no `chan_sip.so` at all:

```
asterisk -rx "module show like chan_"
```

FreePBX 17 ships with Asterisk 22 by default, so a FreePBX 16 system with chan_sip extensions or trunks hits the same wall when you move. Asterisk 20 LTS still has chan_sip, but it enters security-fix-only status on 19 October 2026 and reaches end of life on 19 October 2027.

PJSIP is not a renamed chan_sip. One `[peer]` becomes several objects:

| PJSIP object | Holds | chan_sip equivalent |
| --- | --- | --- |
| type=endpoint | Call settings: context, codecs, DTMF, NAT, media | Most peer options |
| type=aor | Where to reach it: max contacts, static contact, qualify | host=, qualify= |
| type=auth | Username and password | secret=, username= |
| type=identify | Match inbound traffic by source IP | host= on IP-authenticated trunks |
| type=registration | Outbound registration to a provider | register => line |
| type=transport | Bind address, protocol, NAT addresses | [general] bind and NAT settings |

## Run sip_to_pjsip.py

The script lives in the Asterisk source tree at `contrib/scripts/sip_to_pjsip/` and needs its two helper files (`astconfigparser.py`, `astdicts.py`) beside it. The Asterisk 22 version runs on Python 3. Fetch all three into a work folder, away from `/etc/asterisk`:

```
mkdir -p /root/pjsip-migration && cd /root/pjsip-migration
for f in sip_to_pjsip.py astconfigparser.py astdicts.py; do
  curl -fsSO https://raw.githubusercontent.com/asterisk/asterisk/22/contrib/scripts/sip_to_pjsip/$f
done
cp -p /etc/asterisk/sip*.conf .
python3 sip_to_pjsip.py --help
```

Help output on lab3:

```
Usage: sip_to_pjsip.py [options] [input-file [output-file]]

Converts the chan_sip configuration input-file to the chan_pjsip output-file.
The input-file defaults to 'sip.conf'.
The output-file defaults to 'pjsip.conf'.

Options:
  -h, --help            show this help message and exit
  -p PREFIX, --prefix=PREFIX
                        output prefix for include files
  -q, --quiet           don't print messages to stdout
```

Run it from the folder that holds `sip.conf` and any files it `#include`s, and write to a new file so nothing live is touched:

```
python3 sip_to_pjsip.py sip.conf pjsip-converted.conf
```

FreePBX 17 also carries an older copy of this script under `/var/www/html/admin/modules/core/sip_to_pjsip/`. On our lab it is Python 2 code and fails under Debian 12’s Python 3.11 with `SyntaxError: Missing parentheses in call to 'print'`. Use the upstream copy above, or FreePBX’s own fwconsole converters.

## What the converter produced on our lab

Our test `sip.conf` had a NAT setting in `[general]`, a registering trunk and one phone (all example values):

```
[general]
udpbindaddr=0.0.0.0:5060
externip=203.0.113.10
localnet=192.168.1.0/255.255.255.0
register => bob-trunk:ExamplePass1@sip.example.com

[bob-trunk]
type=peer
host=sip.example.com
username=bob-trunk
secret=ExamplePass1
fromuser=bob-trunk
insecure=port,invite
context=from-trunk
disallow=all
allow=ulaw,alaw
dtmfmode=rfc2833
qualify=yes

[201]
type=friend
host=dynamic
secret=ExamplePass2
context=from-internal
nat=force_rport,comedia
qualify=yes
directmedia=no
disallow=all
allow=ulaw,g722
dtmfmode=rfc2833
callerid="Bob"
```

The converter wrote this (exactly as produced, comment banner shortened):

```
;  Non mapped elements start
[general]
udpbindaddr = 0.0.0.0:5060

[bob-trunk]
username = bob-trunk

[201]
qualify = yes
;  Non mapped elements end

[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
external_media_address = 203.0.113.10
external_signaling_address = 203.0.113.10
local_net = 192.168.1.0/255.255.255.0

[reg_sip.example.com]
type = registration
retry_interval = 20
max_retries = 10
expiration = 120
transport = transport-udp
outbound_auth = auth_reg_sip.example.com
client_uri = sip:bob-trunk@sip.example.com
server_uri = sip:sip.example.com

[auth_reg_sip.example.com]
type = auth
password = ExamplePass1
username = bob-trunk

[bob-trunk]
type = aor
contact = sip:bob-trunk@sip.example.com

[bob-trunk]
type = identify
endpoint = bob-trunk
match = sip.example.com

[bob-trunk]
type = auth
username = bob-trunk
password = ExamplePass1

[bob-trunk]
type = endpoint
context = from-trunk
dtmf_mode = rfc4733
disallow = all
allow = ulaw,alaw
from_user = bob-trunk
auth = bob-trunk
outbound_auth = bob-trunk
aors = bob-trunk

[201]
type = aor
max_contacts = 1

[201]
type = auth
username = 201
password = ExamplePass2

[201]
type = endpoint
context = from-internal
dtmf_mode = rfc4733
disallow = all
allow = ulaw,g722
rtp_symmetric = yes
force_rport = yes
rewrite_contact = yes
direct_media = no
callerid = "Bob"
auth = 201
outbound_auth = 201
aors = 201
```

What to fix by hand before using it:

- **qualify was not converted.** `qualify=yes` landed in the “Non mapped” block for the phone and vanished for the trunk. Add `qualify_frequency=60` (or your preferred interval in seconds) to each `type=aor`.

- **The trunk got inbound auth.** `auth = bob-trunk` on the trunk endpoint means Asterisk will challenge calls from the provider. Providers usually do not answer challenges; with an `identify` match you normally keep only `outbound_auth`. The chan_sip `insecure=port,invite` line was dropped without a note.

- **The phone got outbound_auth it does not need.** Harmless, but you can remove it.

- **Registration and auth names changed** (`reg_sip.example.com`, `auth_reg_sip.example.com`). Update any monitoring that checks registration names.

- **Everything in the “Non mapped elements” block** needs a manual decision. Delete the block once handled; it is not valid PJSIP.

## Option mapping for common chan_sip settings

| chan_sip (sip.conf) | PJSIP (pjsip.conf) | Object |
| --- | --- | --- |
| secret=, username= | password=, username=, auth_type=userpass | auth |
| host=dynamic | max_contacts=1 (or more) | aor |
| host=203.0.113.10 | contact=sip:203.0.113.10:5060 plus an identify | aor, identify |
| qualify=yes | qualify_frequency=60 | aor |
| nat=force_rport,comedia (or nat=yes) | rtp_symmetric=yes, force_rport=yes, rewrite_contact=yes | endpoint |
| directmedia=no | direct_media=no | endpoint |
| dtmfmode=rfc2833 | dtmf_mode=rfc4733 | endpoint |
| fromuser=, fromdomain= | from_user=, from_domain= | endpoint |
| context=, allow=, disallow=, callerid= | same names | endpoint |
| register => user:pass@host | type=registration + outbound_auth | registration, auth |
| externip=, localnet= | external_media_address=, external_signaling_address=, local_net= | transport |
| udpbindaddr= | bind= on a protocol=udp transport | transport |
| insecure=invite | No direct option: match the trunk with identify and do not set inbound auth | endpoint, identify |

The NAT mapping follows the Asterisk documentation: `nat=yes` becomes the three options above, `nat=route` becomes `force_rport=yes` and `rewrite_contact=yes` without symmetric RTP. Our [PJSIP NAT settings generator](/tools/pjsip-nat-generator/) writes the full NAT block for you.

## Migrating on FreePBX

FreePBX stores extensions and trunks in its database and writes the `pjsip.*.conf` files itself, so do not paste converted files into `/etc/asterisk`. Use its tools. FreePBX 17.0.33 on our lab has:

```
fwconsole convert2pjsip --all                 # every chan_sip extension
fwconsole convert2pjsip --range=5000-5100,6020  # selected extensions
fwconsole trunks --list                         # see trunks and their technology
fwconsole trunks --convert2pjsip=all            # or a single trunk ID
```

When restoring a FreePBX 16 backup onto 17, `fwconsole backup` has matching switches: `--convertchansipexts2pjsip`, `--convertchansiptrunks2pjsip`, or `--skipchansipexts` and `--skipchansiptrunks` to leave them out. Our [FreePBX 16 to 17 upgrade guide](/guides/upgrade-freepbx-16-to-17/) covers that path.

Converted phones must re-register to the PJSIP port. If PJSIP used a different port than chan_sip on the old system (FreePBX 13-16 often ran PJSIP on 5060 and chan_sip on 5160, or the other way round), update phone provisioning or the Bind Port in Asterisk SIP Settings before the cut-over.

## Test the migration

Load the new configuration on a test box or during a maintenance window. Transport changes need a full restart, not a reload:

```
asterisk -rx "core restart when convenient"
asterisk -rx "pjsip show transports"
asterisk -rx "pjsip show registrations"
asterisk -rx "pjsip show endpoints"
asterisk -rx "pjsip show contacts"
```

Then work through this list:

- Each provider registration shows **Registered**.

- Each phone appears in `pjsip show contacts` with status **Avail** and a round-trip time.

- Inbound call from the provider reaches the right context (watch with `pjsip set logger on`; a `401` or `403` back to the provider points to the inbound auth problem above).

- Outbound call with correct caller ID.

- Audio both ways on an external call and on a phone behind NAT.

- DTMF in voicemail or an IVR (checks `dtmf_mode`).

- Transfers, hold, call parking and BLF (hints use `PJSIP/201` instead of `SIP/201`).

Search your custom dialplan for leftovers: `grep -rn "SIP/" /etc/asterisk/extensions*.conf`. Any `Dial(SIP/...)`, `SIPPEER()` or `SIP_HEADER()` must change to `PJSIP/...`, `PJSIP_ENDPOINT()`/`PJSIP_CONTACT()` and `PJSIP_HEADER()`.

**Official documentation:** [Asterisk: Migrating from chan_sip to res_pjsip](https://docs.asterisk.org/Configuration/Channel-Drivers/SIP/Configuring-res_pjsip/Migrating-from-chan_sip-to-res_pjsip/) · [sip_to_pjsip source (Asterisk 22)](https://github.com/asterisk/asterisk/tree/22/contrib/scripts/sip_to_pjsip) · [Asterisk versions and EOL dates](https://docs.asterisk.org/About-the-Project/Asterisk-Versions/)

**Related:** [PJSIP NAT Settings Generator: Asterisk, FreePBX, Issabel, VitalPBX, 3CX](/tools/pjsip-nat-generator/) · [PJSIP Behind NAT: Asterisk and FreePBX Settings for Two-Way Audio](/guides/pjsip-nat-asterisk-freepbx/) · [Install Asterisk 22 LTS on Debian 13: Step-by-Step Guide](/guides/install-asterisk-22-debian/) · [Troubleshoot SIP Calls with sngrep and the Asterisk PJSIP Logger](/guides/troubleshoot-sip-sngrep/) · [SIP Response Codes: Lookup for Every SIP Error, With Causes and Fixes](/tools/sip-response-codes/)

**See also:** [sip.conf to pjsip.conf Converter for Asterisk](/tools/sip-to-pjsip-converter/) · [Asterisk CLI Commands Cheat Sheet: PJSIP, Calls, Dialplan, Logs](/guides/asterisk-cli-commands-cheat-sheet/) · [PJSIP Endpoint Unreachable or Unavailable: Fix Qualify and NAT](/guides/pjsip-endpoint-unreachable-qualify/) · [FreePBX PJSIP Trunk Setup: Registration, IP Auth, Routes, NAT](/guides/freepbx-pjsip-trunk-setup/)

## Frequently asked questions

### Which Asterisk version removed chan_sip?

Asterisk 21. Asterisk 20 LTS is the last LTS release with chan_sip; it goes security-fix-only on 19 October 2026 and end of life on 19 October 2027.

### Does sip_to_pjsip.py convert everything?

No. The Asterisk docs say it is not meant to cover every configuration. In our test it skipped qualify, dropped insecure, and added inbound auth to a trunk, so review the output line by line.

### Can I run chan_sip and PJSIP side by side during migration?

Only on Asterisk 20 or older, and they must listen on different ports. On Asterisk 21 and later only PJSIP exists.

### How do I convert FreePBX chan_sip extensions?

Run fwconsole convert2pjsip –all or with –range, and fwconsole trunks –convert2pjsip=all for trunks. Then fwconsole reload and re-register the phones.

### What replaces SIPPEER() and SIP_HEADER() in dialplan?

Use PJSIP_ENDPOINT() or PJSIP_CONTACT() for peer data and PJSIP_HEADER() to read or add SIP headers.
