Howwise RSS
VoIP & Phone Systems7 min read

Asterisk PJSIP Trunk Between Two Servers: Working Config

Short answer

An Asterisk PJSIP trunk between two servers needs a transport, endpoint, AOR and identify object on each side, plus a dialplan route per extension range. With static IPs no registration is needed. Lock the link to the peer IP or a VPN.

Key takeaways
  • With two static IPs, use an IP-based trunk: an aor with contact=sip:peer-ip:5060 and an identify with match=peer-ip, with no registration.
  • A missing identify object is the top reason inbound calls hit the anonymous endpoint or get rejected.
  • Keep extension ranges unique per site (1xxx and 2xxx) and route by pattern, for example _2XXX on Office A.
  • Open 5060 and RTP 10000-20000 only to the peer IP, and keep the from-office context away from PSTN dialing to prevent toll fraud.
  • Test both directions, check pjsip show aors for Avail and RTT, and test the failover path with the peer's WAN down.

Which trunk design should you use between two offices?

Pick the design before copying any config, because it decides whether you need credentials at all. If both offices have static public IPs, an IP-based trunk with no registration is the simplest and most robust option. There is no registration to expire, and nothing to break when a password changes.

If one side has a dynamic IP, that side must register to the other, which has the static address. If you can run a site-to-site VPN, do that and treat the tunnel addresses as the peer IPs, since plain SIP and RTP on the internet are unencrypted.

DesignUse whenNeeds authWeakness
IP-based trunk (identify)Both sides have static IPsOptionalAnyone who can spoof the peer IP can reach the context, so firewall it
Registration trunkOne side has a dynamic IPYes, strong passwordRegistration expiry and NAT keepalive problems
WireGuard or IPsec VPN plus IP trunkAny IPs; you want encryptionOptionalOne more service to monitor
SIP TLS plus SRTPNo VPN, but certificates are manageableCertificatesCertificate renewal and trust setup

What are the five PJSIP objects a trunk needs, and why?

PJSIP splits what chan_sip kept in one block into separate objects. Each answers a different question, and a trunk fails in a specific way when one is missing.

The transport is the socket: protocol and bind address, plus the NAT addresses to advertise. The endpoint holds call behavior: context, codecs, direct_media, DTMF and RTP handling. The aor (address of record) tells Asterisk where to send calls, via contact=sip:peer-ip:5060, and is what qualify pings.

The auth object holds credentials and is only needed if the peer must authenticate. The identify object maps an inbound packet to an endpoint by source IP using match=. Without it, an inbound INVITE from the peer has no username to match, so it falls to the anonymous endpoint or gets a 401 or 403. That is the most common reason inbound calls fail while outbound calls work.

Warning: Do not name the trunk endpoint like a local extension (for example 2001 on a site that has a 2001 extension). Endpoint names are global, and a clash silently breaks one of them.

Mirrored pjsip.conf for Office A (1xxx) and Office B (2xxx)

Both servers use the same structure. Only the object names, the peer IP and the local NAT addresses change. This example uses documentation addresses: Office A is 203.0.113.10 and Office B is 198.51.100.20. Replace them and the LAN subnet.

Same-name objects of different types (endpoint, aor, identify) are normal. Add pjsip.conf changes to pjsip_custom.conf on FreePBX so the GUI does not overwrite them.

; ===== OFFICE A pjsip.conf (public IP 203.0.113.10) =====
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
local_net=192.168.1.0/24
external_signaling_address=203.0.113.10
external_media_address=203.0.113.10

[office-b]
type=endpoint
transport=transport-udp
context=from-office-b
disallow=all
allow=ulaw,alaw
aors=office-b
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
dtmf_mode=rfc4733
; outbound_auth=office-b-auth   ; only if credentials are used

[office-b]
type=aor
contact=sip:198.51.100.20:5060
qualify_frequency=30
qualify_timeout=3.0

[office-b]
type=identify
endpoint=office-b
match=198.51.100.20

; ===== OFFICE B pjsip.conf (public IP 198.51.100.20) =====
; Same file with these changes:
;   external_signaling_address / external_media_address = 198.51.100.20
;   local_net = B's LAN, for example 192.168.2.0/24
;   object names office-b -> office-a
;   context=from-office-a
;   contact=sip:203.0.113.10:5060
;   match=203.0.113.10

; Optional auth (add on both sides with matching values):
; [office-b-auth]
; type=auth
; auth_type=userpass
; username=siteA
; password=<long random string>

How do I route calls by extension range in extensions.conf?

The trunk only carries calls the dialplan sends to it. Give each site a non-overlapping range (1xxx and 2xxx), and send the other site's range to the trunk with a pattern. If your numbers overlap, use a site prefix such as 8 plus a site code (8 2 then the extension) and strip it before sending.

The incoming context from-office-b must only reach local extensions. It must never include your outbound or PSTN routes.

If the WAN or the peer is down, Dial returns with DIALSTATUS=CHANUNAVAIL or similar. Add a fallback so the caller does not hear dead air.

; ===== OFFICE A extensions.conf =====
[from-internal]
exten => _2XXX,1,NoOp(To Office B ${EXTEN})
 same => n,Dial(PJSIP/${EXTEN}@office-b,30)
 same => n,GotoIf($["${DIALSTATUS}" = "CHANUNAVAIL"]?down)
 same => n,Hangup()
 same => n(down),Playback(all-circuits-busy-now)
 same => n,Congestion()

[from-office-b]
exten => _1XXX,1,Dial(PJSIP/${EXTEN},30)
 same => n,Hangup()
; no other patterns, no 'i' route to outbound

; ===== OFFICE B =====
; [from-internal]  exten => _1XXX ... Dial(PJSIP/${EXTEN}@office-a,30)
; [from-office-a]  exten => _2XXX ... Dial(PJSIP/${EXTEN},30)

; With a site prefix (dial 82001 from A to reach B's 2001):
; exten => _82XXX,1,Dial(PJSIP/${EXTEN:1}@office-b,30)
Tip: On FreePBX, create a PJSIP trunk (or a Custom Trunk with dial string PJSIP/$OUTNUM$@office-b), then an Outbound Route with dial pattern 2XXX pointing to it. For incoming, create an Inbound Route or point the trunk context at a custom context. Reload with module reload res_pjsip.so and dialplan reload after manual edits.

Which codec should the link use, and why does audio go one way?

Over a LAN, VPN or good WAN link, use G.711 (ulaw or alaw). There is no transcoding cost, and both ends already support it. Both endpoints must share at least one codec, otherwise the call fails with 488 Not Acceptable Here. The codec guide covers the details.

Use G.722 for wideband audio at similar bandwidth. Choose Opus or G.729 only if the link is constrained. G.729 needs a license on Asterisk, and Opus needs a codec module build.

One-way audio is almost always RTP. SDP carries the IP and port the other side should send media to, and behind NAT that address is private unless Asterisk rewrites it. Set local_net, external_media_address and external_signaling_address on the transport, use rtp_symmetric=yes and force_rport=yes, and make sure the firewall RTP range matches rtp.conf (rtpstart=10000, rtpend=20000). Disable SIP ALG on the routers, since it rewrites SIP headers and breaks this.

CodecApprox. bandwidth per call (with IP overhead)Notes
G.711 ulaw/alawabout 87 kbps each wayNo transcoding, best default
G.722about 87 kbps each wayHD audio, same cost
G.729about 31 kbps each wayNeeds license
Opusvariable, often lowerNeeds codec module

How do I secure a trunk between two Asterisk servers?

Firewall by IP first. Allow SIP and the RTP range only from the peer, and drop everything else on those ports. Then set allow_anonymous=no in the [global]/system settings (or remove the anonymous endpoint), run fail2ban on the Asterisk log, and use a long random password if you use auth.

Toll fraud happens when an attacker reaches a context that can dial out. Keep from-office-b limited to _1XXX, and never route it to the PSTN trunk.

For real confidentiality, run the link over WireGuard or IPsec and bind the transport to the tunnel address. Alternatively use SIP TLS on 5061 with media_encryption=sdes or dtls and certificates.

# ufw on Office A (peer = Office B)
ufw allow from 198.51.100.20 to any port 5060 proto udp
ufw allow from 198.51.100.20 to any port 5060 proto tcp
ufw allow from 198.51.100.20 to any port 10000:20000 proto udp

# iptables equivalent
iptables -A INPUT -p udp -s 198.51.100.20 --dport 5060 -j ACCEPT
iptables -A INPUT -p udp -s 198.51.100.20 --dport 10000:20000 -j ACCEPT
iptables -A INPUT -p udp --dport 5060 -j DROP

# fail2ban jail (/etc/fail2ban/jail.local)
[asterisk]
enabled  = true
filter   = asterisk
logpath  = /var/log/asterisk/messages
maxretry = 5
bantime  = 3600
Warning: Check you have console or out-of-band access before enabling a default-drop firewall remotely, or you can lock yourself out. Add the peer IP to fail2ban ignoreip so it never bans the other office.

How do I verify the trunk and troubleshoot failures?

After editing, run the reload commands, then check each object. Make one test call in each direction, because a NAT or identify problem often affects only one direction.

In pjsip show aors, look for Avail and an RTT value. If it shows Unavail, the qualify OPTIONS is not getting a reply. If the call connects and then drops after about 30 seconds, see the 30-second call drop article; for 488, see the SIP 488 article.

SymptomLikely causeWhat confirms it
401/403 on inboundNo identify match, or auth mismatchpjsip show identifies lacks the peer IP; logger shows the anonymous endpoint
404 Not FoundNo dialplan match in the incoming contextcore show channels or CLI shows no extension in from-office-b
408 / timeoutFirewall block or wrong IPNo reply in sngrep; aor shows no RTT
One-way audioNAT or RTP ports blockedrtp set debug on shows packets only one way
aor UnavailQualify failingpjsip show aors shows Unavail
Connects, then dropsACK or SDP problem, usually NATRetransmitted 200 OK in pjsip set logger on
  1. Reload: module reload res_pjsip.so and dialplan reload.
  2. Run pjsip show endpoints, pjsip show aors and pjsip show identifies on both servers.
  3. Run pjsip set logger on, then place a call from A to B and back.
  4. Run core show channels during the call, then rtp set debug on if the audio is one way.
  5. Unplug or block the peer WAN and confirm the fallback plays instead of dead air.
asterisk -rx "pjsip show endpoints"
asterisk -rx "pjsip show aors"        # want: Avail with an RTT
asterisk -rx "pjsip show identifies"
asterisk -rx "pjsip qualify office-b"
asterisk -rx "pjsip set logger on"
asterisk -rx "core show channels"
sngrep -d any port 5060

Common questions

Do I need registration between two Asterisk servers?

No, not if both have static IPs. An aor with the peer contact plus an identify with match=peer-ip is enough. Registration is only for cases where one side has a dynamic address.

What happens if I have more than two sites?

Hub-and-spoke has each office trunk to one central PBX that routes ranges onward, which is easy to manage but makes the hub a single point of failure. Full mesh needs a trunk and a route per pair, which does not scale well beyond a handful of sites.

Can I share BLF or presence between sites?

Yes, but it needs SUBSCRIBE and NOTIFY across the trunk, which means the endpoint must allow subscriptions and the context must map hints. Many deployments skip it over a WAN because of the extra complexity.

Can I still use chan_sip or IAX2 for this?

Both work, but chan_sip is removed in Asterisk 21 and later, so new builds should use PJSIP. IAX2 uses a single UDP port (4569) and traverses NAT easily, but few phones or providers support it.

Why does the trunk work one way only?

Usually the identify is missing or wrong on one server, or the firewall on one side blocks the peer. Check pjsip show identifies on both servers and verify the SIP source address in the logger output.

Related topics:asteriskpjsipsip trunkfreepbxsite to site