Skip to content

sip: redirect RTP destination on re-INVITE port change#728

Open
VictorUvarov wants to merge 12 commits into
livekit:mainfrom
VictorUvarov:vu/issue-661
Open

sip: redirect RTP destination on re-INVITE port change#728
VictorUvarov wants to merge 12 commits into
livekit:mainfrom
VictorUvarov:vu/issue-661

Conversation

@VictorUvarov

Copy link
Copy Markdown

Problem

When a SIP carrier sends a mid-call re-INVITE with a new RTP port (e.g. after a NAT rebind or media relay failover), LiveKit SIP was responding 200 OK but continuing to send RTP to the stale address — violating RFC 3264 §8, which requires the answerer to direct media to the address/port in the new offer.

Closes #661

What changed

pkg/sip/media_port.go: two new methods on MediaPort:

  • UpdateRemote: atomically redirects the RTP destination.
  • RemoteAddr: read accessor, used in tests.

pkg/sip/inbound.go : both re-INVITE paths now parse the SDP body and call UpdateRemote before responding 200 OK.

Tests

pkg/sip/media_port_test.goTestMediaPortUpdateRemote covers thehappy path, the zero-value no-op, and the unspecified-addr no-op. pkg/sip/signaling_test.goTestReinvite extended:

  • inbound/normal: asserts RemoteAddr() changes after a re-INVITE.
  • outbound/normal: same for the outbound path.
  • inbound/no_body: asserts a body-less re-INVITE leaves RemoteAddr() unchanged.

Unit tests

o test ./pkg/sip/... -run 'TestMediaPortUpdateRemote|TestReinvite/inbound/normal|TestReinvite/inbound/no_body|TestReinvite/outbound' -v  -count=1

Integration tests

go test ./pkg/sip/... -count=1

Thought process summary

The root cause was a one-liner gap: the re-INVITE handler called AcceptAsKeepAlive (200 OK) but never forwarded the new c=/m= address to the RTP layer. The SDP body already contained the new addr+port as desc.Addr after ParseWith — nothing needed to be renegotiated, just the destination pointer swapped.

The tricky edge case was RFC 3264 §8.4 call-hold, where c=0.0.0.0 signals hold and netip.MustParseAddrPort("0.0.0.0:N").IsValid() returns true. A plain IsValid() guard would have let hold re-INVITEs overwrite the real destination with the zero address. The !addr.Addr().IsUnspecified() check inside UpdateRemote blocks that.

When a carrier sends a re-INVITE with a new SDP offer containing a different
RTP destination address/port, update the media port to send packets to the new
destination. This fixes mid-call carrier port changes which were previously
ignored.

The fix parses the re-INVITE SDP offer body and calls UpdateRemote() to change
the RTP destination before accepting the re-INVITE. Parsing errors are logged
but don't prevent accepting the re-INVITE.
…INVITE test

- In MediaPort.UpdateRemote(), add !addr.Addr().IsUnspecified() guard to reject
  RFC 3264 §8.4 call-hold form (c=0.0.0.0 with non-zero port). netip.AddrPort
  with unspecified address is IsValid() == true, so prior guard missed it.

- Add TestMediaPortUpdateRemote case verifying 0.0.0.0:N is a no-op.

- Add TestReinvite/inbound/no_body subtest: verify a body-less re-INVITE leaves
  RemoteAddr unchanged (guards against offer-in-ACK / no-SDP-body cases).
- Add sdpBodyFromRequest helper to guard updateRemoteFromSDP against
  non-SDP content types; avoids spurious warn log on e.g. application/json
- Derive expected RemoteAddr in tests from newOffer.Addr instead of a
  hardcoded string literal
- Add outbound/no_body test symmetric to inbound/no_body; use
  call.NewRequest(INVITE) in both no_body cases for a truly body-less
  re-INVITE (Invite(nil) silently generates an SDP offer)
@VictorUvarov VictorUvarov requested a review from a team as a code owner June 21, 2026 20:18
@CLAassistant

CLAassistant commented Jun 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

udpConn.SetDst logged a nil *netip.AddrPort as "prev" on the first call
(before any destination was stored). The logger called .String() on the
nil pointer, causing a panic. Replace with the zero value netip.AddrPort{}.
Same class of bug as SetDst: udpConn.Read logged a nil *netip.AddrPort
as "prev" on the first packet received. Replace with netip.AddrPort{}.
req.ContentType() returns *ContentTypeHeader (a Stringer). When there is
no body the header is nil; fmt calls String() on the typed nil pointer,
causing a recovered panic that tparse reports as PANIC.

Drop content-type from the reinvite log calls — content-length already
indicates whether a body is present.
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.48718% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.85%. Comparing base (0460b40) to head (f9eb666).
⚠️ Report is 314 commits behind head on main.

Files with missing lines Patch % Lines
pkg/sip/inbound.go 80.00% 3 Missing and 2 partials ⚠️
pkg/sip/outbound.go 25.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #728      +/-   ##
==========================================
+ Coverage   65.25%   65.85%   +0.60%     
==========================================
  Files          51       41      -10     
  Lines        6588     7744    +1156     
==========================================
+ Hits         4299     5100     +801     
- Misses       1915     2175     +260     
- Partials      374      469      +95     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The "setting media destination/source" branch fires when prev is nil
(no previous value). Logging prev as an empty AddrPort was misleading.

@dennwc dennwc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in general, thank you! A few small changes needed:

Comment thread pkg/sip/inbound.go Outdated
Comment thread pkg/sip/inbound.go Outdated
@dennwc dennwc requested a review from alexlivekit June 23, 2026 14:04
Use the call's own CodecSet instead of GlobalCodecs() when parsing re-INVITE
SDP. Add updateRemoteFromSDP methods on inboundCall (holds mmu) and
outboundCall to make safe access to media explicit and encapsulated.
Store mediaCodecs on inboundCall alongside media under mmu in runMediaConn.
@VictorUvarov VictorUvarov requested a review from dennwc June 23, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SIP service does not redirect RTP to new remote port after re-INVITE changes m= line

3 participants