Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ build/**
.userData
config/proxy.conf
.vscode/**

# kindling-tester local env files
cmd/kindling-tester/.env
cmd/kindling-tester/.env.local
60 changes: 60 additions & 0 deletions cmd/kindling-tester/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Kindling transport tester

Tests individual [kindling](../../kindling) transports (proxyless, fronted, amp, dnstt).
It receives all its arguments via environment variables and uses the kindling HTTP client directly

## Environment variables

### Required

- `DEVICE_ID`: The device ID to use.
- `USER_ID`: The user ID to use.
- `TOKEN`: The auth token to use.
- `RUN_ID`: The run ID. Added to traces as `pinger-id` — useful for looking up a specific run.
- `TARGET_URL`: The URL that will be fetched through kindling.
- `DATA`: Directory for config files, logs, and output artefacts (`output.txt`, `timing.txt`, `success`).

- `TRANSPORT`: The kindling transport to test. One of: `proxyless`, `fronted`, `amp`, `dnstt`.

## CLI usage

```bash
DEVICE_ID=1234 USER_ID=123 TOKEN=mytoken RUN_ID=run1 TARGET_URL=https://example.com DATA=./mydir \
TRANSPORT=proxyless \
./kindling-tester
```

Replace `proxyless` with the transport you want to test (`fronted`, `amp`, `dnstt`).

Upon success the tester writes:
- `DATA/success` — empty marker file
- `DATA/output.txt` — response body
- `DATA/timing.txt` — timing breakdown (connect + fetch latency)

## Docker usage

A separate image is built per transport. Each image bakes `TRANSPORT` in at build time via `ENV TRANSPORT=${TRANSPORT}`.

### Building

```bash
docker build --build-arg TRANSPORT=proxyless -t radiance-kindling-tester:proxyless -f ./docker/Dockerfile.kindling-tester .
docker build --build-arg TRANSPORT=fronted -t radiance-kindling-tester:fronted -f ./docker/Dockerfile.kindling-tester .
docker build --build-arg TRANSPORT=amp -t radiance-kindling-tester:amp -f ./docker/Dockerfile.kindling-tester .
docker build --build-arg TRANSPORT=dnstt -t radiance-kindling-tester:dnstt -f ./docker/Dockerfile.kindling-tester .
```

### Running

```bash
docker run --rm -v ./mydir:/output \
-e DEVICE_ID=1234 \
-e USER_ID=1234 \
-e TOKEN=mytoken \
-e RUN_ID=run1 \
-e TARGET_URL=https://example.com \
-e DATA=/output \
radiance-kindling-tester:proxyless
```

Swap the image tag to test a different transport; no other flags need to change.
111 changes: 111 additions & 0 deletions cmd/kindling-tester/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strconv"
"time"

"github.com/getlantern/radiance/common/settings"
"github.com/getlantern/radiance/kindling"
)

func performKindlingPing(ctx context.Context, urlToHit string, runID string, deviceID string, userID int64, token string, dataDir string) error {
os.MkdirAll(dataDir, 0o755)
settings.Set(settings.DataPathKey, dataDir)
settings.Set(settings.UserIDKey, userID)
settings.Set(settings.TokenKey, token)
settings.Set(settings.UserLevelKey, "")
settings.Set(settings.EmailKey, "pinger@pinger.com")
settings.Set(settings.DevicesKey, []settings.Device{
{
ID: deviceID,
Name: deviceID,
},
})

t1 := time.Now()
kindling.SetKindling(kindling.NewKindling())
defer kindling.Close(ctx)
cli := kindling.HTTPClient()

t2 := time.Now()
// Run the command and capture the output
resp, err := cli.Get(urlToHit)
if err != nil {
slog.Error("failed on get request", slog.Any("error", err))
return err
}
defer resp.Body.Close()

responseBody, err := io.ReadAll(resp.Body)
if err != nil {
slog.Error("failed to read response body", slog.Any("error", err))
return err
}
t3 := time.Now()
slog.Info("lantern ping completed successfully")
// create a marker file that will be used by the pinger to determine success
if err := os.WriteFile(dataDir+"/success", []byte(""), 0o644); err != nil {
slog.Error("failed to write success file", slog.Any("error", err), slog.String("path", dataDir+"/success"))
}
if err := os.WriteFile(dataDir+"/output.txt", responseBody, 0o644); err != nil {
slog.Error("failed to write output file", slog.Any("error", err), slog.String("path", dataDir+"/output.txt"))
}
return os.WriteFile(dataDir+"/timing.txt", []byte(fmt.Sprintf(`
result: %v
run-id: %s
err: %v
started: %s
connected: %d
fetched: %d
url: %s`,
true, runID, nil, t1, int32(t2.Sub(t1).Milliseconds()), int32(t3.Sub(t1).Milliseconds()), urlToHit)), 0o644)
}

func main() {
deviceID := os.Getenv("DEVICE_ID")
userID := os.Getenv("USER_ID")
token := os.Getenv("TOKEN")
runID := os.Getenv("RUN_ID")
targetURL := os.Getenv("TARGET_URL")
data := os.Getenv("DATA")
transport := os.Getenv("TRANSPORT")

slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))

if deviceID == "" || runID == "" || targetURL == "" || data == "" || transport == "" {
slog.Error("missing required environment variable(s), required environment variables: DEVICE_ID, RUN_ID, TARGET_URL, DATA, TRANSPORT", slog.String("deviceID", deviceID), slog.String("runID", runID), slog.String("targetURL", targetURL), slog.String("data", data), slog.String("transport", transport))
os.Exit(1)
}

var uid int64

if userID != "" {
var err error
uid, err = strconv.ParseInt(userID, 10, 64)
if err != nil {
slog.Error("failed to parse USER_ID", slog.Any("error", err))
os.Exit(1)
}
}

ctx := context.Background()

// disabling all other transports before enabling the selected
for name := range kindling.EnabledTransports {
kindling.EnabledTransports[name] = false
}

kindling.EnabledTransports[transport] = true
slog.Debug("enabled transports", slog.Any("enabled_transports", kindling.EnabledTransports))
if err := performKindlingPing(ctx, targetURL, runID, deviceID, uid, token, data); err != nil {
slog.Error("failed to perform kindling ping", slog.Any("error", err))
os.Exit(1)
}
}
26 changes: 26 additions & 0 deletions docker/Dockerfile.kindling-tester
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM golang:1.25.1 AS builder

RUN apt-get update && apt-get install -y git git-lfs bsdmainutils

ARG TARGETOS
ARG TARGETARCH

WORKDIR /src

COPY . .

ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="$GOCACHE" GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /kindling-tester ./cmd/kindling-tester/...

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /kindling-tester /kindling-tester

# TRANSPORT is set at build time (e.g. --build-arg TRANSPORT=proxyless).
# The entrypoint exports it as the transport-specific env var expected by the binary.
ARG TRANSPORT
ENV TRANSPORT=${TRANSPORT}

ENTRYPOINT ["sh", "-c", "/kindling-tester"]
89 changes: 57 additions & 32 deletions kindling/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/getlantern/radiance/common"
"github.com/getlantern/radiance/common/reporting"
"github.com/getlantern/radiance/common/settings"
"github.com/getlantern/radiance/kindling/dnstt"
"github.com/getlantern/radiance/kindling/fronted"
"github.com/getlantern/radiance/traces"
"go.opentelemetry.io/otel"
Expand All @@ -23,6 +24,13 @@ var (
kindlingMutex sync.Mutex
stopUpdater func()
closeTransports []func() error
// EnabledTransports is used for testing purposes for enabling/disabling kindling transports
EnabledTransports = map[string]bool{
"dnstt": false,
"amp": true,
"proxyless": true,
"fronted": true,
}
)

// HTTPClient returns a http client with kindling transport
Expand Down Expand Up @@ -73,32 +81,6 @@ func NewKindling() kindling.Kindling {
)
defer span.End()

updaterCtx, cancel := context.WithCancel(ctx)
f, err := fronted.NewFronted(updaterCtx, reporting.PanicListener, filepath.Join(dataDir, "fronted_cache.json"), logger)
if err != nil {
slog.Error("failed to create fronted client", slog.Any("error", err))
span.RecordError(err)
}

ampClient, err := fronted.NewAMPClient(updaterCtx, dataDir, logger)
if err != nil {
slog.Error("failed to create amp client", slog.Any("error", err))
span.RecordError(err)
}

stopUpdater = cancel
closeTransports = []func() error{
func() error {
if f != nil {
f.Close()
}
return nil
},
func() error {
return nil
},
}

if common.Stage() {
// Disable domain fronting for stage environment to avoid hitting staging server issues due to fronted client failures.
return kindling.NewKindling("radiance",
Expand All @@ -109,16 +91,59 @@ func NewKindling() kindling.Kindling {
kindling.WithProxyless("df.iantem.io", "api.getiantem.org", "api.staging.iantem.io"),
)
}
return kindling.NewKindling("radiance",

closeTransports = []func() error{}
kindlingOptions := []kindling.Option{
kindling.WithPanicListener(reporting.PanicListener),
kindling.WithLogWriter(logger),
kindling.WithDomainFronting(f),
}

updaterCtx, cancel := context.WithCancel(ctx)
if enabled := EnabledTransports["fronted"]; enabled {
f, err := fronted.NewFronted(updaterCtx, reporting.PanicListener, filepath.Join(dataDir, "fronted_cache.json"), logger)
if err != nil {
slog.Error("failed to create fronted client", slog.Any("error", err))
span.RecordError(err)
}
closeTransports = append(closeTransports, func() error {
if f != nil {
f.Close()
}
return nil
})
kindlingOptions = append(kindlingOptions, kindling.WithDomainFronting(f))
}

if enabled := EnabledTransports["amp"]; enabled {
ampClient, err := fronted.NewAMPClient(updaterCtx, dataDir, logger)
if err != nil {
slog.Error("failed to create amp client", slog.Any("error", err))
span.RecordError(err)
}
// Kindling will skip amp transports if the request has a payload larger than 6kb
kindlingOptions = append(kindlingOptions, kindling.WithAMPCache(ampClient))
}

if enabled := EnabledTransports["dnstt"]; enabled {
dnsttOptions, err := dnstt.DNSTTOptions(updaterCtx, filepath.Join(dataDir, "dnstt.yml.gz"), logger)
if err != nil {
slog.Error("failed to create or load dnstt kindling options", slog.Any("error", err))
span.RecordError(err)
}
if dnsttOptions != nil {
closeTransports = append(closeTransports, dnsttOptions.Close)
}
kindlingOptions = append(kindlingOptions, kindling.WithDNSTunnel(dnsttOptions))
}

if enabled := EnabledTransports["proxyless"]; enabled {
// Most endpoints use df.iantem.io, but for some historical reasons
// "pro-server" calls still go to api.getiantem.org.
kindling.WithProxyless("df.iantem.io", "api.getiantem.org"),
// Kindling will skip amp transports if the request has a payload larger than 6kb
kindling.WithAMPCache(ampClient),
)
kindlingOptions = append(kindlingOptions, kindling.WithProxyless("df.iantem.io", "api.getiantem.org"))
}

stopUpdater = cancel
return kindling.NewKindling("radiance", kindlingOptions...)
}

type slogWriter struct {
Expand Down