-
-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: Implement ICE tester in stunnerctl
- Loading branch information
Showing
30 changed files
with
2,801 additions
and
463 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
########### | ||
# BUILD | ||
FROM docker.io/golang:1.23-alpine as builder | ||
|
||
WORKDIR /app | ||
|
||
COPY go.mod ./ | ||
COPY go.sum ./ | ||
|
||
COPY *.go ./ | ||
COPY internal/ internal/ | ||
COPY pkg/ pkg/ | ||
|
||
COPY cmd/ cmd/ | ||
|
||
COPY .git ./ | ||
COPY Makefile ./ | ||
RUN apk add --no-cache git make | ||
|
||
RUN apkArch="$(apk --print-arch)"; \ | ||
case "$apkArch" in \ | ||
aarch64) export GOARCH='arm64' ;; \ | ||
*) export GOARCH='amd64' ;; \ | ||
esac; \ | ||
export CGO_ENABLED=0; \ | ||
export GOOS=linux; \ | ||
make build-bin | ||
|
||
########### | ||
# STUNNERD | ||
FROM scratch | ||
|
||
WORKDIR /app | ||
|
||
COPY --from=builder /app/bin/icetester /usr/bin/ | ||
|
||
EXPOSE 8089/tcp | ||
|
||
CMD [ "icetester", "-l", "all:INFO" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# icetester: Universal UDP echo service using WebRTC/ICE | ||
|
||
`icetester` is test server that can be used WebRTC/ICE connectivity. The tester serves a simple | ||
WebSocket/JSON API server that clients can use to create a WebRTC data channel. whatever is | ||
received by `icetester` on the data channel will be echoed back to the client over the data channel. | ||
|
||
While `icetester` can be used as a standalone too, the intended use is via `stunnerctl icetest`. | ||
|
||
## Installation | ||
|
||
Install `icetest` using the standard Go toolchain and add it to `$PATH`. | ||
|
||
```console | ||
go install github.com/l7mp/stunner/cmd/icetest@latest | ||
``` | ||
|
||
Building from source is as easy as it usually gets with Go: | ||
|
||
```console | ||
cd stunner | ||
go build -o turncat cmd/turncat/main.go | ||
``` | ||
|
||
The containerized version is available as `docker.io/l7mp/icester`. | ||
|
||
## Usage | ||
|
||
Deploy a STUNner gateway and test is via UDP and TCP through `stunnerctl`: | ||
|
||
```console | ||
stunnerctl icetest | ||
``` | ||
|
||
## License | ||
|
||
Copyright 2021-2024 by its authors. Some rights reserved. See [AUTHORS](../../AUTHORS). | ||
|
||
MIT License - see [LICENSE](../../LICENSE) for full text. | ||
|
||
## Acknowledgments | ||
|
||
Initial code adopted from [pion/stun](https://github.com/pion/stun) and [pion/turn](https://github.com/pion/turn). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"net" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/l7mp/stunner/pkg/logger" | ||
"github.com/l7mp/stunner/pkg/whipconn" | ||
) | ||
|
||
var ( | ||
testerLogLevel = "all:WARN" | ||
// testerLogLevel = "all:TRACE" | ||
// testerLogLevel = "all:INFO" | ||
defaultConfig = whipconn.Config{BearerToken: "whiptoken"} | ||
) | ||
|
||
func echoTest(t *testing.T, conn net.Conn, content string) { | ||
t.Helper() | ||
|
||
n, err := conn.Write([]byte(content)) | ||
assert.NoError(t, err) | ||
assert.Equal(t, len(content), n) | ||
|
||
buf := make([]byte, 2048) | ||
n, err = conn.Read(buf) | ||
assert.NoError(t, err) | ||
assert.Equal(t, content, string(buf[:n])) | ||
} | ||
|
||
var testerTestCases = []struct { | ||
name string | ||
config *whipconn.Config | ||
tester func(t *testing.T, ctx context.Context) | ||
}{ | ||
{ | ||
name: "Basic connectivity", | ||
tester: func(t *testing.T, ctx context.Context) { | ||
log.Debug("Creating dialer") | ||
d := whipconn.NewDialer(defaultConfig, loggerFactory) | ||
assert.NotNil(t, d) | ||
|
||
log.Debug("Dialing") | ||
clientConn, err := d.DialContext(ctx, defaultICETesterAddr) | ||
assert.NoError(t, err) | ||
|
||
log.Debug("Echo test round 1") | ||
echoTest(t, clientConn, "test1") | ||
log.Debug("Echo test round 2") | ||
echoTest(t, clientConn, "test2") | ||
|
||
assert.NoError(t, clientConn.Close(), "client conn close") | ||
}, | ||
}, | ||
} | ||
|
||
func TestICETesterConn(t *testing.T) { | ||
loggerFactory = logger.NewLoggerFactory(testerLogLevel) | ||
log = loggerFactory.NewLogger("icester") | ||
|
||
for _, c := range testerTestCases { | ||
t.Run(c.name, func(t *testing.T) { | ||
log.Infof("--------------------- %s ----------------------", c.name) | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
config := defaultConfig | ||
if c.config != nil { | ||
config = *c.config | ||
} | ||
|
||
log.Debug("Running listener loop") | ||
go func() { | ||
err := runICETesterListener(ctx, defaultICETesterAddr, config) | ||
assert.NoError(t, err) | ||
}() | ||
|
||
c.tester(t, ctx) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
|
||
"github.com/pion/logging" | ||
"github.com/pion/webrtc/v4" | ||
flag "github.com/spf13/pflag" | ||
|
||
v1 "github.com/l7mp/stunner/pkg/apis/v1" | ||
"github.com/l7mp/stunner/pkg/buildinfo" | ||
"github.com/l7mp/stunner/pkg/logger" | ||
"github.com/l7mp/stunner/pkg/whipconn" | ||
) | ||
|
||
const ( | ||
// Name of the environment variable specifying the list of ICE servers, default is no ICE servers. | ||
EnvVarNameICEServers = "ICE_SERVERS" | ||
|
||
// Name of the environment variable specifying the ICE transport policy (either "relay" or "all"), default is "all". | ||
EnvVarNameICETransportPolicy = "ICE_TRANSPORT_POLICY" | ||
|
||
// HIP bearer token for authenticating WHIP requests, default is no bearer token. | ||
EnvVarNameBearerToken = "BEARER_TOKEN" | ||
|
||
// WHIP API endpoint, default is "/whip". Must include the leading slash ("/"). | ||
EnvVarNameWHIPEndpoint = "WHIP_ENDPOINT" | ||
) | ||
|
||
var ( | ||
version = "dev" | ||
commitHash = "n/a" | ||
buildDate = "<unknown>" | ||
|
||
defaultICEServers = []webrtc.ICEServer{} | ||
defaultICETransportPolicy = webrtc.NewICETransportPolicy("all") | ||
defaultBearerToken = "" | ||
defaultWHIPEndpoint = "/whip" | ||
defaultICETesterAddr = fmt.Sprintf(":%d", v1.DefaultICETesterPort) | ||
|
||
loggerFactory logging.LoggerFactory | ||
log logging.LeveledLogger | ||
) | ||
|
||
func main() { | ||
os.Args[0] = "icetester" | ||
var whipServerAddr = flag.StringP("addr", "a", defaultICETesterAddr, "WHIP server listener address") | ||
var level = flag.StringP("log", "l", "all:WARN", "Log level") | ||
var verbose = flag.BoolP("verbose", "v", false, "Enable verbose logging, identical to -l all:DEBUG") | ||
|
||
flag.Parse() | ||
|
||
if *verbose { | ||
*level = "all:DEBUG" | ||
} | ||
|
||
loggerFactory = logger.NewLoggerFactory(*level) | ||
log = loggerFactory.NewLogger("icester") | ||
|
||
buildInfo := buildinfo.BuildInfo{Version: version, CommitHash: commitHash, BuildDate: buildDate} | ||
log.Debugf("Starting icetester %s", buildInfo.String()) | ||
|
||
iceServers := defaultICEServers | ||
if os.Getenv(EnvVarNameICEServers) != "" { | ||
s := []webrtc.ICEServer{} | ||
if err := json.Unmarshal([]byte(os.Getenv(EnvVarNameICEServers)), &s); err != nil { | ||
log.Errorf("Environment ICE_SERVERS is invalid: %s", err.Error()) | ||
os.Exit(1) | ||
} | ||
iceServers = s | ||
} | ||
|
||
iceTransportPolicy := defaultICETransportPolicy | ||
if os.Getenv(EnvVarNameICETransportPolicy) != "" { | ||
iceTransportPolicy = webrtc.NewICETransportPolicy(os.Getenv(EnvVarNameICETransportPolicy)) | ||
} | ||
|
||
token := defaultBearerToken | ||
if os.Getenv(EnvVarNameBearerToken) != "" { | ||
token = os.Getenv(EnvVarNameBearerToken) | ||
} | ||
|
||
whipEndpoint := defaultWHIPEndpoint | ||
if os.Getenv(EnvVarNameWHIPEndpoint) != "" { | ||
endpoint := os.Getenv(EnvVarNameWHIPEndpoint) | ||
if endpoint[0] != '/' { | ||
log.Errorf("Environment WHIP_ENDPOINT is invalid: %s, expecting a leading slash '/'", endpoint) | ||
os.Exit(1) | ||
} | ||
whipEndpoint = endpoint | ||
} | ||
|
||
whipServerConfig := whipconn.Config{ | ||
ICEServers: iceServers, | ||
ICETransportPolicy: iceTransportPolicy, | ||
BearerToken: token, | ||
WHIPEndpoint: whipEndpoint, | ||
} | ||
|
||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||
defer stop() | ||
|
||
if err := runICETesterListener(ctx, *whipServerAddr, whipServerConfig); err != nil { | ||
log.Errorf("Could not create WHIP server listener: %s", err.Error()) | ||
os.Exit(1) | ||
} | ||
|
||
os.Exit(0) | ||
} | ||
|
||
func runICETesterListener(ctx context.Context, addr string, config whipconn.Config) error { | ||
log.Infof("Creating WHIP server listener with config %#v", config) | ||
l, err := whipconn.NewListener(addr, config, loggerFactory) | ||
if err != nil { | ||
return fmt.Errorf("Could not create WHIP server listener: %s", err.Error()) | ||
} | ||
|
||
log.Debug("Creating echo service") | ||
go func() { | ||
for { | ||
conn, err := l.Accept() | ||
if err != nil { | ||
return | ||
} | ||
|
||
log.Debugf("Accepting WHIP server connection with resource ID: %s", | ||
conn.(*whipconn.ListenerConn).ResourceUrl) | ||
|
||
// readloop | ||
go func() { | ||
buf := make([]byte, 100) | ||
for { | ||
n, err := conn.Read(buf) | ||
if err != nil { | ||
return | ||
} | ||
|
||
_, err = conn.Write(buf[:n]) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
}() | ||
} | ||
}() | ||
|
||
<-ctx.Done() | ||
|
||
for _, conn := range l.GetConns() { | ||
if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) && | ||
!errors.Is(err, http.ErrServerClosed) { | ||
return fmt.Errorf("WHIP connection close error: %s", err.Error()) | ||
} | ||
} | ||
|
||
l.Close() | ||
|
||
return nil | ||
} |
Oops, something went wrong.