Skip to content

fix(server): accept v6 peers on default-IPv4 allocations for dual-stack relays - #567

Open
ti wants to merge 1 commit into
pion:mainfrom
ti:fix-ipmatchesfamily-dualstack-relay
Open

fix(server): accept v6 peers on default-IPv4 allocations for dual-stack relays#567
ti wants to merge 1 commit into
pion:mainfrom
ti:fix-ipmatchesfamily-dualstack-relay

Conversation

@ti

@ti ti commented May 28, 2026

Copy link
Copy Markdown

Problem

internal/server/turn.go ipMatchesFamily strictly rejects pure-IPv6
peer addresses against an IPv4 allocation, including the default
IPv4 allocation that pion/turn applies when an Allocate request
omits REQUESTED-ADDRESS-FAMILY (RFC 6156 §4.1.1).

Browser WebRTC TURN clients (Chrome, Firefox, Safari) never set
REQUESTED-ADDRESS-FAMILY in their Allocate requests, so on a
dual-stack or IPv6-only Linux host every browser-driven allocation
ends up IPv4-family even when the listener and relay sockets are
bound [::]. Subsequent CreatePermission/ChannelBind requests
with v6 peer addresses are then rejected with
errPeerAddressFamilyMismatch (logged at Info level, easy to
miss), and the very first Send-indication fails with
errNoPermission.

This is the canonical failure mode for pion/turn-based TURN servers
deployed in front of IPv6-only Kubernetes pod networks. With the
relay socket actually bound dual-stack on Linux (net.ipv6.bindv6only=0,
which is the default), v6 peer reachability is fine; only the
protocol-level family check blocks the permission.

Repro & root-cause

Detailed write-up + minimal repro in #566.

Short version: with a Linux dual-stack relay socket and a v4-default
allocation, peer reachability works in both directions but
ipMatchesFamily(v6-peer, IPv4-family) returns false ⇒ no
permission ⇒ no Send-indication forwarded ⇒ WebRTC PC times out.

Fix (this PR)

Relax the v4-default branch of ipMatchesFamily to also accept v6
peers, matching what a dual-stack relay socket can actually deliver.

 if family == proto.RequestedFamilyIPv4 {
-    return ip.To4() != nil
+    return ip.To4() != nil || (ip.To4() == nil && ip.To16() != nil)
 }

The symmetric IPv6-allocation case is unchanged: a v4 peer on a
v6-explicit allocation is still rejected, preserving RFC 6156
strict-mode for the case where the client opted into v6 by sending
REQUESTED-ADDRESS-FAMILY=IPv6.

Refresh request family handling is untouched.

Tests

internal/server had two assertion sites baked to the strict cross-
family rejection. They are updated to match the new permissive
behavior:

  • TestIPMatchesFamily/IPv6 and /IPv6Loopback: ipMatchesFamily(v6, RequestedFamilyIPv4) now returns true.
  • TestHandleChannelBindRequest/PeerAddressFamilyMismatch is renamed
    to CrossFamilyV6PeerOnV4Alloc and asserts the success response
    (the previous request flow now produces ClassSuccessResponse
    instead of 443 PeerAddrFamilyMismatch).

All other tests in ./... pass unchanged.

Caveats / alternatives discussed in #566

This is the simplest of the three options I sketched in #566. The
other two would be more conservative:

  1. Default the allocation family to the listener's family when
    REQUESTED-ADDRESS-FAMILY is absent — backwards-compatible for
    IPv4-only deployments, correct for IPv6-only listeners.
  2. A Server config knob (PermissiveAddressFamily bool or
    similar) that opts in to relaxed cross-family. RFC 6156 strict
    stays the default.

I went with the simplest patch first because it fixes the canonical
deployment case with the smallest diff. Happy to refactor to either
option (1) or (2) if maintainers prefer — let me know.

Compatibility note

This subtly changes behavior for clients that explicitly relied on
the v4-default + v6-peer = 443 response to detect IPv6 capability.
In practice no real-world TURN client does this (they all either
omit the attribute or explicitly request a family); but worth
flagging for review.

Refs: #566

…ck relays

Browsers' WebRTC TURN clients never set REQUESTED-ADDRESS-FAMILY in
the Allocate request, so per RFC 6156 §4.1.1 the allocation defaults
to IPv4 family. On dual-stack and IPv6-only Linux hosts the relay
socket itself is reachable via both IPv4 and IPv6 (pion/turn binds
":port" wildcard, kernel maps v4 over v6 with the default
net.ipv6.bindv6only=0). The strict ipMatchesFamily check, however,
rejects every CreatePermission/ChannelBind whose XOR-PEER-ADDRESS is
IPv6, returning errPeerAddressFamilyMismatch. The error log is
INFO-level only, so the failure surfaces downstream as
errNoPermission on the first Send-indication.

This is the canonical failure mode for pion/turn-based TURN servers
deployed in front of IPv6-only Kubernetes pod networks (EKS Auto
with IPv6 STACK_TYPE, GKE STACK_TYPE=IPv6, Cilium IPv6-only): the
relay path works, but the protocol-level family check blocks every
permission install for the cluster's v6 backends.

Relax the v4-default case to also accept v6 peers, matching what
the relay socket can actually deliver. The symmetric IPv6 allocation
case keeps RFC 6156 strict behavior (a v6 alloc still rejects v4
peers), so existing IPv4-only deployments are unchanged.

The two test assertions hard-coded to the strict cross-family
rejection are updated:
  - TestIPMatchesFamily/IPv6{,Loopback}: v6 peer on v4-default now
    matches.
  - TestHandleChannelBindRequest/PeerAddressFamilyMismatch becomes
    CrossFamilyV6PeerOnV4Alloc and asserts the success response.

Refresh request family handling and the symmetric (v4 peer on v6
alloc) rejection paths are unchanged.

Verified end-to-end on IPv6-only EKS Auto with l7mp/stunner in front
of LiveKit: with the listener and relay sockets bound dual-stack,
this change makes CreatePermission succeed for the cluster's v6 pod
IPs and TURN forwarding works.
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.54%. Comparing base (f8c9cf8) to head (4312920).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #567      +/-   ##
==========================================
- Coverage   81.73%   81.54%   -0.19%     
==========================================
  Files          46       46              
  Lines        3197     3197              
==========================================
- Hits         2613     2607       -6     
- Misses        382      386       +4     
- Partials      202      204       +2     
Flag Coverage Δ
go 81.54% <100.00%> (-0.19%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rg0now

rg0now commented May 29, 2026

Copy link
Copy Markdown
Contributor

See #566 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants