Skip to content

Commit 920848e

Browse files
authored
Merge pull request #2 from VictorUvarov/vu/pion-interop
Interop testing with pion stun
2 parents 76d435f + 81d8665 commit 920848e

7 files changed

Lines changed: 140 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,28 @@ jobs:
1212
lint:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v4
16-
- uses: actions/setup-go@v5
15+
- uses: actions/checkout@v5
16+
- uses: actions/setup-go@v6
1717
with:
1818
go-version-file: go.mod
1919
cache: true
20-
# `just lint` shells out to golangci-lint; install both up front.
2120
- uses: taiki-e/install-action@v2
2221
with:
23-
tool: just,golangci-lint
22+
tool: just
23+
# `just lint` shells out to golangci-lint and adds GOPATH/bin to PATH,
24+
# so install it there (go install is the method `just lint` recommends).
25+
- run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
2426
- run: just lint
2527

2628
test:
2729
runs-on: ubuntu-latest
2830
steps:
29-
- uses: actions/checkout@v4
30-
- uses: actions/setup-go@v5
31+
- uses: actions/checkout@v5
32+
- uses: actions/setup-go@v6
3133
with:
3234
go-version-file: go.mod
3335
cache: true
34-
- uses: actions/setup-python@v5
36+
- uses: actions/setup-python@v6
3537
with:
3638
python-version: "3.x"
3739
- uses: taiki-e/install-action@v2
@@ -44,3 +46,17 @@ jobs:
4446
# stunc against stund over UDP/TCP/TLS/DTLS + the auth handshake
4547
# (openssl for cert generation ships on the runner).
4648
- run: just test-e2e
49+
50+
interop:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v5
54+
- uses: actions/setup-go@v6
55+
with:
56+
go-version-file: go.mod
57+
cache: true
58+
- uses: taiki-e/install-action@v2
59+
with:
60+
tool: just
61+
# Foreign implementation on the wire: pion/stun against our stund.
62+
- run: just interop

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# build artifacts
22
/bin/
33
/stund
4+
/test/interop/pion/stun-interop-pion
45

56
# generated dev TLS certs (just cert)
67
/dev/

internal/stunclient/client_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ func TestAuth(t *testing.T) {
138138
t.Fatal(err)
139139
}
140140
server.Credentials = auth
141-
addr := startUDP(t)
141+
// Reset only after startUDP's cleanup has stopped the server goroutine —
142+
// cleanups run LIFO, so register this one first (it runs last).
142143
t.Cleanup(func() { server.Credentials = nil })
144+
addr := startUDP(t)
143145

144146
t.Run("good credentials", func(t *testing.T) {
145147
c, err := DialUDP(addr, Config{Username: "alice", Password: "s3cret"})
@@ -184,8 +186,9 @@ func TestAuth(t *testing.T) {
184186
func TestRedirect(t *testing.T) {
185187
target := netip.MustParseAddrPort("192.0.2.7:3478")
186188
server.Alternate = &server.AlternateServer{V4: target, Domain: "stun.example.org"}
187-
addr := startUDP(t)
189+
// Register before startUDP so this reset runs after the server stops (LIFO).
188190
t.Cleanup(func() { server.Alternate = nil })
191+
addr := startUDP(t)
189192

190193
c, err := DialUDP(addr, Config{})
191194
if err != nil {

justfile

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,24 @@ test-e2e: build cert
166166
bin/stunc -user alice:s3cret {{auth-addr}}
167167
echo "e2e ok"
168168
169+
# ── Third-party interop ────────────────────────────────────────────────────
170+
# Validate the wire against an implementation we didn't write — the class of
171+
# bug a stunc-against-stund e2e can't catch, since both sides share our code.
172+
173+
# Foreign Go client (pion/stun) sends a Binding request to our stund.
174+
interop: build
175+
#!/usr/bin/env bash
176+
set -euo pipefail
177+
{{bin}} -addr {{addr}} & pid=$!
178+
trap 'kill $pid 2>/dev/null || true' EXIT
179+
sleep 0.3
180+
go -C test/interop/pion run . {{addr}}
181+
169182
# ── everything ────────────────────────────────────────────────────────────
170183

171-
# Full check: lint, Go tests, and the integration tests (Python + Go e2e).
172-
check: lint test test-py test-e2e
184+
# Full check: lint, Go tests (race), and the integration tests (Python + Go e2e).
185+
# Uses test-race to match CI — a plain `go test` won't surface data races.
186+
check: lint test-race test-py test-e2e
187+
188+
# Everything in `check` plus third-party (pion) interop.
189+
check-all: check interop

test/interop/pion/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module stun-interop-pion
2+
3+
go 1.26.4
4+
5+
require github.com/pion/stun/v3 v3.1.6
6+
7+
require (
8+
github.com/pion/dtls/v3 v3.1.4 // indirect
9+
github.com/pion/logging v0.2.4 // indirect
10+
github.com/pion/transport/v4 v4.0.2 // indirect
11+
github.com/wlynxg/anet v0.0.5 // indirect
12+
golang.org/x/crypto v0.48.0 // indirect
13+
golang.org/x/sys v0.41.0 // indirect
14+
)

test/interop/pion/go.sum

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
4+
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
5+
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
6+
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
7+
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
8+
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
9+
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
10+
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
14+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
15+
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
16+
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
17+
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
18+
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
19+
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
20+
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
21+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
22+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
23+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

test/interop/pion/main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Command pion-interop sends a STUN Binding request to a running stund using
2+
// pion/stun — a wholly independent Go implementation — and validates the
3+
// XOR-MAPPED-ADDRESS it gets back. Sharing no code with our server, it catches
4+
// the class of wire-format bug that a stunc-against-stund e2e never can.
5+
//
6+
// It lives in its own module so pion/stun stays out of the main go.mod.
7+
//
8+
// go -C test/interop/pion run . 127.0.0.1:3478
9+
package main
10+
11+
import (
12+
"fmt"
13+
"os"
14+
15+
"github.com/pion/stun/v3"
16+
)
17+
18+
func main() {
19+
addr := "127.0.0.1:3478"
20+
if len(os.Args) > 1 {
21+
addr = os.Args[1]
22+
}
23+
24+
c, err := stun.Dial("udp", addr)
25+
if err != nil {
26+
fail("dial", err)
27+
}
28+
defer c.Close() //nolint:errcheck // best-effort on a throwaway client
29+
30+
var (
31+
xorAddr stun.XORMappedAddress
32+
inner error
33+
)
34+
req := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
35+
if err := c.Do(req, func(res stun.Event) {
36+
if res.Error != nil {
37+
inner = res.Error
38+
return
39+
}
40+
inner = xorAddr.GetFrom(res.Message)
41+
}); err != nil {
42+
fail("binding", err)
43+
}
44+
if inner != nil {
45+
fail("response", inner)
46+
}
47+
48+
fmt.Printf("pion interop OK: XOR-MAPPED-ADDRESS %s\n", xorAddr)
49+
}
50+
51+
func fail(stage string, err error) {
52+
fmt.Fprintf(os.Stderr, "pion-interop: %s: %v\n", stage, err)
53+
os.Exit(1)
54+
}

0 commit comments

Comments
 (0)