Skip to content

Commit

Permalink
feature: Implement ICE tester in stunnerctl
Browse files Browse the repository at this point in the history
  • Loading branch information
rg0now committed Nov 28, 2024
1 parent 74d5480 commit f48067f
Show file tree
Hide file tree
Showing 30 changed files with 2,801 additions and 463 deletions.
39 changes: 39 additions & 0 deletions Dockerfile.icetester
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" ]
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ test: generate fmt vet
build: generate fmt vet build-bin

.PHONY: build-bin
bin: build-bin
build-bin:
go build ${GOARGS} -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/stunnerd cmd/stunnerd/main.go
go build ${GOARGS} -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/turncat cmd/turncat/main.go
go build ${GOARGS} -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/stunnerctl cmd/stunnerctl/main.go
go build ${GOARGS} -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/stunnerctl cmd/stunnerctl/*.go
go build ${GOARGS} -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/icetester cmd/icetester/main.go

.PHONY: clean
clean:
Expand Down
42 changes: 42 additions & 0 deletions cmd/icetester/README.md
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).
84 changes: 84 additions & 0 deletions cmd/icetester/icetester_test.go
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)
})
}
}
166 changes: 166 additions & 0 deletions cmd/icetester/main.go
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
}
Loading

0 comments on commit f48067f

Please sign in to comment.