mirror of
https://github.com/pion/ice.git
synced 2025-09-26 19:41:11 +08:00
1068 lines
26 KiB
Go
1068 lines
26 KiB
Go
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ice
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pion/stun/v3"
|
|
)
|
|
|
|
type candidateBase struct {
|
|
id string
|
|
networkType NetworkType
|
|
candidateType CandidateType
|
|
|
|
component uint16
|
|
address string
|
|
port int
|
|
relatedAddress *CandidateRelatedAddress
|
|
tcpType TCPType
|
|
|
|
resolvedAddr net.Addr
|
|
|
|
lastSent atomic.Value
|
|
lastReceived atomic.Value
|
|
conn net.PacketConn
|
|
|
|
currAgent *Agent
|
|
closeCh chan struct{}
|
|
closedCh chan struct{}
|
|
|
|
foundationOverride string
|
|
priorityOverride uint32
|
|
|
|
remoteCandidateCaches map[AddrPort]Candidate
|
|
isLocationTracked bool
|
|
extensions []CandidateExtension
|
|
}
|
|
|
|
// Done implements context.Context.
|
|
func (c *candidateBase) Done() <-chan struct{} {
|
|
return c.closeCh
|
|
}
|
|
|
|
// Err implements context.Context.
|
|
func (c *candidateBase) Err() error {
|
|
select {
|
|
case <-c.closedCh:
|
|
return ErrRunCanceled
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Deadline implements context.Context.
|
|
func (c *candidateBase) Deadline() (deadline time.Time, ok bool) {
|
|
return time.Time{}, false
|
|
}
|
|
|
|
// Value implements context.Context.
|
|
func (c *candidateBase) Value(any) any {
|
|
return nil
|
|
}
|
|
|
|
// ID returns Candidate ID.
|
|
func (c *candidateBase) ID() string {
|
|
return c.id
|
|
}
|
|
|
|
func (c *candidateBase) Foundation() string {
|
|
if c.foundationOverride != "" {
|
|
return c.foundationOverride
|
|
}
|
|
|
|
return fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(c.Type().String()+c.address+c.networkType.String())))
|
|
}
|
|
|
|
// Address returns Candidate Address.
|
|
func (c *candidateBase) Address() string {
|
|
return c.address
|
|
}
|
|
|
|
// Port returns Candidate Port.
|
|
func (c *candidateBase) Port() int {
|
|
return c.port
|
|
}
|
|
|
|
// Type returns candidate type.
|
|
func (c *candidateBase) Type() CandidateType {
|
|
return c.candidateType
|
|
}
|
|
|
|
// NetworkType returns candidate NetworkType.
|
|
func (c *candidateBase) NetworkType() NetworkType {
|
|
return c.networkType
|
|
}
|
|
|
|
// Component returns candidate component.
|
|
func (c *candidateBase) Component() uint16 {
|
|
return c.component
|
|
}
|
|
|
|
func (c *candidateBase) SetComponent(component uint16) {
|
|
c.component = component
|
|
}
|
|
|
|
// LocalPreference returns the local preference for this candidate.
|
|
func (c *candidateBase) LocalPreference() uint16 { //nolint:cyclop
|
|
if c.NetworkType().IsTCP() {
|
|
// RFC 6544, section 4.2
|
|
//
|
|
// In Section 4.1.2.1 of [RFC5245], a recommended formula for UDP ICE
|
|
// candidate prioritization is defined. For TCP candidates, the same
|
|
// formula and candidate type preferences SHOULD be used, and the
|
|
// RECOMMENDED type preferences for the new candidate types defined in
|
|
// this document (see Section 5) are 105 for NAT-assisted candidates and
|
|
// 75 for UDP-tunneled candidates.
|
|
//
|
|
// (...)
|
|
//
|
|
// With TCP candidates, the local preference part of the recommended
|
|
// priority formula is updated to also include the directionality
|
|
// (active, passive, or simultaneous-open) of the TCP connection. The
|
|
// RECOMMENDED local preference is then defined as:
|
|
//
|
|
// local preference = (2^13) * direction-pref + other-pref
|
|
//
|
|
// The direction-pref MUST be between 0 and 7 (both inclusive), with 7
|
|
// being the most preferred. The other-pref MUST be between 0 and 8191
|
|
// (both inclusive), with 8191 being the most preferred. It is
|
|
// RECOMMENDED that the host, UDP-tunneled, and relayed TCP candidates
|
|
// have the direction-pref assigned as follows: 6 for active, 4 for
|
|
// passive, and 2 for S-O. For the NAT-assisted and server reflexive
|
|
// candidates, the RECOMMENDED values are: 6 for S-O, 4 for active, and
|
|
// 2 for passive.
|
|
//
|
|
// (...)
|
|
//
|
|
// If any two candidates have the same type-preference and direction-
|
|
// pref, they MUST have a unique other-pref. With this specification,
|
|
// this usually only happens with multi-homed hosts, in which case
|
|
// other-pref is the preference for the particular IP address from which
|
|
// the candidate was obtained. When there is only a single IP address,
|
|
// this value SHOULD be set to the maximum allowed value (8191).
|
|
var otherPref uint16 = 8191
|
|
|
|
directionPref := func() uint16 {
|
|
switch c.Type() {
|
|
case CandidateTypeHost, CandidateTypeRelay:
|
|
switch c.tcpType {
|
|
case TCPTypeActive:
|
|
return 6
|
|
case TCPTypePassive:
|
|
return 4
|
|
case TCPTypeSimultaneousOpen:
|
|
return 2
|
|
case TCPTypeUnspecified:
|
|
return 0
|
|
}
|
|
case CandidateTypePeerReflexive, CandidateTypeServerReflexive:
|
|
switch c.tcpType {
|
|
case TCPTypeSimultaneousOpen:
|
|
return 6
|
|
case TCPTypeActive:
|
|
return 4
|
|
case TCPTypePassive:
|
|
return 2
|
|
case TCPTypeUnspecified:
|
|
return 0
|
|
}
|
|
case CandidateTypeUnspecified:
|
|
return 0
|
|
}
|
|
|
|
return 0
|
|
}()
|
|
|
|
return (1<<13)*directionPref + otherPref
|
|
}
|
|
|
|
return defaultLocalPreference
|
|
}
|
|
|
|
// RelatedAddress returns *CandidateRelatedAddress.
|
|
func (c *candidateBase) RelatedAddress() *CandidateRelatedAddress {
|
|
return c.relatedAddress
|
|
}
|
|
|
|
func (c *candidateBase) TCPType() TCPType {
|
|
return c.tcpType
|
|
}
|
|
|
|
// start runs the candidate using the provided connection.
|
|
func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{}) {
|
|
if c.conn != nil {
|
|
c.agent().log.Warn("Can't start already started candidateBase")
|
|
|
|
return
|
|
}
|
|
c.currAgent = a
|
|
c.conn = conn
|
|
c.closeCh = make(chan struct{})
|
|
c.closedCh = make(chan struct{})
|
|
|
|
go c.recvLoop(initializedCh)
|
|
}
|
|
|
|
var bufferPool = sync.Pool{ // nolint:gochecknoglobals
|
|
New: func() any {
|
|
return make([]byte, receiveMTU)
|
|
},
|
|
}
|
|
|
|
func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) {
|
|
agent := c.agent()
|
|
|
|
defer close(c.closedCh)
|
|
|
|
select {
|
|
case <-initializedCh:
|
|
case <-c.closeCh:
|
|
return
|
|
}
|
|
|
|
bufferPoolBuffer := bufferPool.Get()
|
|
defer bufferPool.Put(bufferPoolBuffer)
|
|
buf, ok := bufferPoolBuffer.([]byte)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for {
|
|
n, srcAddr, err := c.conn.ReadFrom(buf)
|
|
if err != nil {
|
|
if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
|
agent.log.Warnf("Failed to read from candidate %s: %v", c, err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
c.handleInboundPacket(buf[:n], srcAddr)
|
|
}
|
|
}
|
|
|
|
func (c *candidateBase) validateSTUNTrafficCache(addr net.Addr) bool {
|
|
if candidate, ok := c.remoteCandidateCaches[toAddrPort(addr)]; ok {
|
|
candidate.seen(false)
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *candidateBase) addRemoteCandidateCache(candidate Candidate, srcAddr net.Addr) {
|
|
if c.validateSTUNTrafficCache(srcAddr) {
|
|
return
|
|
}
|
|
c.remoteCandidateCaches[toAddrPort(srcAddr)] = candidate
|
|
}
|
|
|
|
func (c *candidateBase) handleInboundPacket(buf []byte, srcAddr net.Addr) {
|
|
agent := c.agent()
|
|
|
|
if stun.IsMessage(buf) {
|
|
msg := &stun.Message{
|
|
Raw: make([]byte, len(buf)),
|
|
}
|
|
|
|
// Explicitly copy raw buffer so Message can own the memory.
|
|
copy(msg.Raw, buf)
|
|
|
|
if err := msg.Decode(); err != nil {
|
|
agent.log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
|
|
|
|
return
|
|
}
|
|
|
|
if err := agent.loop.Run(c, func(_ context.Context) {
|
|
// nolint: contextcheck
|
|
agent.handleInbound(msg, c, srcAddr)
|
|
}); err != nil {
|
|
agent.log.Warnf("Failed to handle message: %v", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if !c.validateSTUNTrafficCache(srcAddr) {
|
|
remoteCandidate, valid := agent.validateNonSTUNTraffic(c, srcAddr) //nolint:contextcheck
|
|
if !valid {
|
|
agent.log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr())
|
|
|
|
return
|
|
}
|
|
c.addRemoteCandidateCache(remoteCandidate, srcAddr)
|
|
}
|
|
|
|
// Note: This will return packetio.ErrFull if the buffer ever manages to fill up.
|
|
n, err := agent.buf.Write(buf)
|
|
if err != nil {
|
|
agent.log.Warnf("Failed to write packet: %s", err)
|
|
|
|
return
|
|
}
|
|
|
|
// Add received application bytes to the currently selected candidate pair.
|
|
if n > 0 {
|
|
if sp := agent.getSelectedPair(); sp != nil {
|
|
sp.UpdatePacketReceived(n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// close stops the recvLoop.
|
|
func (c *candidateBase) close() error {
|
|
// If conn has never been started will be nil
|
|
if c.Done() == nil {
|
|
return nil
|
|
}
|
|
|
|
// Assert that conn has not already been closed
|
|
select {
|
|
case <-c.Done():
|
|
return nil
|
|
default:
|
|
}
|
|
|
|
var firstErr error
|
|
|
|
// Unblock recvLoop
|
|
close(c.closeCh)
|
|
if err := c.conn.SetDeadline(time.Now()); err != nil {
|
|
firstErr = err
|
|
}
|
|
|
|
// Close the conn
|
|
if err := c.conn.Close(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
|
|
if firstErr != nil {
|
|
return firstErr
|
|
}
|
|
|
|
// Wait until the recvLoop is closed
|
|
<-c.closedCh
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *candidateBase) writeTo(raw []byte, dst Candidate) (int, error) {
|
|
n, err := c.conn.WriteTo(raw, dst.addr())
|
|
if err != nil {
|
|
// If the connection is closed, we should return the error
|
|
if errors.Is(err, io.ErrClosedPipe) {
|
|
return n, err
|
|
}
|
|
c.agent().log.Infof("Failed to send packet: %v", err)
|
|
|
|
return n, nil
|
|
}
|
|
c.seen(true)
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// TypePreference returns the type preference for this candidate.
|
|
func (c *candidateBase) TypePreference() uint16 {
|
|
pref := c.Type().Preference()
|
|
if pref == 0 {
|
|
return 0
|
|
}
|
|
|
|
if c.NetworkType().IsTCP() {
|
|
var tcpPriorityOffset uint16 = defaultTCPPriorityOffset
|
|
if c.agent() != nil {
|
|
tcpPriorityOffset = c.agent().tcpPriorityOffset
|
|
}
|
|
|
|
pref -= tcpPriorityOffset
|
|
}
|
|
|
|
return pref
|
|
}
|
|
|
|
// Priority computes the priority for this ICE Candidate
|
|
// See: https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1
|
|
func (c *candidateBase) Priority() uint32 {
|
|
if c.priorityOverride != 0 {
|
|
return c.priorityOverride
|
|
}
|
|
|
|
// The local preference MUST be an integer from 0 (lowest preference) to
|
|
// 65535 (highest preference) inclusive. When there is only a single IP
|
|
// address, this value SHOULD be set to 65535. If there are multiple
|
|
// candidates for a particular component for a particular data stream
|
|
// that have the same type, the local preference MUST be unique for each
|
|
// one.
|
|
|
|
return (1<<24)*uint32(c.TypePreference()) +
|
|
(1<<8)*uint32(c.LocalPreference()) +
|
|
(1<<0)*uint32(256-c.Component())
|
|
}
|
|
|
|
// Equal is used to compare two candidateBases.
|
|
func (c *candidateBase) Equal(other Candidate) bool {
|
|
if c.addr() != other.addr() {
|
|
if c.addr() == nil || other.addr() == nil {
|
|
return false
|
|
}
|
|
if !addrEqual(c.addr(), other.addr()) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return c.NetworkType() == other.NetworkType() &&
|
|
c.Type() == other.Type() &&
|
|
c.Address() == other.Address() &&
|
|
c.Port() == other.Port() &&
|
|
c.TCPType() == other.TCPType() &&
|
|
c.RelatedAddress().Equal(other.RelatedAddress())
|
|
}
|
|
|
|
// DeepEqual is same as Equal but also compares the extensions.
|
|
func (c *candidateBase) DeepEqual(other Candidate) bool {
|
|
return c.Equal(other) && c.extensionsEqual(other.Extensions())
|
|
}
|
|
|
|
// String makes the candidateBase printable.
|
|
func (c *candidateBase) String() string {
|
|
return fmt.Sprintf(
|
|
"%s %s %s%s (resolved: %v)",
|
|
c.NetworkType(),
|
|
c.Type(),
|
|
net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())),
|
|
c.relatedAddress,
|
|
c.resolvedAddr,
|
|
)
|
|
}
|
|
|
|
// LastReceived returns a time.Time indicating the last time
|
|
// this candidate was received.
|
|
func (c *candidateBase) LastReceived() time.Time {
|
|
if lastReceived, ok := c.lastReceived.Load().(time.Time); ok {
|
|
return lastReceived
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (c *candidateBase) setLastReceived(t time.Time) {
|
|
c.lastReceived.Store(t)
|
|
}
|
|
|
|
// LastSent returns a time.Time indicating the last time
|
|
// this candidate was sent.
|
|
func (c *candidateBase) LastSent() time.Time {
|
|
if lastSent, ok := c.lastSent.Load().(time.Time); ok {
|
|
return lastSent
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (c *candidateBase) setLastSent(t time.Time) {
|
|
c.lastSent.Store(t)
|
|
}
|
|
|
|
func (c *candidateBase) seen(outbound bool) {
|
|
if outbound {
|
|
c.setLastSent(time.Now())
|
|
} else {
|
|
c.setLastReceived(time.Now())
|
|
}
|
|
}
|
|
|
|
func (c *candidateBase) addr() net.Addr {
|
|
return c.resolvedAddr
|
|
}
|
|
|
|
func (c *candidateBase) filterForLocationTracking() bool {
|
|
return c.isLocationTracked
|
|
}
|
|
|
|
func (c *candidateBase) agent() *Agent {
|
|
return c.currAgent
|
|
}
|
|
|
|
func (c *candidateBase) context() context.Context {
|
|
return c
|
|
}
|
|
|
|
func (c *candidateBase) copy() (Candidate, error) {
|
|
return UnmarshalCandidate(c.Marshal())
|
|
}
|
|
|
|
func removeZoneIDFromAddress(addr string) string {
|
|
if i := strings.Index(addr, "%"); i != -1 {
|
|
return addr[:i]
|
|
}
|
|
|
|
return addr
|
|
}
|
|
|
|
// Marshal returns the string representation of the ICECandidate.
|
|
func (c *candidateBase) Marshal() string {
|
|
val := c.Foundation()
|
|
if val == " " {
|
|
val = ""
|
|
}
|
|
|
|
val = fmt.Sprintf("%s %d %s %d %s %d typ %s",
|
|
val,
|
|
c.Component(),
|
|
c.NetworkType().NetworkShort(),
|
|
c.Priority(),
|
|
removeZoneIDFromAddress(c.Address()),
|
|
c.Port(),
|
|
c.Type())
|
|
|
|
if r := c.RelatedAddress(); r != nil && r.Address != "" && r.Port != 0 {
|
|
val = fmt.Sprintf("%s raddr %s rport %d",
|
|
val,
|
|
r.Address,
|
|
r.Port)
|
|
}
|
|
|
|
extensions := c.marshalExtensions()
|
|
|
|
if extensions != "" {
|
|
val = fmt.Sprintf("%s %s", val, extensions)
|
|
}
|
|
|
|
return val
|
|
}
|
|
|
|
// CandidateExtension represents a single candidate extension
|
|
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
|
|
// .
|
|
type CandidateExtension struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
func (c *candidateBase) Extensions() []CandidateExtension {
|
|
tcpType := c.TCPType()
|
|
hasTCPType := 0
|
|
if tcpType != TCPTypeUnspecified {
|
|
hasTCPType = 1
|
|
}
|
|
|
|
extensions := make([]CandidateExtension, len(c.extensions)+hasTCPType)
|
|
// We store the TCPType in c.tcpType, but we need to return it as an extension.
|
|
if hasTCPType == 1 {
|
|
extensions[0] = CandidateExtension{
|
|
Key: "tcptype",
|
|
Value: tcpType.String(),
|
|
}
|
|
}
|
|
|
|
copy(extensions[hasTCPType:], c.extensions)
|
|
|
|
return extensions
|
|
}
|
|
|
|
// Get returns the value of the given key if it exists.
|
|
func (c *candidateBase) GetExtension(key string) (CandidateExtension, bool) {
|
|
extension := CandidateExtension{Key: key}
|
|
|
|
for i := range c.extensions {
|
|
if c.extensions[i].Key == key {
|
|
extension.Value = c.extensions[i].Value
|
|
|
|
return extension, true
|
|
}
|
|
}
|
|
|
|
// TCPType was manually set.
|
|
if key == "tcptype" && c.TCPType() != TCPTypeUnspecified { //nolint:goconst
|
|
extension.Value = c.TCPType().String()
|
|
|
|
return extension, true
|
|
}
|
|
|
|
return extension, false
|
|
}
|
|
|
|
func (c *candidateBase) AddExtension(ext CandidateExtension) error {
|
|
if ext.Key == "tcptype" {
|
|
tcpType := NewTCPType(ext.Value)
|
|
if tcpType == TCPTypeUnspecified {
|
|
return fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, ext.Value)
|
|
}
|
|
|
|
c.tcpType = tcpType
|
|
|
|
return nil
|
|
}
|
|
|
|
if ext.Key == "" {
|
|
return fmt.Errorf("%w: key is empty", errParseExtension)
|
|
}
|
|
|
|
// per spec, Extensions aren't explicitly unique, we only set the first one.
|
|
// If the exteion is set multiple times.
|
|
for i := range c.extensions {
|
|
if c.extensions[i].Key == ext.Key {
|
|
c.extensions[i] = ext
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
c.extensions = append(c.extensions, ext)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *candidateBase) RemoveExtension(key string) (ok bool) {
|
|
if key == "tcptype" {
|
|
c.tcpType = TCPTypeUnspecified
|
|
ok = true
|
|
}
|
|
|
|
for i := range c.extensions {
|
|
if c.extensions[i].Key == key {
|
|
c.extensions = append(c.extensions[:i], c.extensions[i+1:]...)
|
|
ok = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return ok
|
|
}
|
|
|
|
// marshalExtensions returns the string representation of the candidate extensions.
|
|
func (c *candidateBase) marshalExtensions() string {
|
|
value := ""
|
|
exts := c.Extensions()
|
|
|
|
for i := range exts {
|
|
if value != "" {
|
|
value += " "
|
|
}
|
|
|
|
value += exts[i].Key + " " + exts[i].Value
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
// Equal returns true if the candidate extensions are equal.
|
|
func (c *candidateBase) extensionsEqual(other []CandidateExtension) bool {
|
|
freq1 := make(map[CandidateExtension]int)
|
|
freq2 := make(map[CandidateExtension]int)
|
|
|
|
if len(c.extensions) != len(other) {
|
|
return false
|
|
}
|
|
|
|
if len(c.extensions) == 0 {
|
|
return true
|
|
}
|
|
|
|
if len(c.extensions) == 1 {
|
|
return c.extensions[0] == other[0]
|
|
}
|
|
|
|
for i := range c.extensions {
|
|
freq1[c.extensions[i]]++
|
|
freq2[other[i]]++
|
|
}
|
|
|
|
for k, v := range freq1 {
|
|
if freq2[k] != v {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (c *candidateBase) setExtensions(extensions []CandidateExtension) {
|
|
c.extensions = extensions
|
|
}
|
|
|
|
// UnmarshalCandidate Parses a candidate from a string
|
|
// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
|
func UnmarshalCandidate(raw string) (Candidate, error) { //nolint:cyclop
|
|
// Handle candidates with the "candidate:" prefix as defined in RFC 5245 section 15.1.
|
|
raw = strings.TrimPrefix(raw, "candidate:")
|
|
|
|
pos := 0
|
|
// foundation ( 1*32ice-char ) But we allow for empty foundation,
|
|
foundation, pos, err := readCandidateCharToken(raw, pos, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v in %s", errParseFoundation, err, raw) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
// Empty foundation, not RFC 8445 compliant but seen in the wild
|
|
if foundation == "" {
|
|
foundation = " "
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected component in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// component-id ( 1*5DIGIT )
|
|
component, pos, err := readCandidateDigitToken(raw, pos, 5)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v in %s", errParseComponent, err, raw) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected transport in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// transport ( "UDP" / transport-extension ; from RFC 3261 ) SP
|
|
protocol, pos := readCandidateStringToken(raw, pos)
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected priority in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// priority ( 1*10DIGIT ) SP
|
|
priority, pos, err := readCandidateDigitToken(raw, pos, 10)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v in %s", errParsePriority, err, raw) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected address in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// connection-address SP ;from RFC 4566
|
|
address, pos := readCandidateStringToken(raw, pos)
|
|
|
|
// Remove IPv6 ZoneID: https://github.com/pion/ice/pull/704
|
|
address = removeZoneIDFromAddress(address)
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected port in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// port from RFC 4566
|
|
port, pos, err := readCandidatePort(raw, pos)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v in %s", errParsePort, err, raw) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
// "typ" SP
|
|
typeKey, pos := readCandidateStringToken(raw, pos)
|
|
if typeKey != "typ" {
|
|
return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typeKey)
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return nil, fmt.Errorf("%w: expected candidate type in %s", errAttributeTooShortICECandidate, raw)
|
|
}
|
|
|
|
// SP cand-type ("host" / "srflx" / "prflx" / "relay")
|
|
typ, pos := readCandidateStringToken(raw, pos)
|
|
|
|
raddr, rport, pos, err := tryReadRelativeAddrs(raw, pos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tcpType := TCPTypeUnspecified
|
|
var extensions []CandidateExtension
|
|
var tcpTypeRaw string
|
|
|
|
if pos < len(raw) {
|
|
extensions, tcpTypeRaw, err = unmarshalCandidateExtensions(raw[pos:])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", errParseExtension, err) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
if tcpTypeRaw != "" {
|
|
tcpType = NewTCPType(tcpTypeRaw)
|
|
if tcpType == TCPTypeUnspecified {
|
|
return nil, fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, tcpTypeRaw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// this code is ugly because we can't break backwards compatibility
|
|
// with the old way of parsing candidates
|
|
switch typ {
|
|
case "host":
|
|
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
|
"",
|
|
protocol,
|
|
address,
|
|
port,
|
|
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
|
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
|
foundation,
|
|
tcpType,
|
|
false,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidate.setExtensions(extensions)
|
|
|
|
return candidate, nil
|
|
case "srflx":
|
|
candidate, err := NewCandidateServerReflexive(&CandidateServerReflexiveConfig{
|
|
"",
|
|
protocol,
|
|
address,
|
|
port,
|
|
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
|
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
|
foundation,
|
|
raddr,
|
|
rport,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidate.setExtensions(extensions)
|
|
|
|
return candidate, nil
|
|
case "prflx":
|
|
candidate, err := NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{
|
|
"",
|
|
protocol,
|
|
address,
|
|
port,
|
|
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
|
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
|
foundation,
|
|
raddr,
|
|
rport,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidate.setExtensions(extensions)
|
|
|
|
return candidate, nil
|
|
case "relay":
|
|
candidate, err := NewCandidateRelay(&CandidateRelayConfig{
|
|
"",
|
|
protocol,
|
|
address,
|
|
port,
|
|
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
|
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
|
foundation,
|
|
raddr,
|
|
rport,
|
|
"",
|
|
nil,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidate.setExtensions(extensions)
|
|
|
|
return candidate, nil
|
|
default:
|
|
return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ)
|
|
}
|
|
}
|
|
|
|
// Read an ice-char token from the raw string
|
|
// ice-char = ALPHA / DIGIT / "+" / "/"
|
|
// stop reading when a space is encountered or the end of the string.
|
|
func readCandidateCharToken(raw string, start int, limit int) (string, int, error) { //nolint:cyclop
|
|
for i, char := range raw[start:] {
|
|
if char == 0x20 { // SP
|
|
return raw[start : start+i], start + i + 1, nil
|
|
}
|
|
|
|
if i == limit {
|
|
//nolint: err113 // handled by caller
|
|
return "", 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit)
|
|
}
|
|
|
|
if (char < 'A' || char > 'Z') &&
|
|
(char < 'a' || char > 'z') &&
|
|
(char < '0' || char > '9') &&
|
|
char != '+' && char != '/' {
|
|
return "", 0, fmt.Errorf("invalid ice-char token: %c", char) //nolint: err113 // handled by caller
|
|
}
|
|
}
|
|
|
|
return raw[start:], len(raw), nil
|
|
}
|
|
|
|
// Read an ice string token from the raw string until a space is encountered
|
|
// Or the end of the string, we imply that ice string are UTF-8 encoded.
|
|
func readCandidateStringToken(raw string, start int) (string, int) {
|
|
for i, char := range raw[start:] {
|
|
if char == 0x20 { // SP
|
|
return raw[start : start+i], start + i + 1
|
|
}
|
|
}
|
|
|
|
return raw[start:], len(raw)
|
|
}
|
|
|
|
// Read a digit token from the raw string
|
|
// stop reading when a space is encountered or the end of the string.
|
|
func readCandidateDigitToken(raw string, start, limit int) (int, int, error) {
|
|
var val int
|
|
for i, char := range raw[start:] {
|
|
if char == 0x20 { // SP
|
|
return val, start + i + 1, nil
|
|
}
|
|
|
|
if i == limit {
|
|
//nolint: err113 // handled by caller
|
|
return 0, 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit)
|
|
}
|
|
|
|
if char < '0' || char > '9' {
|
|
return 0, 0, fmt.Errorf("invalid digit token: %c", char) //nolint: err113 // handled by caller
|
|
}
|
|
|
|
val = val*10 + int(char-'0')
|
|
}
|
|
|
|
return val, len(raw), nil
|
|
}
|
|
|
|
// Read and validate RFC 4566 port from the raw string.
|
|
func readCandidatePort(raw string, start int) (int, int, error) {
|
|
port, pos, err := readCandidateDigitToken(raw, start, 5)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
if port > 65535 {
|
|
return 0, 0, fmt.Errorf("invalid RFC 4566 port %d", port) //nolint: err113 // handled by caller
|
|
}
|
|
|
|
return port, pos, nil
|
|
}
|
|
|
|
// Read a byte-string token from the raw string
|
|
// As defined in RFC 4566 1*(%x01-09/%x0B-0C/%x0E-FF) ;any byte except NUL, CR, or LF
|
|
// we imply that extensions byte-string are UTF-8 encoded.
|
|
func readCandidateByteString(raw string, start int) (string, int, error) {
|
|
for i, char := range raw[start:] {
|
|
if char == 0x20 { // SP
|
|
return raw[start : start+i], start + i + 1, nil
|
|
}
|
|
|
|
// 1*(%x01-09/%x0B-0C/%x0E-FF)
|
|
if (char < 0x01 || char > 0x09) &&
|
|
(char < 0x0B || char > 0x0C) &&
|
|
(char < 0x0E || char > 0xFF) {
|
|
return "", 0, fmt.Errorf("invalid byte-string character: %c", char) //nolint: err113 // handled by caller
|
|
}
|
|
}
|
|
|
|
return raw[start:], len(raw), nil
|
|
}
|
|
|
|
// Read and validate raddr and rport from the raw string
|
|
// [SP rel-addr] [SP rel-port]
|
|
// defined in https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
|
// .
|
|
func tryReadRelativeAddrs(raw string, start int) (raddr string, rport, pos int, err error) {
|
|
key, pos := readCandidateStringToken(raw, start)
|
|
|
|
if key != "raddr" {
|
|
return "", 0, start, nil
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return "", 0, 0, fmt.Errorf("%w: expected raddr value in %s", errParseRelatedAddr, raw)
|
|
}
|
|
|
|
raddr, pos = readCandidateStringToken(raw, pos)
|
|
|
|
if pos >= len(raw) {
|
|
return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
|
|
}
|
|
|
|
key, pos = readCandidateStringToken(raw, pos)
|
|
if key != "rport" {
|
|
return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
|
|
}
|
|
|
|
if pos >= len(raw) {
|
|
return "", 0, 0, fmt.Errorf("%w: expected rport value in %s", errParseRelatedAddr, raw)
|
|
}
|
|
|
|
rport, pos, err = readCandidatePort(raw, pos)
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("%w: %v", errParseRelatedAddr, err) //nolint:errorlint // we wrap the error
|
|
}
|
|
|
|
return raddr, rport, pos, nil
|
|
}
|
|
|
|
// UnmarshalCandidateExtensions parses the candidate extensions from the raw string.
|
|
// *(SP extension-att-name SP extension-att-value)
|
|
// Where extension-att-name, and extension-att-value are byte-strings
|
|
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
|
|
func unmarshalCandidateExtensions(raw string) (extensions []CandidateExtension, rawTCPTypeRaw string, err error) {
|
|
extensions = make([]CandidateExtension, 0)
|
|
|
|
if raw == "" {
|
|
return extensions, "", nil
|
|
}
|
|
|
|
if raw[0] == 0x20 { // SP
|
|
return extensions, "", fmt.Errorf("%w: unexpected space %s", errParseExtension, raw)
|
|
}
|
|
|
|
for i := 0; i < len(raw); {
|
|
key, next, err := readCandidateByteString(raw, i)
|
|
if err != nil {
|
|
return extensions, "", fmt.Errorf(
|
|
"%w: failed to read key %v", errParseExtension, err, //nolint: errorlint // we wrap the error
|
|
)
|
|
}
|
|
i = next
|
|
|
|
// while not spec-compliant, we allow for empty values, as seen in the wild
|
|
var value string
|
|
if i < len(raw) {
|
|
value, next, err = readCandidateByteString(raw, i)
|
|
if err != nil {
|
|
return extensions, "", fmt.Errorf(
|
|
"%w: failed to read value %v", errParseExtension, err, //nolint: errorlint // we are wrapping the error
|
|
)
|
|
}
|
|
i = next
|
|
}
|
|
|
|
if key == "tcptype" {
|
|
rawTCPTypeRaw = value
|
|
|
|
continue
|
|
}
|
|
|
|
extensions = append(extensions, CandidateExtension{key, value})
|
|
}
|
|
|
|
return extensions, rawTCPTypeRaw, nil
|
|
}
|