After some back-and-forth with a PSTN provider about why hangups weren’t getting processed, we found that Asterisk’s BYEs weren’t following the route set but instead being sent back to the (TCP) source port from the original incoming INVITE on the call.
I found two default settings in the pjsip trunk that I think would be uncommon unless you are dealing with a remote NAT, which would not normally apply to trunks, only phones. Namely, “Force rport” and “Rewrite Contact.” Switching these off allowed the request to follow the correct path defined by the route set and Contact header and BYEs worked properly.
As a general design philosophy when it comes to PJSIP, I believe that FreePBX strives to use the same defaults as Asterisk. Perhaps @jcolp can confirm that the GUI is not arbitrarily setting these values to non-asterisk defaults.
I think where this matters is where the provider is not facing the Internet with a topology-hiding SBC.
I have seen several cases where the provider’s traffic shows a multi-hop route set or a Contact header containing internal IP addresses. All of which are valid of course. But aggressive NAT handling might see those internal IPs and try to “fix” them, which then breaks the message flow.
Other providers hide all their topology and all Asterisk sees is a single point that it is talking to and all is well.
In this case I do believe the force_rport needed to be disabled because I am using TLS. The incoming TLS call is not sourced from the provider’s port 5061 but from an ephemeral port. The provider is expecting Asterisk to send requests back to it (such as BYE) on 5061 per the headers. With force_rport turned on, Asterisk was trying to send the BYE request to the ephemeral port which was already closed.