Summary
Outbound trunk resolution does not implement RFC 3263. There are three separate defects
stacked on top of each other, and they interact such that a correctly-provisioned SRV zone is
silently ignored whenever the SIP domain also has an A record — which is the common case.
- The destination port is fixed before DNS runs, so an SRV
port can never be honoured.
resolveAddr tries A/AAAA first and only falls back to SRV on failure, so any domain
with an A record never triggers an SRV query at all.
- The SRV fallback itself cannot succeed — it calls
net.ParseIP() on the SRV target
hostname without resolving it, which always yields nil.
Additionally, the fallback only ever queries _sip._udp / _sip._tcp, so _sips._tcp is
never queried even for a TLS trunk. That last point was reported in #458 (closed as
not-planned), but #458 only captured the service-name facet — the deeper problem is that the
SRV code path is unreachable-then-broken regardless of service name.
Versions
livekit/sip — reproduced on v1.7.0; code is unchanged on main @ 55e76dac
(2026-08-24) and on the v1.11.0 tag.
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb (the pin on main)
github.com/emiago/sipgo v1.4.0
livekit/sipgo/sip is a thin alias layer over emiago/sipgo/sip, so Request.Destination()
and DefaultPort() below are emiago's.
Reproduction
A zone provisioned per RFC 3263 — SRV records for both UDP and TLS, plus an A record on the
domain itself (e.g. for the web/API endpoint sharing the name):
sip.example.com. A 198.51.100.84
sip.example.com. A 198.51.100.85
_sip._udp.sip.example.com. SRV 10 100 5060 sip1.sip.example.com.
_sip._udp.sip.example.com. SRV 10 100 5060 sip2.sip.example.com.
_sips._tcp.sip.example.com. SRV 10 100 5061 sip1.sip.example.com.
_sips._tcp.sip.example.com. SRV 10 100 5061 sip2.sip.example.com.
sip1.sip.example.com. A 198.51.100.84
sip2.sip.example.com. A 198.51.100.85
Create an outbound trunk with --address sip.example.com (no explicit port), for
--transport udp and again for --transport tls.
Expected (RFC 3263 §4.2): sips: / TLS resolves via _sips._tcp, sip: / UDP via
_sip._udp; the SRV target is then A-resolved, and the SRV port, priority and weight
govern target selection and failover.
Actual: both resolve straight to the first A record of the domain, at the hardcoded default
port. No SRV query is ever issued.
sip:sip.example.com transport=udp -> 198.51.100.84:5060 [A record; SRV never queried]
sip:sip.example.com;transport=tcp transport=tcp -> 198.51.100.84:5060 [A record; SRV never queried]
sips:sip.example.com transport=tls -> 198.51.100.84:5061 [A record; SRV never queried]
I reproduced this by running resolveAddr's logic verbatim inside a container built from this
tree, against live DNS; the tracing below is what connects it to the real outbound call path.
Root cause
(1) Port is decided before any DNS lookup.
URI.GetURI() leaves Port unset when the trunk address carries no explicit port
(pkg/sip/types.go#L160-L172).
Request.Destination() then fills it in from a hardcoded table
(emiago/sipgo sip/request.go#L226-L231
→ sip/transport.go#L51-L66).
URI.GetPort() does the same for other call sites
(pkg/sip/types.go#L127-L137):
func (u URI) GetPort() int {
port := int(u.Addr.Port())
if port == 0 {
if u.Transport == TransportTLS {
port = 5061
} else {
port = 5060
}
}
return port
}
By the time ClientRequestConnection calls sip.ParseAddr(req.Destination()), the port is
already 5060/5061. Even a working SRV lookup could not change it.
(2) A/AAAA short-circuits the SRV lookup.
livekit/sipgo transport/layer.go#L396-L420:
func (l *Layer) resolveAddr(ctx context.Context, network string, host string, addr *Addr) error {
// We need to try local resolving.
ip, err := net.ResolveIPAddr("ip", host)
if err == nil {
addr.IP = ip.IP
return nil // <-- SRV is never attempted for any host with an A record
}
...
_, addrs, err := l.dnsResolver.LookupSRV(ctx, "sip", lookupnet, host)
net.ResolveIPAddr is an A/AAAA lookup, so SRV is positioned as a fallback for names that
don't resolve. RFC 3263 §4.2 branches on the syntax of the URI, not on what DNS returns:
If the TARGET was not a numeric IP address, and no port was present in the URI, the client
performs an SRV query [...]
If no SRV records were found, the client performs an A or AAAA record lookup of the
domain name.
The RFC's short-circuits are an IP literal (no DNS at all) and an explicit port in the URI
(A/AAAA, SRV skipped) — the presence of an A record is never a signal it consults for ordering.
resolveAddr substitutes "did the A/AAAA query succeed?" for "does the URI carry a port?".
Those two tests agree only for domains that have no A record.
This inverts the RFC's intent in a way that penalises correct provisioning: under RFC 3263 an A
record alongside SRV is the recommended belt-and-braces setup, present so that clients finding
no SRV still reach the domain. Here, publishing that A record is exactly what disables SRV —
the spec's fallback becomes this implementation's fast path.
(3) The SRV fallback cannot produce a usable address.
Same function, layer.go#L417-L419:
a := addrs[0]
addr.IP = net.ParseIP(a.Target[:len(a.Target)-1])
addr.Port = int(a.Port)
a.Target is a hostname (sip2.sip.example.com.), not an IP. There is no second lookup, so
net.ParseIP returns nil. Confirmed against live DNS:
lookupnet=udp -> SRV target="sip2.sip.example.com." port=5060
==> net.ParseIP("sip2.sip.example.com") = <nil>
addrs[0] is also taken unconditionally — Priority and Weight are discarded, and there is
no failover to addrs[1].
(4) Service name.
LookupSRV(ctx, "sip", lookupnet, host) hardcodes the sip service, so _sips._tcp is never
queried — TLS trunks look for _sip._tcp. RFC 3263 §4.2 requires the _sips service
identifier for SIPS URIs, and for a SIP URI where the client wishes to use TLS. This is what
#458 hit.
(NAPTR is also never queried, but that is a minor point here: §4.1 only calls for NAPTR when
neither transport nor port is specified, and trunks always specify a transport, so a
conformant client would legitimately skip it in this path too.)
Impact
For a zone like the one above the failure is silent rather than loud: the hardcoded ports
happen to match the SRV ports, and the SRV targets happen to resolve to the same IPs the parent
A record returns, so calls connect and nothing looks wrong. What is actually lost is every
property SRV was provisioned for — priority ordering, weighted distribution, and failover to
the second target when the first is down.
It becomes a hard failure when the SRV target uses a non-default port, or points at hosts
outside the parent A set. And per #458, a domain with SRV but no A record fails outright with
fail to resolve target for ...: lookup _sip._tcp....: no such host — which is defect (2)'s
fallback firing and then hitting defect (4), and would hit defect (3) even if the service name
were right.
Suggested fix
All four live in livekit/sipgo's resolveAddr, plus the port plumbing in this repo:
- Invert the order: attempt SRV before A/AAAA when the Request-URI has no explicit port
(RFC 3263 §4.2 — an explicit port disables SRV, an explicit IP disables DNS entirely).
- Select service name by transport:
_sips._tcp for TLS, _sip._tcp for TCP, _sip._udp
for UDP (§4.2). NAPTR is optional — §4.1 only requires it when neither transport nor port
is specified, which trunks never hit.
- A-resolve the chosen SRV
Target instead of net.ParseIP-ing it.
- Honour
Priority/Weight for selection, and fall through to the next target on
connect failure.
- Let the resolved SRV port reach the destination — today
Destination() bakes in
DefaultPort() before the transport layer can override it.
Happy to put up a PR against livekit/sipgo if that's a direction you'd take.
Related: #458 (closed as not-planned) — same zone shape, narrower diagnosis.
Summary
Outbound trunk resolution does not implement RFC 3263. There are three separate defects
stacked on top of each other, and they interact such that a correctly-provisioned SRV zone is
silently ignored whenever the SIP domain also has an A record — which is the common case.
portcan never be honoured.resolveAddrtries A/AAAA first and only falls back to SRV on failure, so any domainwith an A record never triggers an SRV query at all.
net.ParseIP()on the SRV targethostname without resolving it, which always yields
nil.Additionally, the fallback only ever queries
_sip._udp/_sip._tcp, so_sips._tcpisnever queried even for a TLS trunk. That last point was reported in #458 (closed as
not-planned), but #458 only captured the service-name facet — the deeper problem is that the
SRV code path is unreachable-then-broken regardless of service name.
Versions
livekit/sip— reproduced onv1.7.0; code is unchanged onmain@55e76dac(2026-08-24) and on the
v1.11.0tag.github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb(the pin onmain)github.com/emiago/sipgo v1.4.0livekit/sipgo/sipis a thin alias layer overemiago/sipgo/sip, soRequest.Destination()and
DefaultPort()below are emiago's.Reproduction
A zone provisioned per RFC 3263 — SRV records for both UDP and TLS, plus an A record on the
domain itself (e.g. for the web/API endpoint sharing the name):
Create an outbound trunk with
--address sip.example.com(no explicit port), for--transport udpand again for--transport tls.Expected (RFC 3263 §4.2):
sips:/ TLS resolves via_sips._tcp,sip:/ UDP via_sip._udp; the SRV target is then A-resolved, and the SRVport,priorityandweightgovern target selection and failover.
Actual: both resolve straight to the first A record of the domain, at the hardcoded default
port. No SRV query is ever issued.
I reproduced this by running
resolveAddr's logic verbatim inside a container built from thistree, against live DNS; the tracing below is what connects it to the real outbound call path.
Root cause
(1) Port is decided before any DNS lookup.
URI.GetURI()leavesPortunset when the trunk address carries no explicit port(
pkg/sip/types.go#L160-L172).Request.Destination()then fills it in from a hardcoded table(
emiago/sipgo sip/request.go#L226-L231→
sip/transport.go#L51-L66).URI.GetPort()does the same for other call sites(
pkg/sip/types.go#L127-L137):By the time
ClientRequestConnectioncallssip.ParseAddr(req.Destination()), the port isalready
5060/5061. Even a working SRV lookup could not change it.(2) A/AAAA short-circuits the SRV lookup.
livekit/sipgo transport/layer.go#L396-L420:net.ResolveIPAddris an A/AAAA lookup, so SRV is positioned as a fallback for names thatdon't resolve. RFC 3263 §4.2 branches on the syntax of the URI, not on what DNS returns:
The RFC's short-circuits are an IP literal (no DNS at all) and an explicit port in the URI
(A/AAAA, SRV skipped) — the presence of an A record is never a signal it consults for ordering.
resolveAddrsubstitutes "did the A/AAAA query succeed?" for "does the URI carry a port?".Those two tests agree only for domains that have no A record.
This inverts the RFC's intent in a way that penalises correct provisioning: under RFC 3263 an A
record alongside SRV is the recommended belt-and-braces setup, present so that clients finding
no SRV still reach the domain. Here, publishing that A record is exactly what disables SRV —
the spec's fallback becomes this implementation's fast path.
(3) The SRV fallback cannot produce a usable address.
Same function,
layer.go#L417-L419:a.Targetis a hostname (sip2.sip.example.com.), not an IP. There is no second lookup, sonet.ParseIPreturnsnil. Confirmed against live DNS:addrs[0]is also taken unconditionally —PriorityandWeightare discarded, and there isno failover to
addrs[1].(4) Service name.
LookupSRV(ctx, "sip", lookupnet, host)hardcodes thesipservice, so_sips._tcpis neverqueried — TLS trunks look for
_sip._tcp. RFC 3263 §4.2 requires the_sipsserviceidentifier for SIPS URIs, and for a SIP URI where the client wishes to use TLS. This is what
#458 hit.
(NAPTR is also never queried, but that is a minor point here: §4.1 only calls for NAPTR when
neither transport nor port is specified, and trunks always specify a transport, so a
conformant client would legitimately skip it in this path too.)
Impact
For a zone like the one above the failure is silent rather than loud: the hardcoded ports
happen to match the SRV ports, and the SRV targets happen to resolve to the same IPs the parent
A record returns, so calls connect and nothing looks wrong. What is actually lost is every
property SRV was provisioned for — priority ordering, weighted distribution, and failover to
the second target when the first is down.
It becomes a hard failure when the SRV target uses a non-default port, or points at hosts
outside the parent A set. And per #458, a domain with SRV but no A record fails outright with
fail to resolve target for ...: lookup _sip._tcp....: no such host— which is defect (2)'sfallback firing and then hitting defect (4), and would hit defect (3) even if the service name
were right.
Suggested fix
All four live in
livekit/sipgo'sresolveAddr, plus the port plumbing in this repo:(RFC 3263 §4.2 — an explicit port disables SRV, an explicit IP disables DNS entirely).
_sips._tcpfor TLS,_sip._tcpfor TCP,_sip._udpfor UDP (§4.2). NAPTR is optional — §4.1 only requires it when neither transport nor port
is specified, which trunks never hit.
Targetinstead ofnet.ParseIP-ing it.Priority/Weightfor selection, and fall through to the next target onconnect failure.
Destination()bakes inDefaultPort()before the transport layer can override it.Happy to put up a PR against
livekit/sipgoif that's a direction you'd take.Related: #458 (closed as not-planned) — same zone shape, narrower diagnosis.