diff --git a/selection.go b/selection.go index 0ebd8ced..92da5324 100644 --- a/selection.go +++ b/selection.go @@ -424,6 +424,15 @@ func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remot } func (s *controlledSelector) HandleBindingRequest(message *stun.Message, local, remote Candidate) { //nolint:cyclop + s.handleBindingRequest(message, local, remote, true) +} + +//nolint:cyclop +func (s *controlledSelector) handleBindingRequest( + message *stun.Message, + local, remote Candidate, + pingCandidate bool, +) { pair := s.agent.findPair(local, remote) if pair == nil { pair = s.agent.addPair(local, remote) @@ -481,7 +490,7 @@ func (s *controlledSelector) HandleBindingRequest(message *stun.Message, local, // on every inbound request creates a ping-pong busy loop: the remote side responds // and sends its own request, which triggers another check here, repeating at 1/RTT. // After connection, consent freshness is maintained by checkKeepalive() on a timer. - if pair.state != CandidatePairStateSucceeded || s.agent.getSelectedPair() == nil { + if pingCandidate && (pair.state != CandidatePairStateSucceeded || s.agent.getSelectedPair() == nil) { s.PingCandidate(local, remote) } @@ -496,6 +505,25 @@ type liteSelector struct { pairCandidateSelector } +// A lite selector only acts as a STUN server, so it responds to Binding +// Requests but does not generate triggered checks of its own. +func (s *liteSelector) HandleBindingRequest(message *stun.Message, local, remote Candidate) { + if v, ok := s.pairCandidateSelector.(*controlledSelector); ok { + if message.Contains(stun.AttrUseCandidate) || message.Contains(v.agent.nominationAttribute) { + pair := v.agent.findPair(local, remote) + if pair == nil { + pair = v.agent.addPair(local, remote) + } + pair.state = CandidatePairStateSucceeded + } + v.handleBindingRequest(message, local, remote, false) + + return + } + + s.pairCandidateSelector.HandleBindingRequest(message, local, remote) +} + // A lite selector should not contact candidates. func (s *liteSelector) ContactCandidates() { if _, ok := s.pairCandidateSelector.(*controllingSelector); ok { diff --git a/selection_test.go b/selection_test.go index 0a1c5836..105c8c8c 100644 --- a/selection_test.go +++ b/selection_test.go @@ -465,6 +465,47 @@ func TestControlledSelector_TriggeredCheckDuringChecking(t *testing.T) { "triggered check should be sent during ICE checking phase") } +func TestLiteSelector_NoTriggeredCheckForAcceptedNomination(t *testing.T) { + agent := bareAgentForPing() + agent.log = logging.NewDefaultLoggerFactory().NewLogger("test") + agent.remoteUfrag = selectionTestRemoteUfrag + agent.localUfrag = selectionTestLocalUfrag + agent.remotePwd = selectionTestPassword + agent.localPwd = selectionTestPassword + agent.tieBreaker = 1 + agent.nominationAttribute = DefaultNominationAttribute + agent.onConnected = make(chan struct{}) + + selector := &liteSelector{pairCandidateSelector: &controlledSelector{agent: agent, log: agent.log}} + selector.Start() + + local := newPingNoIOCand() + local.candidateBase.networkType = NetworkTypeUDP4 + local.candidateBase.resolvedAddr = &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 10000} + + remote := newPingNoIOCand() + remote.candidateBase.networkType = NetworkTypeUDP4 + remote.candidateBase.resolvedAddr = &net.UDPAddr{IP: net.ParseIP("192.168.1.2"), Port: 20000} + + msg, err := stun.Build( + stun.BindingRequest, + stun.TransactionID, + stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), + UseCandidate(), + stun.NewShortTermIntegrity(agent.localPwd), + stun.Fingerprint, + ) + require.NoError(t, err) + + selector.HandleBindingRequest(msg, local, remote) + + pair := agent.findPair(local, remote) + require.NotNil(t, pair) + assert.Equal(t, CandidatePairStateSucceeded, pair.state) + assert.Equal(t, pair, agent.getSelectedPair()) + assert.Zero(t, pair.RequestsSent(), "lite agent should not send a triggered check") +} + func TestAutomaticRenomination(t *testing.T) { //nolint:maintidx report := test.CheckRoutines(t) defer report()