From 4a56a79bda2cfe8a079e4c3923f8f9a77627c751 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sat, 20 Jun 2026 22:34:27 -0700 Subject: [PATCH 01/11] sip: add MediaPort.UpdateRemote and RemoteAddr for re-INVITE redirect --- pkg/sip/media_port.go | 14 ++++++++++++++ pkg/sip/media_port_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index 00567906..c622eb77 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -630,6 +630,20 @@ func (p *MediaPort) Port() int { return p.port.LocalAddr().(*net.UDPAddr).Port } +func (p *MediaPort) RemoteAddr() netip.AddrPort { + dst := p.port.dst.Load() + if dst == nil { + return netip.AddrPort{} + } + return *dst +} + +func (p *MediaPort) UpdateRemote(addr netip.AddrPort) { + if addr.IsValid() { + p.port.SetDst(addr) + } +} + func (p *MediaPort) Received() <-chan struct{} { return p.mediaReceived.Watch() } diff --git a/pkg/sip/media_port_test.go b/pkg/sip/media_port_test.go index f8d5823c..009ef5b1 100644 --- a/pkg/sip/media_port_test.go +++ b/pkg/sip/media_port_test.go @@ -165,6 +165,31 @@ func newIP(v string) netip.Addr { return ip } +func TestMediaPortUpdateRemote(t *testing.T) { + log := logger.GetLogger() + mon := newTestCallMonitor(t) + + // newUDPPipe wires two in-memory testUDPConn together. + c1, _ := newUDPPipe() + mp, err := NewMediaPortWith(1, log, mon, c1, &MediaOptions{ + IP: netip.MustParseAddr("127.0.0.1"), + }, 8000) + require.NoError(t, err) + defer mp.Close() + + // Initially no destination is set. + require.False(t, mp.RemoteAddr().IsValid(), "RemoteAddr should be invalid before any update") + + // Update to a valid address. + addr := netip.MustParseAddrPort("9.8.7.6:12345") + mp.UpdateRemote(addr) + require.Equal(t, addr, mp.RemoteAddr(), "RemoteAddr should reflect the updated address") + + // UpdateRemote with invalid addr should be a no-op. + mp.UpdateRemote(netip.AddrPort{}) + require.Equal(t, addr, mp.RemoteAddr(), "UpdateRemote with invalid addr should not change RemoteAddr") +} + func TestMediaPort(t *testing.T) { // Main resampler has unpredictable (although tiny) output delay // and other randomness in the generated samples. From feeb4122cf1178de12f038cbfd99eacff3be072c Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sat, 20 Jun 2026 22:44:02 -0700 Subject: [PATCH 02/11] sip: redirect RTP destination on inbound re-INVITE port change 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. --- pkg/sip/inbound.go | 7 +++++++ pkg/sip/signaling_test.go | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 2d5f7bd9..e8fd970c 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -381,6 +381,13 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE s.cmu.RUnlock() if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() { existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) + if body := req.Body(); len(body) != 0 && existing.media != nil { + if desc, err := sdp.Parse(body); err == nil { + existing.media.UpdateRemote(desc.Addr) + } else { + existing.log().Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) + } + } cc.AcceptAsKeepAlive(existing.cc.OwnSDP()) return nil } diff --git a/pkg/sip/signaling_test.go b/pkg/sip/signaling_test.go index 389f4255..fb878784 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/sip/signaling_test.go @@ -641,7 +641,7 @@ func TestReinvite(t *testing.T) { t.Run("inbound", func(t *testing.T) { t.Run("normal", func(t *testing.T) { st := NewServiceTest(t, nil) - call, _ := st.CreateInboundCall(t) + call, ic := st.CreateInboundCall(t) serverLocalSDP := call.remoteSDP // Re-INVITE @@ -661,6 +661,13 @@ func TestReinvite(t *testing.T) { resp = st.TestUA.TransactionRequest(t, req, true) require.Equal(t, sip.StatusCode(200), resp.StatusCode, "reinvite for outbound call should get 200 OK") require.Equal(t, serverLocalSDP, resp.Body(), "reinvite 200 OK should return server local SDP") + + // After the re-INVITE with new offer, the media port destination must be updated. + require.Equal(t, + netip.MustParseAddrPort("9.8.7.6:12345"), + ic.media.RemoteAddr(), + "re-INVITE should redirect RTP to the new remote address", + ) }) t.Run("miss", func(t *testing.T) { From dcaf53b5d20d339236161765f7c1ff357957f2d1 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sat, 20 Jun 2026 23:04:00 -0700 Subject: [PATCH 03/11] sip: redirect RTP destination on outbound re-INVITE port change --- pkg/sip/inbound.go | 13 ++++++++++--- pkg/sip/signaling_test.go | 7 +++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index e8fd970c..a6dd6eb2 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -395,11 +395,18 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE oc := s.cli.getActiveCall(cc.ID()) newCSeq := cc.InviteCSeq() if oc != nil && oc.cc != nil && oc.cc.InviteCSeq() < newCSeq { - sdp := oc.cc.LocalSDP() - if len(sdp) != 0 { + localSDP := oc.cc.LocalSDP() + if len(localSDP) != 0 { oc.log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) + if body := req.Body(); len(body) != 0 && oc.media != nil { + if desc, err := sdp.Parse(body); err == nil { + oc.media.UpdateRemote(desc.Addr) + } else { + oc.log.Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) + } + } oc.cc.RecordInvite(newCSeq) - cc.AcceptAsKeepAlive(sdp) + cc.AcceptAsKeepAlive(localSDP) return nil } } diff --git a/pkg/sip/signaling_test.go b/pkg/sip/signaling_test.go index fb878784..b3cf38ed 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/sip/signaling_test.go @@ -715,6 +715,13 @@ func TestReinvite(t *testing.T) { resp = st.TestUA.TransactionRequest(t, req, false) require.Equal(t, sip.StatusCode(200), resp.StatusCode, "reinvite for outbound call should get 200 OK") require.Equal(t, serverLocalSDP, resp.Body(), "reinvite 200 OK should return server local SDP") + + // After the re-INVITE with new offer, the media port destination must be updated. + require.Equal(t, + netip.MustParseAddrPort("9.8.7.6:12345"), + oc.media.RemoteAddr(), + "re-INVITE should redirect outbound call RTP to the new remote address", + ) }) t.Run("miss", func(t *testing.T) { From 539ade53e6653f5573d00b6ed5173318d70d2522 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sat, 20 Jun 2026 23:25:32 -0700 Subject: [PATCH 04/11] sip: replace deprecated sdp.Parse with sdp.ParseWith --- pkg/sip/inbound.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index a6dd6eb2..8d2768ae 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -382,7 +382,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() { existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) if body := req.Body(); len(body) != 0 && existing.media != nil { - if desc, err := sdp.Parse(body); err == nil { + if desc, err := sdp.ParseWith(msdk.GlobalCodecs(), body); err == nil { existing.media.UpdateRemote(desc.Addr) } else { existing.log().Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) @@ -399,7 +399,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE if len(localSDP) != 0 { oc.log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) if body := req.Body(); len(body) != 0 && oc.media != nil { - if desc, err := sdp.Parse(body); err == nil { + if desc, err := sdp.ParseWith(msdk.GlobalCodecs(), body); err == nil { oc.media.UpdateRemote(desc.Addr) } else { oc.log.Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) From 4f91d0384e7e75f8e2ef824a4f765801b1cad5bd Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 12:13:30 -0700 Subject: [PATCH 05/11] sip: guard UpdateRemote against unspecified addr; add body-absent re-INVITE test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- pkg/sip/media_port.go | 2 +- pkg/sip/media_port_test.go | 4 ++++ pkg/sip/signaling_test.go | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index c622eb77..e581f07e 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -639,7 +639,7 @@ func (p *MediaPort) RemoteAddr() netip.AddrPort { } func (p *MediaPort) UpdateRemote(addr netip.AddrPort) { - if addr.IsValid() { + if addr.IsValid() && !addr.Addr().IsUnspecified() { p.port.SetDst(addr) } } diff --git a/pkg/sip/media_port_test.go b/pkg/sip/media_port_test.go index 009ef5b1..e2ee1222 100644 --- a/pkg/sip/media_port_test.go +++ b/pkg/sip/media_port_test.go @@ -188,6 +188,10 @@ func TestMediaPortUpdateRemote(t *testing.T) { // UpdateRemote with invalid addr should be a no-op. mp.UpdateRemote(netip.AddrPort{}) require.Equal(t, addr, mp.RemoteAddr(), "UpdateRemote with invalid addr should not change RemoteAddr") + + // UpdateRemote with unspecified address (c=0.0.0.0 hold form) should be a no-op. + mp.UpdateRemote(netip.MustParseAddrPort("0.0.0.0:12345")) + require.Equal(t, addr, mp.RemoteAddr(), "UpdateRemote with unspecified addr should not change RemoteAddr") } func TestMediaPort(t *testing.T) { diff --git a/pkg/sip/signaling_test.go b/pkg/sip/signaling_test.go index b3cf38ed..63471143 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/sip/signaling_test.go @@ -690,6 +690,21 @@ func TestReinvite(t *testing.T) { require.Equal(t, sip.StatusCode(200), resp.StatusCode, "reinvite for outbound call should get 200 OK") require.NotEqual(t, serverLocalSDP, resp.Body(), "reinvite for new call should return new server local SDP") }) + + t.Run("no_body", func(t *testing.T) { + st := NewServiceTest(t, nil) + call, ic := st.CreateInboundCall(t) + serverLocalSDP := call.remoteSDP + initialRemote := ic.media.RemoteAddr() + + // Re-INVITE with no SDP body — destination must not change. + req, _, err := call.Invite(nil) + require.NoError(t, err) + resp := st.TestUA.TransactionRequest(t, req, true) + require.Equal(t, sip.StatusCode(200), resp.StatusCode, "body-less re-INVITE should still get 200 OK") + require.Equal(t, serverLocalSDP, resp.Body(), "body-less re-INVITE should return server local SDP") + require.Equal(t, initialRemote, ic.media.RemoteAddr(), "body-less re-INVITE must not change RTP destination") + }) }) t.Run("outbound", func(t *testing.T) { t.Run("normal", func(t *testing.T) { From 27281abfb620ae1aeea59bd35a5c5983caeceefd Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 12:35:28 -0700 Subject: [PATCH 06/11] sip: extract updateRemoteFromSDP helper to reduce re-INVITE nesting --- pkg/sip/inbound.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 8d2768ae..91c90bc8 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -312,6 +312,18 @@ func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Re return true, false } +func updateRemoteFromSDP(media *MediaPort, log logger.Logger, body []byte) { + if len(body) == 0 || media == nil { + return + } + desc, err := sdp.ParseWith(msdk.GlobalCodecs(), body) + if err != nil { + log.Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) + return + } + media.UpdateRemote(desc.Addr) +} + func (s *Server) onInvite(log *slog.Logger, req *sip.Request, tx sip.ServerTransaction) { // Error processed in defer _ = s.processInvite(req, tx) @@ -381,13 +393,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE s.cmu.RUnlock() if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() { existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) - if body := req.Body(); len(body) != 0 && existing.media != nil { - if desc, err := sdp.ParseWith(msdk.GlobalCodecs(), body); err == nil { - existing.media.UpdateRemote(desc.Addr) - } else { - existing.log().Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) - } - } + updateRemoteFromSDP(existing.media, existing.log(), req.Body()) cc.AcceptAsKeepAlive(existing.cc.OwnSDP()) return nil } @@ -398,13 +404,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE localSDP := oc.cc.LocalSDP() if len(localSDP) != 0 { oc.log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) - if body := req.Body(); len(body) != 0 && oc.media != nil { - if desc, err := sdp.ParseWith(msdk.GlobalCodecs(), body); err == nil { - oc.media.UpdateRemote(desc.Addr) - } else { - oc.log.Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err) - } - } + updateRemoteFromSDP(oc.media, oc.log, req.Body()) oc.cc.RecordInvite(newCSeq) cc.AcceptAsKeepAlive(localSDP) return nil From f96381d13e7c9642d60e1bfbbf0eb9df5c233b4d Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 13:10:27 -0700 Subject: [PATCH 07/11] sip: address review findings from re-INVITE redirect implementation - 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) --- pkg/sip/inbound.go | 12 ++++++++++-- pkg/sip/signaling_test.go | 29 +++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 91c90bc8..6dc81eb0 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -312,6 +312,14 @@ func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Re return true, false } +func sdpBodyFromRequest(req *sip.Request) []byte { + ct := req.ContentType() + if ct != nil && ct.Value() != "application/sdp" { + return nil + } + return req.Body() +} + func updateRemoteFromSDP(media *MediaPort, log logger.Logger, body []byte) { if len(body) == 0 || media == nil { return @@ -393,7 +401,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE s.cmu.RUnlock() if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() { existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) - updateRemoteFromSDP(existing.media, existing.log(), req.Body()) + updateRemoteFromSDP(existing.media, existing.log(), sdpBodyFromRequest(req)) cc.AcceptAsKeepAlive(existing.cc.OwnSDP()) return nil } @@ -404,7 +412,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE localSDP := oc.cc.LocalSDP() if len(localSDP) != 0 { oc.log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) - updateRemoteFromSDP(oc.media, oc.log, req.Body()) + updateRemoteFromSDP(oc.media, oc.log, sdpBodyFromRequest(req)) oc.cc.RecordInvite(newCSeq) cc.AcceptAsKeepAlive(localSDP) return nil diff --git a/pkg/sip/signaling_test.go b/pkg/sip/signaling_test.go index 63471143..8744f46f 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/sip/signaling_test.go @@ -663,11 +663,7 @@ func TestReinvite(t *testing.T) { require.Equal(t, serverLocalSDP, resp.Body(), "reinvite 200 OK should return server local SDP") // After the re-INVITE with new offer, the media port destination must be updated. - require.Equal(t, - netip.MustParseAddrPort("9.8.7.6:12345"), - ic.media.RemoteAddr(), - "re-INVITE should redirect RTP to the new remote address", - ) + require.Equal(t, newOffer.Addr, ic.media.RemoteAddr(), "re-INVITE should redirect RTP to the new remote address") }) t.Run("miss", func(t *testing.T) { @@ -698,8 +694,7 @@ func TestReinvite(t *testing.T) { initialRemote := ic.media.RemoteAddr() // Re-INVITE with no SDP body — destination must not change. - req, _, err := call.Invite(nil) - require.NoError(t, err) + req := call.NewRequest(sip.INVITE) // no body, no Content-Type resp := st.TestUA.TransactionRequest(t, req, true) require.Equal(t, sip.StatusCode(200), resp.StatusCode, "body-less re-INVITE should still get 200 OK") require.Equal(t, serverLocalSDP, resp.Body(), "body-less re-INVITE should return server local SDP") @@ -732,11 +727,21 @@ func TestReinvite(t *testing.T) { require.Equal(t, serverLocalSDP, resp.Body(), "reinvite 200 OK should return server local SDP") // After the re-INVITE with new offer, the media port destination must be updated. - require.Equal(t, - netip.MustParseAddrPort("9.8.7.6:12345"), - oc.media.RemoteAddr(), - "re-INVITE should redirect outbound call RTP to the new remote address", - ) + require.Equal(t, newOffer.Addr, oc.media.RemoteAddr(), "re-INVITE should redirect outbound call RTP to the new remote address") + }) + + t.Run("no_body", func(t *testing.T) { + st := NewServiceTest(t, nil) + call, oc, _ := st.CreateOutboundCall(t) + serverLocalSDP := oc.cc.LocalSDP() + initialRemote := oc.media.RemoteAddr() + + // Re-INVITE with no SDP body — destination must not change. + req := call.NewRequest(sip.INVITE) // no body, no Content-Type + resp := st.TestUA.TransactionRequest(t, req, false) + require.Equal(t, sip.StatusCode(200), resp.StatusCode, "body-less re-INVITE should still get 200 OK") + require.Equal(t, serverLocalSDP, resp.Body(), "body-less re-INVITE should return server local SDP") + require.Equal(t, initialRemote, oc.media.RemoteAddr(), "body-less re-INVITE must not change RTP destination") }) t.Run("miss", func(t *testing.T) { From 28281e06d98c1f9a2b3681b6804892beb3fc70bb Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 13:59:02 -0700 Subject: [PATCH 08/11] sip: fix nil pointer panic in SetDst log when first destination is set 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{}. --- pkg/sip/media_port.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index e581f07e..fed864f4 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -241,7 +241,7 @@ func (c *udpConn) SetDst(addr netip.AddrPort) { if addr.IsValid() { prev := c.dst.Swap(&addr) if prev == nil || !prev.IsValid() { - c.log.Infow("setting media destination", "prev", prev, "addr", addr.String()) + c.log.Infow("setting media destination", "prev", netip.AddrPort{}, "addr", addr.String()) } else if *prev != addr { changeCount := c.dstChangeCount.Add(1) now := time.Now().UnixNano() From c71b97f1a39ef77678425e916bea2d615f5510f4 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 14:17:47 -0700 Subject: [PATCH 09/11] sip: fix nil pointer panic in Read log when first source is seen Same class of bug as SetDst: udpConn.Read logged a nil *netip.AddrPort as "prev" on the first packet received. Replace with netip.AddrPort{}. --- pkg/sip/media_port.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index fed864f4..5aaccc6d 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -257,7 +257,7 @@ func (c *udpConn) Read(b []byte) (n int, err error) { n, addr, err := c.ReadFromUDPAddrPort(b) prev := c.src.Swap(&addr) if prev == nil || !prev.IsValid() { - c.log.Infow("setting media source", "prev", prev, "addr", addr.String()) + c.log.Infow("setting media source", "prev", netip.AddrPort{}, "addr", addr.String()) } else if *prev != addr { changeCount := c.srcChangeCount.Add(1) now := time.Now().UnixNano() From a823febd03ae0e0f5ac7edada81c718e726b1b31 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 15:01:59 -0700 Subject: [PATCH 10/11] sip: fix nil pointer panic when logging body-less re-INVITE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pkg/sip/inbound.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 6dc81eb0..2282183e 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -400,7 +400,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE existing := s.byLocalTag[cc.ID()] s.cmu.RUnlock() if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() { - existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) + existing.log().Infow("reinvite", "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) updateRemoteFromSDP(existing.media, existing.log(), sdpBodyFromRequest(req)) cc.AcceptAsKeepAlive(existing.cc.OwnSDP()) return nil @@ -411,7 +411,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE if oc != nil && oc.cc != nil && oc.cc.InviteCSeq() < newCSeq { localSDP := oc.cc.LocalSDP() if len(localSDP) != 0 { - oc.log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) + oc.log.Infow("accepting reinvite", "content-length", req.ContentLength(), "cseq", cc.InviteCSeq()) updateRemoteFromSDP(oc.media, oc.log, sdpBodyFromRequest(req)) oc.cc.RecordInvite(newCSeq) cc.AcceptAsKeepAlive(localSDP) From 60cb302fe586d7dc3336390e8a7b9aba4526c076 Mon Sep 17 00:00:00 2001 From: Victor Uvarov Date: Sun, 21 Jun 2026 15:16:58 -0700 Subject: [PATCH 11/11] sip: drop prev field from first-set log messages in udpConn The "setting media destination/source" branch fires when prev is nil (no previous value). Logging prev as an empty AddrPort was misleading. --- pkg/sip/media_port.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index 5aaccc6d..9ab48b65 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -241,7 +241,7 @@ func (c *udpConn) SetDst(addr netip.AddrPort) { if addr.IsValid() { prev := c.dst.Swap(&addr) if prev == nil || !prev.IsValid() { - c.log.Infow("setting media destination", "prev", netip.AddrPort{}, "addr", addr.String()) + c.log.Infow("setting media destination", "addr", addr.String()) } else if *prev != addr { changeCount := c.dstChangeCount.Add(1) now := time.Now().UnixNano() @@ -257,7 +257,7 @@ func (c *udpConn) Read(b []byte) (n int, err error) { n, addr, err := c.ReadFromUDPAddrPort(b) prev := c.src.Swap(&addr) if prev == nil || !prev.IsValid() { - c.log.Infow("setting media source", "prev", netip.AddrPort{}, "addr", addr.String()) + c.log.Infow("setting media source", "addr", addr.String()) } else if *prev != addr { changeCount := c.srcChangeCount.Add(1) now := time.Now().UnixNano()