Skip to main content

Overview

Server reflexive candidates (srflx) are discovered using STUN (Session Traversal Utilities for NAT) servers. They represent the public IP address and port mapping allocated by a NAT device, enabling peer-to-peer connections across NATs.

CandidateServerReflexive

A server reflexive candidate represents a public address binding allocated by a NAT, discovered through STUN.
candidate_server_reflexive.go
type CandidateServerReflexive struct {
    candidateBase
}
Server reflexive candidates embed candidateBase which provides common functionality like priority calculation, marshaling, and traffic tracking.

Creating Server Reflexive Candidates

NewCandidateServerReflexive

Creates a new server reflexive candidate from the provided configuration.
func NewCandidateServerReflexive(config *CandidateServerReflexiveConfig) (*CandidateServerReflexive, error)
config
*CandidateServerReflexiveConfig
required
Configuration for the server reflexive candidate.
Returns the created CandidateServerReflexive or an error if the configuration is invalid (e.g., invalid IP address).

CandidateServerReflexiveConfig

Configuration structure for creating server reflexive candidates.
candidate_server_reflexive.go
type CandidateServerReflexiveConfig struct {
    CandidateID string
    Network     string
    Address     string
    Port        int
    Component   uint16
    Priority    uint32
    Foundation  string
    RelAddr     string
    RelPort     int
}

Configuration Fields

CandidateID
string
Unique identifier for the candidate. If empty, a UUID will be automatically generated.
Network
string
required
Network protocol: “udp”, “tcp”, “udp4”, “udp6”, “tcp4”, or “tcp6”.
Address
string
required
The public IP address discovered via STUN (the reflexive address).
Port
int
required
Port number of the public address.
Component
uint16
required
Component identifier. Use ComponentRTP (1) for RTP or ComponentRTCP (2) for RTCP.
Priority
uint32
Custom priority value. If 0, priority will be calculated automatically. Server reflexive candidates have a type preference of 100.
Foundation
string
Custom foundation string. If empty, foundation will be calculated as a CRC32 checksum of the candidate type, address, and network type.
RelAddr
string
required
The base (local) IP address from which the STUN request was sent.
RelPort
int
required
The port of the base (local) address.

Examples

Creating a Server Reflexive Candidate

candidate, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
    Network:   "udp",
    Address:   "203.0.113.1",
    Port:      54321,
    Component: ice.ComponentRTP,
    RelAddr:   "192.168.1.100",
    RelPort:   50000,
})
if err != nil {
    log.Fatal(err)
}

fmt.Println("Candidate:", candidate.Marshal())
// Output: candidate:842163049 1 udp 1677729535 203.0.113.1 54321 typ srflx raddr 192.168.1.100 rport 50000
candidate, _ := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
    Network:   "udp",
    Address:   "203.0.113.1",
    Port:      54321,
    Component: ice.ComponentRTP,
    RelAddr:   "192.168.1.100",
    RelPort:   50000,
})

relatedAddr := candidate.RelatedAddress()
if relatedAddr != nil {
    fmt.Printf("Public: %s:%d\n", candidate.Address(), candidate.Port())
    fmt.Printf("Local: %s:%d\n", relatedAddr.Address, relatedAddr.Port)
}
// Output:
// Public: 203.0.113.1:54321
// Local: 192.168.1.100:50000

Creating an IPv6 Server Reflexive Candidate

candidate, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
    Network:   "udp6",
    Address:   "2001:db8::1",
    Port:      54321,
    Component: ice.ComponentRTP,
    RelAddr:   "fe80::1",
    RelPort:   50000,
})
if err != nil {
    log.Fatal(err)
}

fmt.Println("Network Type:", candidate.NetworkType())
// Output: Network Type: udp6

STUN Usage

Server reflexive candidates are discovered through the STUN protocol. The typical discovery process is:
  1. Send STUN Binding Request - Send a STUN request from a local interface to a STUN server
  2. Receive STUN Response - The STUN server responds with the public IP and port it observed
  3. Create Candidate - Create a server reflexive candidate with the discovered address
  4. Set Related Address - Record the local interface as the related address

Example STUN Integration

import (
    "github.com/pion/ice/v4"
    "github.com/pion/stun/v3"
)

// Local address
localAddr := "192.168.1.100:50000"

// STUN server
stunServer := "stun.l.google.com:19302"

// Send STUN binding request
conn, err := net.Dial("udp", stunServer)
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
if _, err := conn.Write(message.Raw); err != nil {
    log.Fatal(err)
}

// Read STUN response
buf := make([]byte, 1500)
n, err := conn.Read(buf)
if err != nil {
    log.Fatal(err)
}

// Parse response
var response stun.Message
response.Raw = buf[:n]
if err := response.Decode(); err != nil {
    log.Fatal(err)
}

// Extract mapped address
var xorAddr stun.XORMappedAddress
if err := xorAddr.GetFrom(&response); err != nil {
    log.Fatal(err)
}

// Create server reflexive candidate
candidate, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
    Network:   "udp",
    Address:   xorAddr.IP.String(),
    Port:      xorAddr.Port,
    Component: ice.ComponentRTP,
    RelAddr:   "192.168.1.100",
    RelPort:   50000,
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Discovered public address: %s:%d\n", 
    candidate.Address(), candidate.Port())

Priority Calculation

Server reflexive candidates have a type preference of 100:
priority = (2^24 * 100) + (2^8 * local-preference) + (256 - component)
This makes them less preferred than host candidates (126) and peer reflexive candidates (110), but more preferred than relay candidates (0).

When to Use Server Reflexive Candidates

Server reflexive candidates are essential when:
  • Peers are behind different NATs
  • Direct peer-to-peer connections need NAT traversal
  • You want to avoid TURN server overhead
  • The NAT allows inbound traffic after outbound binding
  • Both peers can reach public STUN servers

NAT Compatibility

Server reflexive candidates work with most NAT types:
Full Cone NAT
Compatible
Works perfectly. Single public binding for all destinations.
Address-Restricted NAT
Compatible
Works well. Requires coordination between peers.
Port-Restricted NAT
Compatible
Works with ICE connectivity checks.
Symmetric NAT
Limited
May require peer reflexive candidates or relay candidates.
The related address for a server reflexive candidate is the base (host) candidate from which it was derived:
relatedAddr := candidate.RelatedAddress()
if relatedAddr != nil {
    fmt.Printf("Base address: %s:%d\n", relatedAddr.Address, relatedAddr.Port)
}
This is useful for:
  • Debugging NAT behavior
  • Understanding address mappings
  • Diagnostics and logging

Connection Characteristics

Advantages:
  • Direct peer-to-peer connectivity through NAT
  • No media relay required
  • Lower latency than relay candidates
  • No server bandwidth costs for media
  • Works with most NAT types
Disadvantages:
  • Requires STUN server for discovery
  • May not work with symmetric NATs
  • Higher priority than relay but lower than host
  • Depends on NAT binding timeout

Common Patterns

Multiple STUN Servers

Query multiple STUN servers for redundancy:
stunServers := []string{
    "stun.l.google.com:19302",
    "stun1.l.google.com:19302",
    "stun2.l.google.com:19302",
}

for _, server := range stunServers {
    // Perform STUN binding request
    publicAddr := performSTUNRequest(server, localAddr)
    
    candidate, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
        Network:   "udp",
        Address:   publicAddr.IP,
        Port:      publicAddr.Port,
        Component: ice.ComponentRTP,
        RelAddr:   localAddr.IP,
        RelPort:   localAddr.Port,
    })
    if err != nil {
        continue
    }
    
    // Use candidate
}

Caching STUN Results

Cache discovered public addresses to avoid repeated STUN queries:
var (
    publicAddrCache     net.IP
    publicPortCache     int
    lastSTUNQuery       time.Time
    stunCacheDuration   = 5 * time.Minute
)

if time.Since(lastSTUNQuery) > stunCacheDuration {
    // Query STUN server
    publicAddrCache, publicPortCache = performSTUNRequest(stunServer, localAddr)
    lastSTUNQuery = time.Now()
}

// Use cached values
candidate, _ := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
    Network:   "udp",
    Address:   publicAddrCache.String(),
    Port:      publicPortCache,
    Component: ice.ComponentRTP,
    RelAddr:   localAddr.IP,
    RelPort:   localAddr.Port,
})
NAT bindings typically have timeouts (often 30-300 seconds). Ensure keepalives are sent to maintain the binding if long-lived connections are needed.

See Also

Build docs developers (and LLMs) love