1package ice 2 3import ( 4 "net" 5 "time" 6 7 "github.com/pion/logging" 8 "github.com/pion/stun" 9) 10 11type pairCandidateSelector interface { 12 Start() 13 ContactCandidates() 14 PingCandidate(local, remote Candidate) 15 HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) 16 HandleBindingRequest(m *stun.Message, local, remote Candidate) 17} 18 19type controllingSelector struct { 20 startTime time.Time 21 agent *Agent 22 nominatedPair *candidatePair 23 nominationRequestCount uint16 24 log logging.LeveledLogger 25} 26 27func (s *controllingSelector) Start() { 28 s.startTime = time.Now() 29 go func() { 30 select { 31 case <-s.agent.done: 32 return 33 case <-time.After(s.agent.candidateSelectionTimeout): 34 } 35 36 err := s.agent.run(func(a *Agent) { 37 if s.nominatedPair == nil { 38 p := s.agent.getBestValidCandidatePair() 39 if p == nil { 40 s.log.Trace("check timeout reached and no valid candidate pair found, marking connection as failed") 41 s.agent.updateConnectionState(ConnectionStateFailed) 42 } else { 43 s.log.Tracef("check timeout reached, nominating (%s, %s)", p.local.String(), p.remote.String()) 44 s.nominatedPair = p 45 s.nominatePair(p) 46 } 47 } 48 }) 49 50 if err != nil { 51 s.log.Errorf("error processing checkCandidatesTimeout handler %v", err.Error()) 52 } 53 }() 54} 55 56func (s *controllingSelector) isNominatable(c Candidate) bool { 57 switch { 58 case c.Type() == CandidateTypeHost: 59 return time.Since(s.startTime).Nanoseconds() > s.agent.hostAcceptanceMinWait.Nanoseconds() 60 case c.Type() == CandidateTypeServerReflexive: 61 return time.Since(s.startTime).Nanoseconds() > s.agent.srflxAcceptanceMinWait.Nanoseconds() 62 case c.Type() == CandidateTypePeerReflexive: 63 return time.Since(s.startTime).Nanoseconds() > s.agent.prflxAcceptanceMinWait.Nanoseconds() 64 case c.Type() == CandidateTypeRelay: 65 return time.Since(s.startTime).Nanoseconds() > s.agent.relayAcceptanceMinWait.Nanoseconds() 66 } 67 68 s.log.Errorf("isNominatable invalid candidate type %s", c.Type().String()) 69 return false 70} 71 72func (s *controllingSelector) ContactCandidates() { 73 switch { 74 case s.agent.selectedPair != nil: 75 if s.agent.validateSelectedPair() { 76 s.log.Trace("checking keepalive") 77 s.agent.checkKeepalive() 78 } 79 case s.nominatedPair != nil: 80 if s.nominationRequestCount > s.agent.maxBindingRequests { 81 s.log.Trace("max nomination requests reached, setting the connection state to failed") 82 s.agent.updateConnectionState(ConnectionStateFailed) 83 return 84 } 85 s.nominatePair(s.nominatedPair) 86 default: 87 p := s.agent.getBestValidCandidatePair() 88 if p != nil && s.isNominatable(p.local) && s.isNominatable(p.remote) { 89 s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.local.String(), p.remote.String()) 90 p.nominated = true 91 s.nominatedPair = p 92 s.nominatePair(p) 93 return 94 } 95 96 s.log.Trace("pinging all candidates") 97 s.agent.pingAllCandidates() 98 } 99} 100 101func (s *controllingSelector) nominatePair(pair *candidatePair) { 102 // The controlling agent MUST include the USE-CANDIDATE attribute in 103 // order to nominate a candidate pair (Section 8.1.1). The controlled 104 // agent MUST NOT include the USE-CANDIDATE attribute in a Binding 105 // request. 106 msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, 107 stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag), 108 UseCandidate, 109 AttrControlling(s.agent.tieBreaker), 110 PriorityAttr(pair.local.Priority()), 111 stun.NewShortTermIntegrity(s.agent.remotePwd), 112 stun.Fingerprint, 113 ) 114 115 if err != nil { 116 s.log.Error(err.Error()) 117 return 118 } 119 120 s.log.Tracef("ping STUN (nominate candidate pair) from %s to %s\n", pair.local.String(), pair.remote.String()) 121 s.agent.sendBindingRequest(msg, pair.local, pair.remote) 122 s.nominationRequestCount++ 123} 124 125func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) { 126 s.agent.sendBindingSuccess(m, local, remote) 127 128 p := s.agent.findPair(local, remote) 129 130 if p == nil { 131 s.agent.addPair(local, remote) 132 return 133 } 134 135 if p.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.selectedPair == nil { 136 bestPair := s.agent.getBestAvailableCandidatePair() 137 if bestPair == nil { 138 s.log.Tracef("No best pair available\n") 139 } else if bestPair.Equal(p) && s.isNominatable(p.local) && s.isNominatable(p.remote) { 140 s.log.Tracef("The candidate (%s, %s) is the best candidate available, marking it as nominated\n", 141 p.local.String(), p.remote.String()) 142 s.nominatedPair = p 143 s.nominatePair(p) 144 } 145 } 146} 147 148func (s *controllingSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) { 149 ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID) 150 if !ok { 151 s.log.Warnf("discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID) 152 return 153 } 154 155 transactionAddr := pendingRequest.destination 156 157 // Assert that NAT is not symmetric 158 // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 159 if !addrEqual(transactionAddr, remoteAddr) { 160 s.log.Debugf("discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote) 161 return 162 } 163 164 s.log.Tracef("inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String()) 165 p := s.agent.findPair(local, remote) 166 167 if p == nil { 168 // This shouldn't happen 169 s.log.Error("Success response from invalid candidate pair") 170 return 171 } 172 173 p.state = CandidatePairStateSucceeded 174 s.log.Tracef("Found valid candidate pair: %s", p) 175 if pendingRequest.isUseCandidate && s.agent.selectedPair == nil { 176 s.agent.setSelectedPair(p) 177 } 178} 179 180func (s *controllingSelector) PingCandidate(local, remote Candidate) { 181 msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, 182 stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag), 183 AttrControlling(s.agent.tieBreaker), 184 PriorityAttr(local.Priority()), 185 stun.NewShortTermIntegrity(s.agent.remotePwd), 186 stun.Fingerprint, 187 ) 188 189 if err != nil { 190 s.log.Error(err.Error()) 191 return 192 } 193 194 s.agent.sendBindingRequest(msg, local, remote) 195} 196 197type controlledSelector struct { 198 startTime time.Time 199 agent *Agent 200 log logging.LeveledLogger 201} 202 203func (s *controlledSelector) Start() { 204 s.startTime = time.Now() 205} 206 207func (s *controlledSelector) ContactCandidates() { 208 if s.agent.selectedPair != nil { 209 if s.agent.validateSelectedPair() { 210 s.log.Trace("checking keepalive") 211 s.agent.checkKeepalive() 212 } 213 } else { 214 if time.Since(s.startTime) > s.agent.candidateSelectionTimeout { 215 s.log.Trace("check timeout reached and no valid candidate pair found, marking connection as failed") 216 s.agent.updateConnectionState(ConnectionStateFailed) 217 } else { 218 s.log.Trace("pinging all candidates") 219 s.agent.pingAllCandidates() 220 } 221 } 222} 223 224func (s *controlledSelector) PingCandidate(local, remote Candidate) { 225 msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, 226 stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag), 227 AttrControlled(s.agent.tieBreaker), 228 PriorityAttr(local.Priority()), 229 stun.NewShortTermIntegrity(s.agent.remotePwd), 230 stun.Fingerprint, 231 ) 232 233 if err != nil { 234 s.log.Error(err.Error()) 235 return 236 } 237 238 s.agent.sendBindingRequest(msg, local, remote) 239} 240 241func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) { 242 // TODO according to the standard we should specifically answer a failed nomination: 243 // https://tools.ietf.org/html/rfc8445#section-7.3.1.5 244 // If the controlled agent does not accept the request from the 245 // controlling agent, the controlled agent MUST reject the nomination 246 // request with an appropriate error code response (e.g., 400) 247 // [RFC5389]. 248 249 ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID) 250 if !ok { 251 s.log.Warnf("discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID) 252 return 253 } 254 255 transactionAddr := pendingRequest.destination 256 257 // Assert that NAT is not symmetric 258 // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 259 if !addrEqual(transactionAddr, remoteAddr) { 260 s.log.Debugf("discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote) 261 return 262 } 263 264 s.log.Tracef("inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String()) 265 266 p := s.agent.findPair(local, remote) 267 if p == nil { 268 // This shouldn't happen 269 s.log.Error("Success response from invalid candidate pair") 270 return 271 } 272 273 p.state = CandidatePairStateSucceeded 274 s.log.Tracef("Found valid candidate pair: %s", p) 275} 276 277func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) { 278 useCandidate := m.Contains(stun.AttrUseCandidate) 279 280 p := s.agent.findPair(local, remote) 281 282 if p == nil { 283 p = s.agent.addPair(local, remote) 284 } 285 286 if useCandidate { 287 // https://tools.ietf.org/html/rfc8445#section-7.3.1.5 288 289 if p.state == CandidatePairStateSucceeded { 290 // If the state of this pair is Succeeded, it means that the check 291 // previously sent by this pair produced a successful response and 292 // generated a valid pair (Section 7.2.5.3.2). The agent sets the 293 // nominated flag value of the valid pair to true. 294 if s.agent.selectedPair == nil { 295 s.agent.setSelectedPair(p) 296 } 297 s.agent.sendBindingSuccess(m, local, remote) 298 } else { 299 // If the received Binding request triggered a new check to be 300 // enqueued in the triggered-check queue (Section 7.3.1.4), once the 301 // check is sent and if it generates a successful response, and 302 // generates a valid pair, the agent sets the nominated flag of the 303 // pair to true. If the request fails (Section 7.2.5.2), the agent 304 // MUST remove the candidate pair from the valid list, set the 305 // candidate pair state to Failed, and set the checklist state to 306 // Failed. 307 s.PingCandidate(local, remote) 308 } 309 } else { 310 s.agent.sendBindingSuccess(m, local, remote) 311 s.PingCandidate(local, remote) 312 } 313} 314 315type liteSelector struct { 316 pairCandidateSelector 317} 318 319// A lite selector should not contact candidates 320func (s *liteSelector) ContactCandidates() { 321 if _, ok := s.pairCandidateSelector.(*controllingSelector); ok { 322 // pion/ice#96 323 // TODO: implement lite controlling agent. For now falling back to full agent. 324 // This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2 325 s.pairCandidateSelector.ContactCandidates() 326 } 327} 328