Skip to content

Commit b82306a

Browse files
committed
Fix detached datachannels handling
pion#2696 introduced removing datachannels from the sctptransport for better garbage collection. That PR introduced a race condition for data channels created before connection establishment. When an out of band negotiated data channel, created before peerconnection establishment is detached, there's a race between the data channel being removed from `r.dataChannels` and it being copied in to the existing data channel slice in the acceptDataChannels goroutine. This PR fixes this race by copying the slice before any datachannels could be detached.
1 parent fbf79c1 commit b82306a

File tree

2 files changed

+128
-7
lines changed

2 files changed

+128
-7
lines changed

sctptransport.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (r *SCTPTransport) Start(_ SCTPCapabilities) error {
142142
r.dataChannelsOpened += openedDCCount
143143
r.lock.Unlock()
144144

145-
go r.acceptDataChannels(sctpAssociation)
145+
go r.acceptDataChannels(sctpAssociation, dataChannels)
146146

147147
return nil
148148
}
@@ -163,10 +163,9 @@ func (r *SCTPTransport) Stop() error {
163163
return nil
164164
}
165165

166-
func (r *SCTPTransport) acceptDataChannels(a *sctp.Association) {
167-
r.lock.RLock()
168-
dataChannels := make([]*datachannel.DataChannel, 0, len(r.dataChannels))
169-
for _, dc := range r.dataChannels {
166+
func (r *SCTPTransport) acceptDataChannels(a *sctp.Association, existingDataChannels []*DataChannel) {
167+
dataChannels := make([]*datachannel.DataChannel, 0, len(existingDataChannels))
168+
for _, dc := range existingDataChannels {
170169
dc.mu.Lock()
171170
isNil := dc.dataChannel == nil
172171
dc.mu.Unlock()
@@ -175,8 +174,6 @@ func (r *SCTPTransport) acceptDataChannels(a *sctp.Association) {
175174
}
176175
dataChannels = append(dataChannels, dc.dataChannel)
177176
}
178-
r.lock.RUnlock()
179-
180177
ACCEPT:
181178
for {
182179
dc, err := datachannel.Accept(a, &datachannel.Config{

sctptransport_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package webrtc
88

99
import (
1010
"bytes"
11+
"sync"
1112
"testing"
1213
"time"
1314

@@ -126,3 +127,126 @@ func TestSCTPTransportOnClose(t *testing.T) {
126127
t.Fatal("timed out")
127128
}
128129
}
130+
131+
func TestSCTPTransportOutOfBandNegotiatedDataChannelDetach(t *testing.T) {
132+
const N = 10
133+
done := make(chan struct{}, N)
134+
for i := 0; i < N; i++ {
135+
go func() {
136+
// Use Detach data channels mode
137+
s := SettingEngine{}
138+
s.DetachDataChannels()
139+
api := NewAPI(WithSettingEngine(s))
140+
141+
// Set up two peer connections.
142+
config := Configuration{}
143+
offerPC, err := api.NewPeerConnection(config)
144+
if err != nil {
145+
t.Error(err)
146+
return
147+
}
148+
answerPC, err := api.NewPeerConnection(config)
149+
if err != nil {
150+
t.Error(err)
151+
return
152+
}
153+
154+
defer closePairNow(t, offerPC, answerPC)
155+
defer func() { done <- struct{}{} }()
156+
157+
negotiated := true
158+
id := uint16(0)
159+
readDetach := make(chan struct{})
160+
dc1, err := offerPC.CreateDataChannel("", &DataChannelInit{
161+
Negotiated: &negotiated,
162+
ID: &id,
163+
})
164+
if err != nil {
165+
t.Error(err)
166+
return
167+
}
168+
dc1.OnOpen(func() {
169+
_, _ = dc1.Detach()
170+
close(readDetach)
171+
})
172+
173+
writeDetach := make(chan struct{})
174+
dc2, err := answerPC.CreateDataChannel("", &DataChannelInit{
175+
Negotiated: &negotiated,
176+
ID: &id,
177+
})
178+
if err != nil {
179+
t.Error(err)
180+
return
181+
}
182+
dc2.OnOpen(func() {
183+
_, _ = dc2.Detach()
184+
close(writeDetach)
185+
})
186+
187+
var wg sync.WaitGroup
188+
wg.Add(2)
189+
go func() {
190+
defer wg.Done()
191+
connestd := make(chan struct{}, 1)
192+
offerPC.OnConnectionStateChange(func(state PeerConnectionState) {
193+
if state == PeerConnectionStateConnected {
194+
connestd <- struct{}{}
195+
}
196+
})
197+
select {
198+
case <-connestd:
199+
case <-time.After(10 * time.Second):
200+
t.Error("conn establishment timed out")
201+
return
202+
}
203+
<-readDetach
204+
err1 := dc1.dataChannel.SetReadDeadline(time.Now().Add(10 * time.Second))
205+
if err1 != nil {
206+
t.Error(err)
207+
return
208+
}
209+
buf := make([]byte, 10)
210+
n, err1 := dc1.dataChannel.Read(buf)
211+
if err1 != nil {
212+
t.Error(err)
213+
return
214+
}
215+
if string(buf[:n]) != "hello" {
216+
t.Error("invalid read")
217+
}
218+
}()
219+
go func() {
220+
defer wg.Done()
221+
connestd := make(chan struct{}, 1)
222+
answerPC.OnConnectionStateChange(func(state PeerConnectionState) {
223+
if state == PeerConnectionStateConnected {
224+
connestd <- struct{}{}
225+
}
226+
})
227+
select {
228+
case <-connestd:
229+
case <-time.After(10 * time.Second):
230+
t.Error("connection establishment timed out")
231+
return
232+
}
233+
<-writeDetach
234+
n, err1 := dc2.dataChannel.Write([]byte("hello"))
235+
if err1 != nil || n != len("hello") {
236+
t.Error(err)
237+
}
238+
}()
239+
err = signalPair(offerPC, answerPC)
240+
require.NoError(t, err)
241+
wg.Wait()
242+
}()
243+
}
244+
245+
for i := 0; i < N; i++ {
246+
select {
247+
case <-done:
248+
case <-time.After(20 * time.Second):
249+
t.Fatal("timed out")
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)