status-go/vendor/github.com/pion/ice/v2/selection.go

309 lines
9.8 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ice
import (
"net"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
)
type pairCandidateSelector interface {
Start()
ContactCandidates()
PingCandidate(local, remote Candidate)
HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr)
HandleBindingRequest(m *stun.Message, local, remote Candidate)
}
type controllingSelector struct {
startTime time.Time
agent *Agent
nominatedPair *CandidatePair
log logging.LeveledLogger
}
func (s *controllingSelector) Start() {
s.startTime = time.Now()
s.nominatedPair = nil
}
func (s *controllingSelector) isNominatable(c Candidate) bool {
switch {
case c.Type() == CandidateTypeHost:
return time.Since(s.startTime).Nanoseconds() > s.agent.hostAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypeServerReflexive:
return time.Since(s.startTime).Nanoseconds() > s.agent.srflxAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypePeerReflexive:
return time.Since(s.startTime).Nanoseconds() > s.agent.prflxAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypeRelay:
return time.Since(s.startTime).Nanoseconds() > s.agent.relayAcceptanceMinWait.Nanoseconds()
}
s.log.Errorf("Invalid candidate type: %s", c.Type())
return false
}
func (s *controllingSelector) ContactCandidates() {
switch {
case s.agent.getSelectedPair() != nil:
if s.agent.validateSelectedPair() {
s.log.Trace("Checking keepalive")
s.agent.checkKeepalive()
}
case s.nominatedPair != nil:
s.nominatePair(s.nominatedPair)
default:
p := s.agent.getBestValidCandidatePair()
if p != nil && s.isNominatable(p.Local) && s.isNominatable(p.Remote) {
s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.Local, p.Remote)
p.nominated = true
s.nominatedPair = p
s.nominatePair(p)
return
}
s.agent.pingAllCandidates()
}
}
func (s *controllingSelector) nominatePair(pair *CandidatePair) {
// The controlling agent MUST include the USE-CANDIDATE attribute in
// order to nominate a candidate pair (Section 8.1.1). The controlled
// agent MUST NOT include the USE-CANDIDATE attribute in a Binding
// request.
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
UseCandidate(),
AttrControlling(s.agent.tieBreaker),
PriorityAttr(pair.Local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.log.Tracef("Ping STUN (nominate candidate pair) from %s to %s", pair.Local, pair.Remote)
s.agent.sendBindingRequest(msg, pair.Local, pair.Remote)
}
func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
s.agent.sendBindingSuccess(m, local, remote)
p := s.agent.findPair(local, remote)
if p == nil {
s.agent.addPair(local, remote)
return
}
if p.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.getSelectedPair() == nil {
bestPair := s.agent.getBestAvailableCandidatePair()
if bestPair == nil {
s.log.Tracef("No best pair available")
} else if bestPair.equal(p) && s.isNominatable(p.Local) && s.isNominatable(p.Remote) {
s.log.Tracef("The candidate (%s, %s) is the best candidate available, marking it as nominated", p.Local, p.Remote)
s.nominatedPair = p
s.nominatePair(p)
}
}
if s.agent.userBindingRequestHandler != nil {
if shouldSwitch := s.agent.userBindingRequestHandler(m, local, remote, p); shouldSwitch {
s.agent.setSelectedPair(p)
}
}
}
func (s *controllingSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID)
if !ok {
s.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID)
return
}
transactionAddr := pendingRequest.destination
// Assert that NAT is not symmetric
// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
if !addrEqual(transactionAddr, remoteAddr) {
s.log.Debugf("Discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote)
return
}
s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote, local)
p := s.agent.findPair(local, remote)
if p == nil {
// This shouldn't happen
s.log.Error("Success response from invalid candidate pair")
return
}
p.state = CandidatePairStateSucceeded
s.log.Tracef("Found valid candidate pair: %s", p)
if pendingRequest.isUseCandidate && s.agent.getSelectedPair() == nil {
s.agent.setSelectedPair(p)
}
}
func (s *controllingSelector) PingCandidate(local, remote Candidate) {
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
AttrControlling(s.agent.tieBreaker),
PriorityAttr(local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.agent.sendBindingRequest(msg, local, remote)
}
type controlledSelector struct {
agent *Agent
log logging.LeveledLogger
}
func (s *controlledSelector) Start() {
}
func (s *controlledSelector) ContactCandidates() {
if s.agent.getSelectedPair() != nil {
if s.agent.validateSelectedPair() {
s.log.Trace("Checking keepalive")
s.agent.checkKeepalive()
}
} else {
s.agent.pingAllCandidates()
}
}
func (s *controlledSelector) PingCandidate(local, remote Candidate) {
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
AttrControlled(s.agent.tieBreaker),
PriorityAttr(local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.agent.sendBindingRequest(msg, local, remote)
}
func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
//nolint:godox
// TODO according to the standard we should specifically answer a failed nomination:
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
// If the controlled agent does not accept the request from the
// controlling agent, the controlled agent MUST reject the nomination
// request with an appropriate error code response (e.g., 400)
// [RFC5389].
ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID)
if !ok {
s.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID)
return
}
transactionAddr := pendingRequest.destination
// Assert that NAT is not symmetric
// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
if !addrEqual(transactionAddr, remoteAddr) {
s.log.Debugf("Discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote)
return
}
s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote, local)
p := s.agent.findPair(local, remote)
if p == nil {
// This shouldn't happen
s.log.Error("Success response from invalid candidate pair")
return
}
p.state = CandidatePairStateSucceeded
s.log.Tracef("Found valid candidate pair: %s", p)
if p.nominateOnBindingSuccess {
if selectedPair := s.agent.getSelectedPair(); selectedPair == nil ||
(selectedPair != p && selectedPair.priority() <= p.priority()) {
s.agent.setSelectedPair(p)
} else if selectedPair != p {
s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", p, selectedPair)
}
}
}
func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
p := s.agent.findPair(local, remote)
if p == nil {
p = s.agent.addPair(local, remote)
}
if m.Contains(stun.AttrUseCandidate) {
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
if p.state == CandidatePairStateSucceeded {
// If the state of this pair is Succeeded, it means that the check
// previously sent by this pair produced a successful response and
// generated a valid pair (Section 7.2.5.3.2). The agent sets the
// nominated flag value of the valid pair to true.
selectedPair := s.agent.getSelectedPair()
if selectedPair == nil || (selectedPair != p && selectedPair.priority() <= p.priority()) {
s.agent.setSelectedPair(p)
} else if selectedPair != p {
s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", p, selectedPair)
}
} else {
// If the received Binding request triggered a new check to be
// enqueued in the triggered-check queue (Section 7.3.1.4), once the
// check is sent and if it generates a successful response, and
// generates a valid pair, the agent sets the nominated flag of the
// pair to true. If the request fails (Section 7.2.5.2), the agent
// MUST remove the candidate pair from the valid list, set the
// candidate pair state to Failed, and set the checklist state to
// Failed.
p.nominateOnBindingSuccess = true
}
}
s.agent.sendBindingSuccess(m, local, remote)
s.PingCandidate(local, remote)
if s.agent.userBindingRequestHandler != nil {
if shouldSwitch := s.agent.userBindingRequestHandler(m, local, remote, p); shouldSwitch {
s.agent.setSelectedPair(p)
}
}
}
type liteSelector struct {
pairCandidateSelector
}
// A lite selector should not contact candidates
func (s *liteSelector) ContactCandidates() {
if _, ok := s.pairCandidateSelector.(*controllingSelector); ok {
//nolint:godox
// https://github.com/pion/ice/issues/96
// TODO: implement lite controlling agent. For now falling back to full agent.
// This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2
s.pairCandidateSelector.ContactCandidates()
} else if v, ok := s.pairCandidateSelector.(*controlledSelector); ok {
v.agent.validateSelectedPair()
}
}