Skip to content
Developer Tools9 min readPublished: August 23, 2026

How to Test STUN and TURN Servers: Low-Overhead OS-Level NAT Traversal Analysis

Evaluate STUN and TURN server performance at the OS network stack level using native packet analysis tools instead of bloated utilities. Master raw UDP socket testing, Netfilter state tracking, and interrupt moderation profiling for WebRTC NAT traversal.

Written by WasyTech Engineering Β· Core Engineering Team
Share:𝕏RedditLinkedIn

Quick Diagnostic: Bare-Metal STUN/TURN Testing via CLI

Third-party web tools for WebRTC testing often introduce unnecessary overheadβ€”injecting browser engine latency, JavaScript execution delays, and unoptimized socket wrappers into your measurements. To measure the true performance of your Session Traversal Utilities for NAT (STUN) and Traversal Using Relays around NAT (TURN) infrastructure, you should test directly at the OS shell.

bash
# 1. Test STUN binding resolution and measure raw RTT (RFC 8489)
turnutils_stunclient -p 3478 stun.yourserver.com

# 2. Benchmark TURN relay allocation, channel binding, and throughput (RFC 8656)
# Simulates 10 concurrent clients sending 100 packets/sec over UDP
turnutils_uclient -u testuser -w testpassword -p 3478 -e 127.0.0.1 -y -c -m 10 -n 100 turn.yourserver.com

# 3. Capture raw NAT traversal packets on the local interface (Linux)
sudo tshark -i eth0 -f "udp port 3478 or udp port 5349" -T fields \
  -e frame.time_epoch -e ip.src -e udp.srcport -e ip.dst -e udp.dstport \
  -e stun.type -e stun.att.type

On Windows, use native PowerShell sockets combined with raw packet capture to verify edge connectivity before testing with higher-level binaries:

powershell
# Windows PowerShell UDP Endpoint Socket Probe
$udpClient = New-Object System.Net.Sockets.UdpClient
$udpClient.Connect("turn.yourserver.com", 3478)
# STUN Binding Request Header: Type 0x0001, Length 0x0000, Magic Cookie 0x2112A442
[byte[]]$stunRequest = 0x00,0x01,0x00,0x00,0x21,0x12,0xA4,0x42,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C
$udpClient.Send($stunRequest, $stunRequest.Length)
$remoteEndpoint = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0)
$udpClient.Client.ReceiveTimeout = 2000
try {
    $response = $udpClient.Receive([ref]$remoteEndpoint)
    Write-Host "STUN Response received: $($response.Length) bytes from $($remoteEndpoint.ToString())" -ForegroundColor Green
} catch {
    Write-Warning "STUN request timed out or was blocked by firewall."
} finally {
    $udpClient.Close()
}

---

NAT Traversal at the OS Stack: Windows `tcpip.sys` vs. Linux Netfilter

Understanding how the OS network subsystem manages UDP state tracking is essential when evaluating Interactive Connectivity Establishment (ICE) candidate gathering.

bash
       [ Local User Space Application ]
                     β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                           β–Ό
[ Linux Kernel ]           [ Windows Kernel ]
 β”œβ”€ Netfilter/conntrack     β”œβ”€ tcpip.sys
 └─ nftables / iptables     └─ Windows Defender Firewall
       β”‚                           β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β–Ό
        [ Network Interface Card ]

Linux Netfilter and `conntrack` State Tracking

Under Linux, NAT traversal relies on the nf_conntrack engine. When an ICE agent emits a STUN Binding Request, Netfilter allocates a dynamic connection tracking tuple:

$$\text{Tuple} = \{\text{src\_ip}, \text{dst\_ip}, \text{sport}, \text{dport}, \text{proto}\}$$

When testing NAT mapping behavior, query the Netfilter state table directly to inspect how your local network handles outward UDP states:

bash
# Query active UDP NAT states for STUN/TURN bindings
sudo conntrack -L -p udp --dport 3478

The output reveals the connection lifecycle:

text
udp      17 29 src=192.168.1.50 dst=198.51.100.1 sport=54321 dport=3478 \
            src=198.51.100.1 dst=192.168.1.50 sport=3478 dport=54321 [ASSURED] mark=0 use=1
  1. UNREPLIED: The initial outbound STUN Binding Request has traversed the local Netfilter prerouting and postrouting chains. The ephemeral port is registered, but no inbound match has arrived.
  2. ASSURED: A valid STUN Binding Response matching the 5-tuple has traversed the inbound path. NAT hole punching is active for this dynamic mapping.

If the underlying network employs Endpoint-Independent Mapping (Full-Cone NAT), external hosts can reuse this mapping. If it implements Address-Restricted or Address and Port-Restricted Mapping (Symmetric NAT), any packet arriving from an untracked endpoint is dropped immediately at the PREROUTING chain.

Windows `tcpip.sys` and Firewall Edge Traversal

The Windows network architecture processes outbound UDP traffic through tcpip.sys, interacting directly with the Windows Filtering Platform (WFP).

According to the Microsoft Learn documentation on the Windows Defender Firewall API, UDP hole punching and unsolicited inbound traffic must satisfy strict Edge Traversal rules. When testing STUN/TURN endpoints on Windows hosts, edge traversal settings dictate whether an inbound STUN response or peer packet can bypass host-level state restrictions.

You can inspect and configure rule-specific Edge Traversal behaviors via PowerShell:

powershell
# Inspect firewall rules handling dynamic UDP traversal
Get-NetFirewallRule -Protocol UDP | Where-Object { $_.EdgeTraversalPolicy -ne "Block" } | 
    Select-Object Name, DisplayName, Direction, Action, EdgeTraversalPolicy

# Explicitly enable edge traversal for a designated low-overhead testing agent
Set-NetFirewallRule -DisplayName "Local WebRTC Test Agent" -EdgeTraversalPolicy Allow

If EdgeTraversalPolicy is set to Deny or DeferToApp, tcpip.sys discards inbound UDP traffic from external STUN servers if the ephemeral socket state was closed or garbage-collected prematurely by the OS runtime.

---

Hardware-Level Packet Jitter: NIC Interrupt Moderation

When benchmarking TURN relay performance under high load, unexpected packet jitter often stems from the hardware layer rather than the server's network path.

bash
Incoming UDP Packets:   [P1]  [P2]  [P3]  [P4]  [P5]
                          β”‚     β”‚     β”‚     β”‚     β”‚
NIC Coalescing Buffer:  [=========================] (Wait 125Β΅s / 64 pkts)
                                     β”‚
Hardware Interrupt:                  β–Ό (Single CPU Interrupt Fired)
OS Kernel Subsystem:    [Process P1, P2, P3, P4, P5 in batch] ──> Jitter Spike

Modern Network Interface Controllers (NICs) use Interrupt Moderation (also known as Interrupt Coalescing) to reduce CPU overhead. Instead of triggering an interrupt for every incoming UDP packet, the NIC buffers packets and fires a single interrupt after reaching a threshold:

  • A configured time threshold (e.g., $125\ \mu\text{s}$)
  • A specific frame count threshold (e.g., 64 packets)

While this optimizes bulk TCP throughput, it introduces artificial jitter during STUN/TURN latency testing by batching time-sensitive signaling and media packets.

Inspecting and Disabling NIC Moderation on Linux

Use ethtool to examine and adjust your NIC's ring buffers and interrupt moderation parameters:

bash
# Query current NIC interrupt coalescing parameters
ethtool -c eth0

# Temporarily disable RX/TX interrupt moderation for precise latency testing
sudo ethtool -C eth0 adaptive-rx off adaptive-tx off rx-usecs 0 tx-usecs 0

Disabling Interrupt Moderation on Windows

On Windows platforms, interrupt moderation can be tuned directly on the network adapter properties:

powershell
# Query NIC advanced properties for Interrupt Moderation
Get-NetAdapterAdvancedProperty -Name "Ethernet 1" -DisplayName "Interrupt Moderation"

# Disable Interrupt Moderation to eliminate hardware-level packet batching
Set-NetAdapterAdvancedProperty -Name "Ethernet 1" -DisplayName "Interrupt Moderation" -DisplayValue "Disabled"

*Note: Disabling interrupt moderation increases per-packet CPU utilization on the host. Always restore default settings after benchmarking.*

---

Packet-Level Analysis: Dissecting RFC 8489 and RFC 8656

Testing STUN/TURN infrastructure requires packet-level validation. Using tools like tshark or tcpdump, you can verify compliance with RFC 8489 (STUN) and RFC 8656 (TURN).

bash
Client                                                  TURN Server
  β”‚                                                          β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€ STUN Binding Request (RFC 8489: 0x0001) ─────────>β”‚
  β”‚<────── STUN Binding Success (RFC 8489: 0x0101) ───────────
  β”‚                                                          β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€ TURN Allocate Request (RFC 8656: 0x0003) ────────>β”‚
  β”‚<────── TURN 401 Unauthorized (Nonce Challenge) ───────────
  β”‚                                                          β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€ TURN Allocate Request (With MESSAGE-INTEGRITY) ──>β”‚
  β”‚<────── TURN 200 OK Allocate Success (0x0103) ─────────────
  β”‚                                                          β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€ TURN CreatePermission (0x0008) ──────────────────>β”‚
  β”‚<────── TURN CreatePermission Success (0x0108) ────────────
  β”‚                                                          β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€ TURN ChannelBind Request (0x0009) ───────────────>β”‚
  β”‚<────── TURN ChannelBind Success (0x0109) ─────────────────

1. STUN Binding Transaction (RFC 8489)

A lightweight STUN transaction consists of two raw frames:

  1. STUN Binding Request (0x0001): Emitted from the client to the server's UDP port 3478.
  2. STUN Binding Success Response (0x0101): Returned by the server, carrying the critical XOR-MAPPED-ADDRESS attribute (0x0020).

To extract the public reflexive endpoint directly from a packet trace:

bash
sudo tshark -i eth0 -Y "stun.type == 0x0101" -T fields \
  -e frame.time_delta \
  -e stun.att.ipv4-xor \
  -e stun.att.port-xor

2. TURN Allocation and Relaying Lifecycle (RFC 8656)

Unlike stateless STUN requests, TURN allocations require stateful, authenticated handshakes:

  1. Allocate Request (0x0003): Initial unauthenticated probe from the client.
  2. Allocate Error Response (0x0113): Server returns 401 Unauthorized, providing a NONCE and REALM.
  3. Authenticated Allocate Request (0x0003): Client re-sends the request with USERNAME, REALM, NONCE, and an HMAC-SHA1 MESSAGE-INTEGRITY attribute.
  4. Allocate Success Response (0x0103): Server allocates a public relay endpoint (XOR-RELAYED-ADDRESS, 0x0016) and returns a dynamic LIFETIME attribute.
  5. CreatePermission (0x0008) / ChannelBind (0x0009): Authorizes a remote peer's IP to exchange data through the allocated relay.
bash
# Capture full TURN allocation lifecycle and track round-trip timing
sudo tshark -i any -f "udp port 3478" -Y "stun" -T fields \
  -e frame.number \
  -e ip.src -e ip.dst \
  -e stun.type \
  -e stun.att.realm \
  -e stun.att.error-code

---

Benchmarking STUN vs. TURN Overhead

A direct STUN connection enables peer-to-peer communication, while TURN relays traffic through an intermediate proxy. This introduces structural performance differences across your network stack:

Metric / ParameterSTUN (Direct P2P Path)TURN Relay (RFC 8656)
**Path Complexity**Direct point-to-point routing ($N \leftrightarrow N$)Dual-hop routing ($N \leftrightarrow \text{Relay} \leftrightarrow N$)
**Encapsulation Overhead**0 bytes (Direct UDP payload)4 bytes (ChannelData) to 36+ bytes (`Send`/`Data` Indications)
**Host Kernel Overhead**Standard socket read/write operationsRelay socket buffer amplification & context switching
**Latency Penalty**Native baseline RTT ($\Delta t_0$)$\Delta t_{\text{Client}\to\text{Relay}} + \Delta t_{\text{Processing}} + \Delta t_{\text{Relay}\to\text{Peer}}$
**Bandwidth Amplification**Single-hop egressDouble egress bandwidth billed at the relay node
bash
STUN Direct:  [Client A] ──────────────────────────────────────────> [Client B]
                           (Zero Relay Processing Overhead)

TURN Relay:   [Client A] ───> [TURN Relay Node: Decapsulate] ───> [Client B]
                               [Re-encapsulate & Re-route  ]
                                (Adds Processing Latency)

Network Disclaimer

> Important: Local loopback and isolated subnet testing cannot fully replicate the behaviors of Carrier-Grade NAT (CGNAT / RFC 6598), asymmetric route flapping, or strict carrier firewalls found in production networks. > > Furthermore, TURN relays cannot achieve zero-latency performance. Relaying traffic introduces deterministic computational and routing overhead, including UDP socket buffer queues, context switches, framing headers, and additional network hops.

---

Step-by-Step Server Validation Checklist

Follow this workflow to validate your STUN/TURN server without relying on heavyweight web tools:

Phase 1: Port and Socket Verification Confirm that the underlying UDP/TCP daemon (`coturn`, `eturnal`, or a custom binary) is actively bound to the interface without socket exhaustion:

bash
# Linux
ss -lunp 'sport = :3478 or sport = :5349'

# Windows
netstat -ano -p UDP | findstr "3478"

Phase 2: Firewall Layer and Edge Traversal Ensure edge rules permit dynamic, high-port UDP traffic:

bash
# Linux (nftables validation)
sudo nft list ruleset | grep -E "3478|5349"

# Windows (Verify Firewall Inbound UDP Profiles)
Get-NetFirewallPortFilter | Where-Object { $_.LocalPort -eq 3478 }

Phase 3: Hardware Optimization Ensure NIC interrupt moderation is adjusted on both test runners and dedicated relay targets to prevent artificial jitter:

bash
sudo ethtool -C eth0 adaptive-rx off rx-usecs 0

Phase 4: CLI Functional and Throughput Stress Test Execute the test using native utilities, capturing network activity with `tshark`:

bash
turnutils_uclient -u testuser -w testpassword -p 3478 -y -e 198.51.100.2 -m 50 -n 200 turn.yourserver.com

By testing your STUN and TURN infrastructure at the OS stack level with native utilities and raw packet analysis, you eliminate the overhead and inconsistencies of browser-based tools. This approach provides clean, reproducible metrics for NAT traversal latency, throughput, and connection stability.

Frequently Asked Technical Questions

Capture interface traffic using native utilities like tcpdump or tshark while executing an allocation request. Filter for STUN/TURN frames (RFC 8656) to confirm the XOR-RELAYED-ADDRESS attribute inside the Allocate Success response. Verify active relaying by checking for bidirectional Send and Data indications or ChannelData messages. On Linux, inspect kernel translation states with 'conntrack -L -p udp' to ensure the Netfilter connection tracking table maintains active bindings for the assigned relay port.

WasyTech Engineering

Core Engineering Team

The collective engineering minds behind WasyTech's zero-bloat utility architecture.

Focus:Systems ProgrammingPerformance DiagnosticsWindows InternalsNetwork Protocols

Related Systems Guides

View all guides β†’
Developer Tools8 min readAugust 23, 2026

How to View EXIF Data Without Uploading: Local File I/O and Native Metadata Extraction

Discover how operating system frameworks like Windows propsys.dll, macOS ImageIO, and Linux libexif parse image headers directly from disk without network transmission. Learn the underlying binary architecture of JPEG APP1 markers and HEIC metadata containers while avoiding the telemetry risks of web-based EXIF viewers.