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