Skip to content

Commit 5e6bf75

Browse files
committed
Add Coturn Interop. Tests
1 parent f45251c commit 5e6bf75

File tree

5 files changed

+616
-0
lines changed

5 files changed

+616
-0
lines changed

.github/workflows/e2e.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
# SPDX-License-Identifier: MIT
3+
4+
name: E2E
5+
on:
6+
pull_request:
7+
branches:
8+
- master
9+
push:
10+
branches:
11+
- master
12+
13+
jobs:
14+
e2e-test:
15+
name: Test
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 10
18+
steps:
19+
- name: checkout
20+
uses: actions/checkout@v6
21+
- name: test
22+
run: |
23+
docker build -t pion-turn-e2e -f e2e/Dockerfile .
24+
docker run -i --rm pion-turn-e2e

e2e/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
# SPDX-License-Identifier: MIT
3+
4+
FROM docker.io/library/golang:1.26-trixie
5+
6+
RUN apt-get update && apt-get install -y coturn
7+
8+
COPY . /go/src/github.com/pion/turn
9+
WORKDIR /go/src/github.com/pion/turn/e2e
10+
11+
CMD ["go", "test", "-tags=coturn", "-v", "."]

e2e/e2e.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
// Package e2e contains end to end tests for pion/turn
5+
package e2e

e2e/e2e_coturn_test.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build coturn && !js
5+
// +build coturn,!js
6+
7+
package e2e
8+
9+
import (
10+
"bufio"
11+
"context"
12+
"fmt"
13+
"io"
14+
"net"
15+
"os"
16+
"os/exec"
17+
"strings"
18+
"testing"
19+
"time"
20+
21+
"github.com/pion/logging"
22+
"github.com/pion/turn/v5"
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
const (
27+
coturnServerPort = 3478
28+
coturnConfigFile = "/tmp/turnserver.conf"
29+
coturnStartupDelay = 2 * time.Second
30+
)
31+
32+
// serverCoturn starts a coturn TURN server
33+
func serverCoturn(c *comm) {
34+
go func() {
35+
c.serverMutex.Lock()
36+
defer c.serverMutex.Unlock()
37+
38+
// Create coturn configuration file
39+
config := fmt.Sprintf(`
40+
listening-port=%d
41+
listening-ip=127.0.0.1
42+
relay-ip=127.0.0.1
43+
external-ip=127.0.0.1
44+
user=%s:%s
45+
realm=%s
46+
lt-cred-mech
47+
fingerprint
48+
no-tls
49+
no-dtls
50+
log-file=stdout
51+
`, c.serverPort, c.username, c.password, c.realm)
52+
53+
configFile := fmt.Sprintf("/tmp/turnserver-%d.conf", c.serverPort)
54+
if err := os.WriteFile(configFile, []byte(config), 0600); err != nil {
55+
c.errChan <- err
56+
return
57+
}
58+
defer func() {
59+
_ = os.Remove(configFile)
60+
}()
61+
62+
// Start coturn server with context
63+
cmd := exec.CommandContext(c.ctx, "turnserver", "-c", configFile)
64+
cmd.Stdout = io.Discard
65+
cmd.Stderr = io.Discard
66+
67+
if err := cmd.Start(); err != nil {
68+
c.errChan <- fmt.Errorf("failed to start coturn: %w (is coturn installed?)", err)
69+
return
70+
}
71+
72+
// Ensure proper cleanup
73+
defer func() {
74+
if cmd.Process != nil {
75+
_ = cmd.Process.Kill()
76+
_, _ = cmd.Process.Wait()
77+
}
78+
}()
79+
80+
// Ensure server has time to start
81+
time.Sleep(coturnStartupDelay)
82+
83+
c.serverReady <- struct{}{}
84+
85+
// Wait for context cancellation or process exit
86+
done := make(chan error, 1)
87+
go func() {
88+
done <- cmd.Wait()
89+
}()
90+
91+
select {
92+
case <-c.ctx.Done():
93+
// Context cancelled, kill process
94+
if cmd.Process != nil {
95+
_ = cmd.Process.Kill()
96+
}
97+
case <-done:
98+
// Process exited
99+
}
100+
101+
c.serverDone <- nil
102+
close(c.serverDone)
103+
}()
104+
}
105+
106+
// clientCoturn uses turnutils_uclient to test against a Pion TURN server
107+
func clientCoturn(c *comm) {
108+
select {
109+
case <-c.serverReady:
110+
// OK
111+
case <-time.After(time.Second * 5):
112+
c.errChan <- errServerTimeout
113+
return
114+
}
115+
116+
c.clientMutex.Lock()
117+
defer c.clientMutex.Unlock()
118+
119+
// Use turnutils_uclient to connect to the Pion server
120+
// -v: verbose
121+
// -u: username
122+
// -w: password
123+
// -r: realm
124+
// -e: peer address (echo server)
125+
// -n: number of messages
126+
// -m: message size
127+
// -W: time to run (seconds)
128+
args := []string{
129+
"-v",
130+
"-u", c.username,
131+
"-w", c.password,
132+
"-r", c.realm,
133+
"-n", "1",
134+
"-m", "1",
135+
"-W", "5",
136+
fmt.Sprintf("127.0.0.1:%d", c.serverPort),
137+
}
138+
139+
// #nosec G204
140+
cmd := exec.Command("turnutils_uclient", args...)
141+
142+
stdout, err := cmd.StdoutPipe()
143+
if err != nil {
144+
c.errChan <- fmt.Errorf("failed to create stdout pipe: %w", err)
145+
return
146+
}
147+
stderr, err := cmd.StderrPipe()
148+
if err != nil {
149+
c.errChan <- fmt.Errorf("failed to create stderr pipe: %w", err)
150+
return
151+
}
152+
153+
if err := cmd.Start(); err != nil {
154+
c.errChan <- fmt.Errorf("failed to start turnutils_uclient: %w (is coturn installed?)", err)
155+
return
156+
}
157+
158+
// Monitor output for success indicators
159+
go func() {
160+
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
161+
for scanner.Scan() {
162+
line := scanner.Text()
163+
fmt.Println("turnutils_uclient:", line)
164+
// Look for success indicators in coturn client output
165+
if strings.Contains(line, "success") || strings.Contains(line, "allocate msg sent") {
166+
c.messageReceived <- "success"
167+
}
168+
}
169+
}()
170+
171+
_ = cmd.Wait()
172+
c.clientDone <- nil
173+
close(c.clientDone)
174+
}
175+
176+
func TestPionCoturnE2EClientServer(t *testing.T) {
177+
t.Run("CoturnServer", func(t *testing.T) {
178+
// Test Pion Client -> Coturn Server
179+
testPionE2ESimple(t, serverCoturn, clientPion)
180+
})
181+
t.Run("CoturnClient", func(t *testing.T) {
182+
// Test Coturn Client (turnutils_uclient) -> Pion Server
183+
testPionE2ESimple(t, serverPion, clientCoturn)
184+
})
185+
}
186+
187+
// TestCoturnVersion checks if coturn is installed and reports version
188+
func TestCoturnVersion(t *testing.T) {
189+
cmd := exec.Command("turnserver", "--version")
190+
output, err := cmd.CombinedOutput()
191+
if err != nil {
192+
t.Skipf("coturn not installed: %v", err)
193+
}
194+
t.Logf("coturn version: %s", string(output))
195+
}
196+
197+
// TestPionCoturnE2EAllocationLifecycle tests the full allocation lifecycle with coturn
198+
func TestPionCoturnE2EAllocationLifecycle(t *testing.T) {
199+
t.Run("CoturnServer", func(t *testing.T) {
200+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
201+
defer cancel()
202+
203+
serverPort := randomPort(t)
204+
205+
// Start coturn server
206+
config := fmt.Sprintf(`
207+
listening-port=%d
208+
listening-ip=127.0.0.1
209+
relay-ip=127.0.0.1
210+
external-ip=127.0.0.1
211+
user=%s:%s
212+
realm=%s
213+
lt-cred-mech
214+
fingerprint
215+
no-tls
216+
no-dtls
217+
log-file=stdout
218+
`, serverPort, testUsername, testPassword, testRealm)
219+
220+
configFile := "/tmp/turnserver-lifecycle.conf"
221+
err := os.WriteFile(configFile, []byte(config), 0600)
222+
assert.NoError(t, err)
223+
defer func() {
224+
_ = os.Remove(configFile)
225+
}()
226+
227+
// Start coturn
228+
cmd := exec.CommandContext(ctx, "turnserver", "-c", configFile)
229+
cmd.Stdout = io.Discard
230+
cmd.Stderr = io.Discard
231+
err = cmd.Start()
232+
assert.NoError(t, err)
233+
234+
// Ensure cleanup
235+
defer func() {
236+
if cmd.Process != nil {
237+
_ = cmd.Process.Kill()
238+
}
239+
}()
240+
241+
// Wait for server to start
242+
time.Sleep(coturnStartupDelay)
243+
244+
// Test allocation lifecycle
245+
clientConn, err := net.ListenPacket("udp4", "127.0.0.1:0")
246+
assert.NoError(t, err)
247+
defer func() {
248+
_ = clientConn.Close()
249+
}()
250+
251+
turnClient, err := turn.NewClient(&turn.ClientConfig{
252+
TURNServerAddr: fmt.Sprintf("127.0.0.1:%d", serverPort),
253+
Username: testUsername,
254+
Password: testPassword,
255+
Realm: testRealm,
256+
Conn: clientConn,
257+
LoggerFactory: logging.NewDefaultLoggerFactory(),
258+
})
259+
assert.NoError(t, err)
260+
defer turnClient.Close()
261+
262+
err = turnClient.Listen()
263+
assert.NoError(t, err)
264+
265+
// Test allocation
266+
relayConn, err := turnClient.Allocate()
267+
assert.NoError(t, err)
268+
assert.NotNil(t, relayConn)
269+
270+
relayAddr := relayConn.LocalAddr()
271+
assert.NotNil(t, relayAddr)
272+
t.Logf("Allocated relay address: %s", relayAddr)
273+
274+
// Close relay connection
275+
err = relayConn.Close()
276+
assert.NoError(t, err)
277+
})
278+
}

0 commit comments

Comments
 (0)