Skip to content

BUG: ACK for 2xx INVITE fails when destination is a domain (UDP path lacks DNS resolution) #50

@FedorKiselev76

Description

@FedorKiselev76

BUG: ACK for 2xx INVITE fails when destination is a domain (UDP path lacks DNS resolution)
Summary

When sending an ACK for a 2xx final response to an outgoing INVITE,
rsipstack constructs the destination for the ACK based on the Contact header or Request-URI.
If that URI contains a DNS domain name, the ACK send path bypasses the usual SIP DNS logic and attempts to send UDP directly to a domain.

Since UdpConnection::send() cannot convert Host::Domain into SocketAddr, the ACK is never transmitted.

This produces errors like:

UDP send ERROR: Cannot convert domain to SocketAddr

and results in:

retransmissions of the 200 OK,

premature BYE or call teardown,

failure to establish a confirmed dialog state.

Reproduction Scenario

Client sends an outgoing INVITE.

Remote UAS replies with 2xx, with Contact: sip:servername:port.

send_ack() constructs ACK with Request-URI = the Contact URI.

ACK destination is overwritten with a SipAddr containing a domain instead of an IP.

SipConnection::send() for UDP tries to convert domain → SocketAddr without DNS resolution.

ACK send fails; no packet is emitted.

The INVITE path correctly uses TransportLayer::lookup() and DNS resolver.
The ACK path does not.

Root Cause
✔ INVITE & REGISTER use DNS resolution

Through TransportLayerInner::lookup() + DomainResolver.

❌ ACK path does not use DNS

Transaction::send_ack() bypasses the transport layer and sends directly to UdpConnection:

self.destination = destination_from_request(&ack);

If this destination contains a domain, UdpConnection::send() fails.

Minimal & Safe Fix (Option A)
Do not overwrite resolved destination for 2xx INVITE

The INVITE client transaction already resolved the correct IP address and stored it in self.destination.
Per RFC 3261 §13.2.2.4, the ACK for a 2xx response is an end-to-end request and may reuse this address.

Proposed patch (in transaction.rs → send_ack):

if let SipMessage::Request(ref req) = ack {
if let Some(resp) = self.last_response.as_ref() {

    // Only update destination for non-2xx responses.
    if resp.status_code.kind() != StatusCodeKind::Successful {
        self.destination = destination_from_request(&req);
    } else {
        // For 2xx INVITE responses, keep the original destination (already resolved).
        tracing::debug!(
            "send_ack: keeping existing destination for 2xx, dest={:?}",
            self.destination
        );
    }
}

}

This guarantees:

ACK uses the same resolved IP as INVITE,

avoids DNS lookup in hot paths,

preserves existing logic for error responses.

Alternative Fix (Option B)
Resolve domains in the UDP send path

Before calling UdpConnection::send(), detect domains and apply DNS resolution:

if let SipConnection::Udp(transport) = self {
let dest = if let Some(d) = destination {
match d.addr.host {
Host::Domain(_) => domain_resolver.resolve(d).await?,
_ => d.clone(),
}
} else {
None
};
transport.send(msg, dest.as_ref()).await
}

This ensures no UDP send ever fails due to Host::Domain.

Why This Should Be Fixed Upstream

SIP UAS commonly use domain names in Contact headers — this is RFC-compliant.

Dropping ACK breaks interoperability with many servers.

INVITE path already performs DNS, but ACK path does not — inconsistent behavior.

Fix is simple, localized, and does not change public API.

Zero performance impact.

Conclusion

Transaction::send_ack() for 2xx INVITE responses should not construct a raw domain-based destination for UDP sends.

Either:

Keep the resolved destination from INVITE (recommended), or

Perform DNS resolution before UDP send.

Happy to provide a PR with the patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions