Skip to content

Commit 04a5cd5

Browse files
committed
RFC 6156: TURN Extension for IPv6
This PR implements RFC 6156: Traversal Using Relays around NAT (TURN) Extension for IPv6 enabling UDP and TCP relay allocations on IPv6 addresses.
1 parent edb776d commit 04a5cd5

17 files changed

Lines changed: 1401 additions & 81 deletions

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ Yes.
7171
* **RFC 5389**: [Session Traversal Utilities for NAT (STUN)][rfc5389]
7272
* **RFC 5766**: [Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN)][rfc5766]
7373
* **RFC 6062**: [Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations][rfc6062]
74-
75-
#### Planned
7674
* **RFC 6156**: [Traversal Using Relays around NAT (TURN) Extension for IPv6][rfc6156]
7775

7876
[rfc5389]: https://tools.ietf.org/html/rfc5389

addr_family.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package turn
5+
6+
import "github.com/pion/turn/v4/internal/proto"
7+
8+
// RequestedAddressFamily represents the REQUESTED-ADDRESS-FAMILY Attribute as
9+
// defined in RFC 6156 Section 4.1.1.
10+
type RequestedAddressFamily = proto.RequestedAddressFamily
11+
12+
// Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1.
13+
const (
14+
RequestedAddressFamilyIPv4 = proto.RequestedFamilyIPv4
15+
RequestedAddressFamilyIPv6 = proto.RequestedFamilyIPv6
16+
)

client.go

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ type ClientConfig struct {
5151
// PermissionTimeout sets the refresh interval of permissions. Defaults to 2 minutes.
5252
PermissionRefreshInterval time.Duration
5353

54+
// RequestedAddressFamily is the address family to request in allocations (IPv4 or IPv6).
55+
// If not specified (zero value), the client will attempt to infer from the PacketConn's
56+
// local address, falling back to IPv4 if inference fails. See RFC 6156.
57+
RequestedAddressFamily RequestedAddressFamily
58+
5459
evenPort bool // If EVEN-PORT Attribute should be sent in Allocation
5560
reservationToken []byte // If Server responds with RESERVATION-TOKEN or if Client wishes to send one
5661
bindingRefreshInterval time.Duration
@@ -79,16 +84,76 @@ type Client struct {
7984
mutexTrMap sync.Mutex // Thread-safe
8085
log logging.LeveledLogger // Read-only
8186

82-
evenPort bool // If EVEN-PORT Attribute should be sent in Allocation
83-
reservationToken []byte // If Server responds with RESERVATION-TOKEN or if Client wishes to send one
87+
// If EVEN-PORT Attribute should be sent in Allocation
88+
evenPort bool
89+
90+
// If Server responds with RESERVATION-TOKEN or if Client wishes to send one
91+
reservationToken []byte
92+
93+
// REQUESTED-ADDRESS-FAMILY attribute for allocations (RFC 6156)
94+
requestedAddressFamily proto.RequestedAddressFamily
95+
8496
permissionRefreshInterval time.Duration
8597
bindingRefreshInterval time.Duration
8698
bindingCheckInterval time.Duration
8799
}
88100

101+
// inferAddressFamilyFromConn attempts to determine the address
102+
// family (IPv4 or IPv6) from a PacketConn's local address.
103+
// Returns an error if the address type is not IP-based.
104+
func inferAddressFamilyFromConn(
105+
conn net.PacketConn,
106+
) (proto.RequestedAddressFamily, error) {
107+
addr := conn.LocalAddr()
108+
109+
switch a := addr.(type) {
110+
case *net.UDPAddr:
111+
if a.IP.To4() != nil {
112+
return proto.RequestedFamilyIPv4, nil
113+
}
114+
115+
return proto.RequestedFamilyIPv6, nil
116+
case *net.TCPAddr:
117+
if a.IP.To4() != nil {
118+
return proto.RequestedFamilyIPv4, nil
119+
}
120+
121+
return proto.RequestedFamilyIPv6, nil
122+
default:
123+
return 0, fmt.Errorf("cannot infer address family from %T", addr) //nolint:err113
124+
}
125+
}
126+
127+
// getRequestedAddressFamily determines the address family to use
128+
// for TURN allocations. It follows this priority:
129+
// 1. Use explicitly configured RequestedAddressFamily if set
130+
// 2. Try to infer from the PacketConn's local address
131+
// 3. Fall back to IPv4 default per RFC 6156
132+
func getRequestedAddressFamily(
133+
log logging.LeveledLogger,
134+
config *ClientConfig,
135+
) proto.RequestedAddressFamily {
136+
// If explicitly set, use it
137+
if config.RequestedAddressFamily != 0 {
138+
return config.RequestedAddressFamily
139+
}
140+
141+
// Try to infer from the PacketConn
142+
if inferred, err := inferAddressFamilyFromConn(config.Conn); err == nil {
143+
log.Debugf("Inferred address family %v from connection", inferred)
144+
145+
return inferred
146+
}
147+
148+
log.Debugf("Could not infer address family, defaulting to IPv4")
149+
150+
// Default to IPv4 per RFC 6156
151+
return proto.RequestedFamilyIPv4
152+
}
153+
89154
// NewClient returns a new Client instance. listeningAddress is the address and port to listen on,
90155
// default "0.0.0.0:0".
91-
func NewClient(config *ClientConfig) (*Client, error) {
156+
func NewClient(config *ClientConfig) (*Client, error) { //nolint:gocyclo,cyclop
92157
loggerFactory := config.LoggerFactory
93158
if loggerFactory == nil {
94159
loggerFactory = logging.NewDefaultLoggerFactory()
@@ -113,11 +178,14 @@ func NewClient(config *ClientConfig) (*Client, error) {
113178
config.Net = n
114179
}
115180

181+
// Determine the requested address family (RFC 6156)
182+
requestedAddressFamily := getRequestedAddressFamily(log, config)
183+
116184
var stunServ, turnServ net.Addr
117185
var err error
118186

119187
if len(config.STUNServerAddr) > 0 {
120-
stunServ, err = config.Net.ResolveUDPAddr("udp4", config.STUNServerAddr)
188+
stunServ, err = config.Net.ResolveUDPAddr("udp", config.STUNServerAddr)
121189
if err != nil {
122190
return nil, err
123191
}
@@ -126,7 +194,7 @@ func NewClient(config *ClientConfig) (*Client, error) {
126194
}
127195

128196
if len(config.TURNServerAddr) > 0 {
129-
turnServ, err = config.Net.ResolveUDPAddr("udp4", config.TURNServerAddr)
197+
turnServ, err = config.Net.ResolveUDPAddr("udp", config.TURNServerAddr)
130198
if err != nil {
131199
return nil, err
132200
}
@@ -148,6 +216,7 @@ func NewClient(config *ClientConfig) (*Client, error) {
148216
log: log,
149217
evenPort: config.evenPort,
150218
reservationToken: config.reservationToken,
219+
requestedAddressFamily: requestedAddressFamily,
151220
permissionRefreshInterval: config.PermissionRefreshInterval,
152221
bindingRefreshInterval: config.bindingRefreshInterval,
153222
bindingCheckInterval: config.bindingCheckInterval,
@@ -272,11 +341,14 @@ func (c *Client) sendAllocateRequest(protocol proto.Protocol) ( //nolint:cyclop
272341
proto.RequestedTransport{Protocol: protocol},
273342
stun.Fingerprint,
274343
}
275-
if c.evenPort {
276-
allocationSetters = append(allocationSetters, proto.EvenPort{ReservePort: true})
277-
}
344+
// RFC 6156: REQUESTED-ADDRESS-FAMILY and RESERVATION-TOKEN are mutually exclusive.
278345
if len(c.reservationToken) != 0 {
279346
allocationSetters = append(allocationSetters, proto.ReservationToken(c.reservationToken))
347+
} else {
348+
allocationSetters = append(allocationSetters, c.requestedAddressFamily)
349+
}
350+
if c.evenPort {
351+
allocationSetters = append(allocationSetters, proto.EvenPort{ReservePort: true})
280352
}
281353

282354
msg, err := stun.Build(allocationSetters...)
@@ -313,11 +385,14 @@ func (c *Client) sendAllocateRequest(protocol proto.Protocol) ( //nolint:cyclop
313385
&c.integrity,
314386
stun.Fingerprint,
315387
}
316-
if c.evenPort {
317-
allocationSetters = append(allocationSetters, proto.EvenPort{ReservePort: true})
318-
}
388+
// RFC 6156: REQUESTED-ADDRESS-FAMILY and RESERVATION-TOKEN are mutually exclusive.
319389
if len(c.reservationToken) != 0 {
320390
allocationSetters = append(allocationSetters, proto.ReservationToken(c.reservationToken))
391+
} else {
392+
allocationSetters = append(allocationSetters, c.requestedAddressFamily)
393+
}
394+
if c.evenPort {
395+
allocationSetters = append(allocationSetters, proto.EvenPort{ReservePort: true})
321396
}
322397

323398
msg, err = stun.Build(allocationSetters...)

client_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,92 @@ func TestTCPClientMultipleConns(t *testing.T) {
698698
assert.NoError(t, clientConn.Close())
699699
}
700700

701+
func TestInferAddressFamilyFromConn(t *testing.T) {
702+
t.Run("IPv4 UDP connection", func(t *testing.T) {
703+
conn, err := net.ListenPacket("udp4", "0.0.0.0:0") //nolint:noctx
704+
assert.NoError(t, err)
705+
defer conn.Close() //nolint:errcheck
706+
707+
family, err := inferAddressFamilyFromConn(conn)
708+
assert.NoError(t, err)
709+
assert.Equal(t, proto.RequestedFamilyIPv4, family)
710+
})
711+
712+
t.Run("IPv6 UDP connection", func(t *testing.T) {
713+
conn, err := net.ListenPacket("udp6", "[::]:0") //nolint:noctx
714+
assert.NoError(t, err)
715+
defer conn.Close() //nolint:errcheck
716+
717+
family, err := inferAddressFamilyFromConn(conn)
718+
assert.NoError(t, err)
719+
assert.Equal(t, proto.RequestedFamilyIPv6, family)
720+
})
721+
}
722+
723+
func TestGetRequestedAddressFamily(t *testing.T) {
724+
log := logging.NewDefaultLoggerFactory().NewLogger("test")
725+
726+
t.Run("Explicit IPv4 in config", func(t *testing.T) {
727+
conn, err := net.ListenPacket("udp6", "[::]:0") //nolint:noctx
728+
assert.NoError(t, err)
729+
defer conn.Close() //nolint:errcheck
730+
731+
config := &ClientConfig{
732+
Conn: conn,
733+
RequestedAddressFamily: proto.RequestedFamilyIPv4,
734+
}
735+
736+
// Should use explicit config even though conn is IPv6
737+
family := getRequestedAddressFamily(log, config)
738+
assert.Equal(t, proto.RequestedFamilyIPv4, family)
739+
})
740+
741+
t.Run("Explicit IPv6 in config", func(t *testing.T) {
742+
conn, err := net.ListenPacket("udp4", "0.0.0.0:0") //nolint:noctx
743+
assert.NoError(t, err)
744+
defer conn.Close() //nolint:errcheck
745+
746+
config := &ClientConfig{
747+
Conn: conn,
748+
RequestedAddressFamily: proto.RequestedFamilyIPv6,
749+
}
750+
751+
// Should use explicit config even though conn is IPv4
752+
family := getRequestedAddressFamily(log, config)
753+
assert.Equal(t, proto.RequestedFamilyIPv6, family)
754+
})
755+
756+
t.Run("Infer IPv4 from connection", func(t *testing.T) {
757+
conn, err := net.ListenPacket("udp4", "0.0.0.0:0") //nolint:noctx
758+
assert.NoError(t, err)
759+
defer conn.Close() //nolint:errcheck
760+
761+
config := &ClientConfig{
762+
Conn: conn,
763+
// RequestedAddressFamily not set (zero value)
764+
}
765+
766+
// Should infer IPv4 from connection
767+
family := getRequestedAddressFamily(log, config)
768+
assert.Equal(t, proto.RequestedFamilyIPv4, family)
769+
})
770+
771+
t.Run("Infer IPv6 from connection", func(t *testing.T) {
772+
conn, err := net.ListenPacket("udp6", "[::]:0") //nolint:noctx
773+
assert.NoError(t, err)
774+
defer conn.Close() //nolint:errcheck
775+
776+
config := &ClientConfig{
777+
Conn: conn,
778+
// RequestedAddressFamily not set (zero value)
779+
}
780+
781+
// Should infer IPv6 from connection
782+
family := getRequestedAddressFamily(log, config)
783+
assert.Equal(t, proto.RequestedFamilyIPv6, family)
784+
})
785+
}
786+
701787
type channelBindFilterConn struct {
702788
net.PacketConn
703789

examples/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ This example demonstrates listening on TLS. You could combine this
103103
example with `simple` and you will have a Pion TURN instance that is
104104
available via TLS and UDP.
105105

106+
#### ipv6 (server)
107+
108+
This example demonstrates a TURN server with IPv6 support (RFC 6156).
109+
The server listens on all IPv6 interfaces (`[::]`) and allocates IPv6
110+
relay addresses to clients that request them.
111+
112+
``` sh
113+
$ cd ipv6
114+
$ go build
115+
$ ./ipv6 -public-ip 2001:db8::1 -users username=password
116+
```
117+
118+
For local testing with IPv6 localhost:
119+
120+
``` sh
121+
$ ./ipv6 -public-ip ::1 -users username=password
122+
```
123+
124+
This example shows how to configure a TURN server for IPv6 clients,
125+
which is essential for environments where IPv4 addresses are limited or
126+
IPv6-only networks.
127+
106128
#### lt-creds
107129

108130
This example shows how to use long term credentials. You can issue
@@ -172,6 +194,33 @@ $ go build
172194
./turn-client -host <turn-server-name> -user=user=pass -ping
173195
```
174196

197+
#### ipv6 (client)
198+
199+
Dials the requested TURN server via IPv6 (RFC 6156). This example
200+
demonstrates how to request IPv6 relay allocations from a TURN server.
201+
202+
``` sh
203+
$ cd ipv6
204+
$ go build
205+
$ ./ipv6 -host 2001:db8::1 -user username=password
206+
```
207+
208+
For local testing with IPv6 localhost:
209+
210+
``` sh
211+
$ ./ipv6 -host ::1 -user username=password
212+
```
213+
214+
With `-ping`, it will perform a ping test over IPv6:
215+
216+
``` sh
217+
$ ./ipv6 -host ::1 -user username=password -ping
218+
```
219+
220+
This client sets `RequestedAddressFamily` to `proto.RequestedFamilyIPv6`
221+
to request an IPv6 relay allocation. The server must support IPv6 and be
222+
configured to listen on IPv6 addresses.
223+
175224
Following diagram shows what turn-client does:
176225

177226
+----------------+

0 commit comments

Comments
 (0)