Featured

Welcome to Arcadiy Ivanov Home!

Let me introduce myself.

My name is Arcadiy Ivanov, and I’m a workaholic. I’ve been a workaholic for as long as I can remember.

At some point during 2011 I’ve finally decided to capitalize on the fact that I spend twelve hours a day, every day, designing, developing and managing the development of software. The result of that surge of pride and greed is this sorry mess (aka professional blog), organized herein for the shameless self-plugging across your Interwebs.

Firstly, please visit the Legal section. No, it’s not particularly interesting, but certain obligations I have require me to send you there. Once you’re done reading, understanding and accepting the terms, conditions, licenses and disclaimers accompanying the content on this website feel free to look around.

One-Way Audio on Asterisk Behind NAT: A Deep Dive into Conntrack Tuple Collisions

The Setup

Asterisk PBX runs directly on a Linux firewall (Fedora) with two network interfaces:

  • WAN interface: public IP, connected to the internet
  • LAN interface: 192.168.x.1/24, serving the internal network

An FXS adapter with an analog phone sits on the LAN. Outbound calls go through a SIP trunking provider. The firewall handles NAT for the entire LAN using iptables:

# NAT for SIP signaling and RTP media
-A PREROUTING -i $WAN -p udp --dport 63000:63199 -j DNAT --to-destination 192.168.x.1:63000-63199
-A POSTROUTING -o $WAN -j MASQUERADE

The PJSIP transport is bound to the LAN address with proper NAT configuration:

[transport-udp]
type = transport
protocol = udp
bind = 192.168.x.1:5060
external_media_address = my.public.hostname
external_signaling_address = my.public.hostname
local_net = 192.168.x.0/24

SIP signaling works perfectly: calls connect, the phone rings, the caller ID is correct, and the called party answers. But the called party hears nothing.

The Symptom

Outgoing calls from the home phone to cell phones have one-way audio: the home phone hears the cell phone fine, but the cell phone hears silence.

The Investigation

A packet capture on both interfaces during a call shows four RTP flows:

Flow Packets Status
Provider → Asterisk (WAN inbound) 1150 Working
Asterisk → Phone (LAN) 1143 Working
Phone → Asterisk (LAN) 1139 Working
Asterisk → Provider (WAN outbound RTP) 0 Missing
Asterisk → Provider (WAN outbound RTCP) 4 Working

The SDP negotiation is correct on all sides: Asterisk’s res_pjsip_nat module properly rewrites the SDP connection address to the public IP, codecs match (G.711 ulaw), and the media ports are valid. Asterisk’s internal bridging operates normally; the native RTP bridge is established and bridge_p2p_rtp_write() processes every packet. No errors or warnings appear in the log, even at debug level 99. The sendto() system call succeeds for every packet.

Yet zero RTP packets reach the wire toward the provider, while RTCP (a separate socket on the adjacent port) goes out just fine.

The Root Cause

The problem involves three interacting components: PJSIP endpoint configuration, kernel source address selection, and iptables conntrack.

Step 1: RTP Socket Binds to the Wrong Address

When PJSIP creates an RTP socket for a call, it determines the bind address through the following logic in res_pjsip_sdp_rtp.c:

struct ast_sockaddr *media_address = &address_rtp; /* default: [::] */

if (session->endpoint->media.bind_rtp_to_media_address && ...) {
    media_address = &temp_media_address; /* use endpoint media_address */
} else {
    transport = ast_sorcery_retrieve_by_id(..., session->endpoint->transport);
    if (transport) {
        /* use transport's bind address (e.g., 192.168.x.1) */
        media_address = &temp_media_address;
    }
}

The default address_rtp is initialized to :: (IPv6 wildcard) at module load time if IPv6 is available. The transport lookup only happens if the endpoint explicitly sets transport = transport-udp in its configuration. If the transport option is omitted, PJSIP still auto-selects the correct transport for signaling, but the RTP socket creation code never learns about it: the socket binds to [::] (all interfaces) instead of 192.168.x.1.

Step 2: Kernel Picks the WAN IP as Source

When Asterisk calls sendto() on a socket bound to [::] with a destination on the internet (the provider’s media server), the Linux kernel consults the routing table and selects the WAN interface address as the source IP. The outbound packet becomes:

src=<PUBLIC_IP>:63016 → dst=<PROVIDER_MEDIA>:18052

Step 3: Conntrack Collision and Port Remapping

Here’s where it gets interesting. The provider’s media server is simultaneously sending RTP to Asterisk. This inbound traffic triggers the DNAT rule, and conntrack creates an entry:

ORIGINAL: src=<PROVIDER_MEDIA>:18052 dst=<PUBLIC_IP>:63016
          → DNAT to dst=192.168.x.1:63016
REPLY:    src=192.168.x.1:63016 dst=<PROVIDER_MEDIA>:18052

When Asterisk’s outbound RTP packet (<PUBLIC_IP>:63016 → <PROVIDER_MEDIA>:18052) hits the POSTROUTING chain, conntrack does not recognize it as a reply to the DNAT connection because the source address (<PUBLIC_IP>) doesn’t match the expected reply source (192.168.x.1). Instead, conntrack treats it as a brand new connection and hands it to the MASQUERADE target.

MASQUERADE attempts to create a NAT mapping, but the reverse tuple for this new connection (<PROVIDER_MEDIA>:18052 → <PUBLIC_IP>:63016) collides with the original tuple of the existing DNAT entry. To resolve the collision, MASQUERADE remaps the source port from 63016 to an arbitrary port like 3096.

The packet reaches the provider, but from port 3096 instead of the 63016 negotiated in the SDP. The provider’s media server either ignores or drops it.

Why RTCP Works

RTCP uses port 63017 (RTP port + 1). In this particular call, the provider doesn’t send any inbound RTCP, so no DNAT conntrack entry exists for that port pair. Asterisk’s outbound RTCP creates a clean new connection with no tuple collision, MASQUERADE applies without port remapping, and the packets go out with the correct source port.

Why sendto() Doesn’t Fail

On Linux, UDP sendto() returns the byte count immediately. The conntrack processing and MASQUERADE port remapping happen asynchronously in the netfilter chain, after the system call returns. From Asterisk’s perspective, every send succeeds, and no error is logged.

Why rtpbindaddr Doesn’t Help

A reasonable first instinct is to set rtpbindaddr=192.168.x.1 in rtp.conf. However, this setting does not exist in Asterisk 22. It is silently ignored with no warning, and the RTP socket continues to bind to [::]. The RTP bind address in PJSIP is determined entirely by the endpoint and transport configuration in pjsip.conf, not by rtp.conf.

The Fix

Add transport = transport-udp to the PJSIP endpoint configuration:

[my-trunk]
type = endpoint
transport = transport-udp
; ... rest of endpoint config

With this setting, the RTP socket binds to 192.168.x.1:63016 (the transport’s bind address) instead of [::]:63016. When Asterisk sends outbound RTP, the packet is:

src=192.168.x.1:63016 → dst=<PROVIDER_MEDIA>:18052

This matches the reply tuple of the existing DNAT conntrack entry. Conntrack recognizes it as an established reply, applies the reverse NAT (source changes from 192.168.x.1:63016 to <PUBLIC_IP>:63016), and the packet goes out on the WAN interface with the correct source port. No collision, no remapping, bidirectional audio.

Takeaways

  • When Asterisk runs on the NAT gateway itself (rather than behind it), the RTP socket bind address matters for conntrack interaction with DNAT rules. A wildcard bind causes the kernel to select the WAN IP as source, which creates a conntrack entry that collides with the inbound DNAT entry.
  • PJSIP’s auto-transport selection for signaling does not extend to RTP socket creation. The transport option must be explicitly set on each endpoint for RTP to inherit the transport’s bind address.
  • Conntrack tuple collisions don’t necessarily drop packets. MASQUERADE can silently remap source ports to resolve them, which is arguably worse than a drop: the packets reach the destination but are ignored because they don’t match the negotiated SDP, and no errors appear anywhere in the logs.

How to Optimize Intel Graphics Performance on Fedora KDE Linux Laptop

For several years I’ve struggled with poor Intel integrated graphics performance on my Dell Precision Mobile 7510 (Intel HD 530 primary and Quadro M2000M discrete via Bumblebee) on Fedora Linux. After starting to experience hard full system locks on video playback requiring a hard power off a few times a week, I have dug into the settings yet another time and finally had a breakthrough.
The symptoms I was experiencing were:

  • High baseline idle CPU consumption in Chrome/Chromium and IntelliJ/PyCharm
  • Extremely high CPU consumption on video playback in Chrome/Chromium and scrolling
  • Extremely high CPU consumption working in IntelliJ/PyCharm with huge typing, scrolling, rendering lag
  • Obvious resultant high fan speed/noise
  • Obvious resultant poor battery life
  • Extremely low framerate on two 4K 60Hz Samsung monitors connected via a Thunderbolt hub with simple window rendering, browsing and video
  • Extreme tearing of high resolution video or full-screen 1080p upscaled to 4K video in VLC and Chrome/Chromium

Below is the final configuration that I came up with that resolved all of my issues. While the exact configuration is specific to Fedora and my system, with minor modifications it can be easily applied to all other Linux flavors. Similarly this likely should also apply to other Dell Precision Mobile 5000- and 7000-series laptops as well as the XPS and Inspiron lines. I have also verified this to be working on at least one Intel NUC.

All recommendations are provided AS IS with no warranties or guarantees of any kind. You accept all responsibility for any and all consequences of your following of the instructions below.

Continue reading “How to Optimize Intel Graphics Performance on Fedora KDE Linux Laptop”

PSA: Default Configuration IPMI Vulnerability on Supermicro Boards

I’m using Supermicro boards (X11SBA-F) for virtually all of my servers, including the firewall. These boards come with IPMI enabled. The IPMI default configuration vulnerability I’m going to describe here rears its ugly head in dual-homed machines and machines plugged directly to WAN, putting them specifically in great danger.

Continue reading “PSA: Default Configuration IPMI Vulnerability on Supermicro Boards”