Resolve outbound destinations with DNS SRV - #809
Open
jaylim95 wants to merge 1 commit into
Open
Conversation
An outbound trunk address like "sip.example.com" is handed to the sipgo transport layer as a name, and resolveAddr() there tries net.ResolveIPAddr first and only falls back to SRV when the host has no address record. That is the reverse of RFC 3263 section 4, so in practice SRV records published for a trunk are never used. The fallback would not work either: it runs net.ParseIP over the SRV target, which is a host name, so the resolved IP comes out nil. Resolve the next hop here instead and pin it on the INVITE, which also keeps the transport layer from resolving the name a second time. SRV is only consulted when the URI carries no explicit port, per RFC 3263 section 4.2, so an address with a port or an IP literal keeps behaving exactly as before, and a name that publishes no usable SRV record still falls back to an address lookup at the transport's default port. Pinning the destination also means the ACK, CANCEL and BYE built from the INVITE reach the same host it did, rather than being resolved again and possibly landing on a different address record. Set disable_dns_srv to opt out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jaylim95
force-pushed
the
dns-srv-outbound
branch
from
August 25, 2026 04:47
8958677 to
dad2227
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #809 +/- ##
==========================================
+ Coverage 65.25% 67.34% +2.08%
==========================================
Files 51 43 -8
Lines 6588 8258 +1670
==========================================
+ Hits 4299 5561 +1262
- Misses 1915 2207 +292
- Partials 374 490 +116 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An outbound trunk address that is a bare host name never goes through DNS SRV. A trunk configured with
address: sip.example.comalways lands on that name's A record at port 5060, even when the carrier publishes_sip._udp.sip.example.com.livekit/sipdoesn't resolve the trunk address itself: the host name reaches sipgo as the request URI, andresolveAddrintransport/layer.go(github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb) resolves it like this:net.ParseIPis given the SRV target, which is a host name. It returns nil, and the connection is built againstnet.UDPAddr{IP: nil, Port: <srv port>}. The target needs a second address lookup that never happens.Quick check against a domain that publishes SIP SRV records:
resolveAddralso maps the TLS transport onto_sip._tcprather than_sips._tcp.What this does
Resolves the next hop in
sipOutboundand pins it on the INVITE withSetDestination, so the transport layer sees an address literal and doesn't resolve the name again.pkg/sip/dns.go—resolveNextHopimplements the RFC 3263 section 4 lookup: SRV for the request's transport (_sip._udp,_sip._tcp,_sips._tcp, plus the RFC 7118 WebSocket labels), then an address lookup of the chosen target.LookupSRValready orders records by priority and shuffles by weight, so the first target that resolves wins; targets that don't resolve are skipped.pkg/sip/outbound.go—attemptInvitepins the resolved destination. The next hop is the first Route header if there is one and the request URI otherwise, matching how the transport layer picks it.pkg/config/config.go—disable_dns_srvto opt out.Compatibility
The lookup order only changes for a host name that has no explicit port, which is the case RFC 3263 reserves for SRV:
1.2.3.4/1.2.3.4:5080sip.example.com:5080sip.example.com, no SRV publishedsip.example.com, SRV publishedResolution failures are not fatal: the destination is left unset and the transport layer falls back to its own lookup, so an unresolvable host still fails the way it does today.
Two things fall out of pinning the destination:
applyInviteResponsestill flushes the destination when the answering Contact or the route set moves the dialog somewhere else.Not included
transportparameter, so there is nothing for NAPTR to select.applyInviteResponserepoints the dialog at the answering Contact, later requests are resolved by the transport layer as before.The underlying
resolveAddrbug is in sipgo, and fixing it there would cover inbound and in-dialog paths too. I put the fix here because it's contained and doesn't need a dependency bump, but happy to send it tolivekit/sipgoinstead, or as well, if you'd rather have it at the root.Also happy to flip
disable_dns_srvinto an opt-in flag if you'd prefer the new lookup order behind a switch to start with.Testing
pkg/sip/dns_test.go— table tests over a stubbed resolver: SRV per transport, explicit port skipping SRV, unresolvable targets being skipped, an RFC 2782.target, address-record fallback, IPv6, IP literals.pkg/sip/outbound_test.go—TestOutboundINVITEUsesSRVDestinationasserts the INVITE goes to the SRV target while the request URI keeps the host name, and that the ACK follows it there.TestOutboundINVITERouteHeaderIsTheNextHopcoversoutbound_route_headers: the proxy in the Route header is what gets resolved, not the request URI.TestOutboundINVITEDNSSRVDisabledcovers the config flag. Without the change, the first fails withexpected: 192.0.2.10:5080, actual: sip.example.com:5060.NewOutboundTestClientnow injects a resolver that resolves nothing by default, so the existing tests don't touch the network.go test ./pkg/...andgolangci-lint run(v2.13.1, matching CI) are clean locally. Note thatTestTransferandTestRouteSetcan fail theirmonitor should be healthyassertion when the full suite runs on a loaded machine — that reproduces on unmodifiedmainand is unrelated to this change.