Files
ice/candidate_base.go
2025-09-16 10:50:44 -04:00

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
}