EndlessNet STUN
endlessnet-stun is a standalone STUN product. EndlessNet integrates it through standard UDP Binding to discover public UDP mappings; other compatible clients can use it independently. It does not depend on the EndlessNet control plane, database, credentials, or internal Go packages.
This repository owns the STUN source, tests, immutable artifacts, publication,
container image, and service configuration examples. Production inventory, host
access, activation, rollout, and rollback belong exclusively to Infrastructure
under D-014/D-026. The public history in this repository is authoritative.
Endpoint selection and client-side Binding behavior are owned by their
respective consumers.
Boundaries
The service provides unauthenticated STUN Binding over UDP, per-source-IP token-bucket limiting, structured JSON logs, Prometheus metrics, and HTTP health/readiness endpoints. It does not provide TURN, relay user traffic, authorization, billing, coordinator access, PostgreSQL access, DNS discovery, Anycast, or Kubernetes deployment.
Protocol support is documented in docs/supported-protocol.md. The stable network contract is a standard STUN Binding request and response; repository releases do not change public DNS names or UDP ports.
Documentation
Configuration
Flags override environment variables. All configuration is validated before listeners start.
| Environment variable |
Flag |
Default |
ENDLESSNET_STUN_ADDRS |
--addr |
required, comma-separated |
ENDLESSNET_STUN_METRICS_ADDR |
--metrics-addr |
127.0.0.1:9090 |
ENDLESSNET_STUN_RATE_LIMIT_PER_SECOND |
--rate-limit-per-second |
20 |
ENDLESSNET_STUN_RATE_LIMIT_BURST |
--rate-limit-burst |
40 |
ENDLESSNET_STUN_LOG_LEVEL |
--log-level |
info |
Explicit IPv4 and IPv6 listen addresses use separate socket families. Use
0.0.0.0:3478,[::]:3478 to serve both families on the same port; IPv6 must be
available on the host. An empty host (:3478) lets Go select the socket family.
Use the safe example in configs/stun.example.env; no secrets are required. Validate a host configuration without opening sockets:
set -a
. /etc/endlessnet-stun/stun.env
set +a
/opt/endlessnet-stun/current/endlessnet-stun --check-config
Local development
go run ./cmd/endlessnet-stun \
--addr 127.0.0.1:3478 \
--metrics-addr 127.0.0.1:9090
Run the complete quality gate:
./scripts/verify.sh
It runs module verification, unit and process-level integration tests, race tests, go vet, formatting checks, and builds the service and smoke-test binaries. A real local Binding request can be checked with:
./scripts/smoke-test.sh \
--stun-addr 127.0.0.1:3478
Container
docker build -t endlessnet-stun:dev .
docker run --read-only --cap-drop ALL --network host \
-e ENDLESSNET_STUN_ADDRS=0.0.0.0:3478 \
-e ENDLESSNET_STUN_METRICS_ADDR=127.0.0.1:9090 \
endlessnet-stun:dev
The image runs as a non-root user. deploy/docker/docker-compose.example.yml pins an exact example version; operational deployments must never use latest.
Local health and observability
GET http://127.0.0.1:9090/healthz reports process health.
GET http://127.0.0.1:9090/readyz returns success only while every configured UDP listener is active. All UDP and HTTP sockets are bound before serving starts.
GET http://127.0.0.1:9090/metrics returns Prometheus text format for the host-local Alloy agent.
GET http://127.0.0.1:9090/revisionz returns the running executable's SHA-256 digest, commit, version, and build date for Infrastructure diagnostics. These fields are also present in stun_build_info; they contain no client or deployment configuration data.
The HTTP listener is required to use a loopback address. Health, readiness, and metrics are not part of the public network contract; only STUN on UDP port 3478 is exposed externally.
Metrics include stun_requests_total, stun_responses_total, stun_invalid_requests_total, stun_rate_limited_total, stun_errors_total, stun_active_listeners, stun_request_duration_seconds, and stun_build_info. Labels are limited to listener, result, and address family; source IP is never a label. Successful client addresses and packet bodies are not logged. Rejected request logs redact the remote address.
Release process
The single .github/workflows/ci.yml workflow runs checks on pull requests and commits to main. Tags matching vMAJOR.MINOR.PATCH run its release job. A tag is accepted only when its commit is reachable from origin/main and GitHub associates that commit with a merged pull request whose base is main. The workflow repeats the quality gate, requires all three product E2E jobs from a successful main CI run for the exact commit, builds Linux AMD64 and ARM64 server and smoke-test binaries, creates SHA256 checksums, and publishes a GitHub Release and GHCR image. Major and minor image tags are mutable aliases; consumers pin an image digest or an exact binary with its checksum.
Update CHANGELOG.md on a feature branch, merge it through a pull request and CI into main, and only then create a signed or protected version tag. Direct-push and unmerged commits are rejected by the release provenance check.
Commit-addressed production publication
The manual publish operation in .github/workflows/ci.yml accepts one full
commit SHA from main through the commit_sha input. The default manual
operation, check, only runs checks. Publication requires a successful
ci.yml run for that application commit and
for the exact main commit containing the publisher workflow. A successful run
uploads one Actions artifact named endlessnet-stun-<commit_sha> containing:
- one archive with AMD64 and ARM64 server and smoke-test binaries, the systemd
unit, and license notices;
- the archive SHA-256 checksum;
- a schema-v1 publication manifest binding the archive to the producer commit,
CI runs, and publication run;
- an in-toto/SLSA provenance statement and its Sigstore/Rekor bundle.
The producer workflow and Sigstore certificate identity now use
.github/workflows/ci.yml@refs/heads/main. Infrastructure consumers must trust
this exact path for new artifacts; the manifest schema remains unchanged.
Publisher scripts are checked out at the workflow revision separately from the
application source, so publishing an earlier application commit uses the verified
current publisher.
The workflow verifies the archive layout, checksum, manifest, provenance, and
Rekor entry before upload. It has no target, inventory, production credentials,
or host access. This commit-addressed artifact is the STUN producer side of D-014/D-026;
the public semver Release and GHCR image remain a separate supported contract.
Publication ends after upload and verification. It does not accept targets or
inventory, read deployment secrets, connect to hosts, activate a release,
perform rollout, or rollback. Infrastructure consumes the immutable artifact
through separate STUN desired state and owns the complete production lifecycle.
STUN is not a component of the common EndlessNet server released manifest.
Troubleshooting
- Configuration failure: run
endlessnet-stun --check-config with the environment file loaded.
- UDP timeout: verify DNS, host firewall, security group, and
3478/udp; TCP reachability does not prove STUN reachability.
- Readiness failure: inspect
systemctl status endlessnet-stun and confirm at least one configured UDP address can bind.
- Rate limiting: inspect
stun_rate_limited_total and adjust both rate and burst deliberately.
- Production lifecycle issues: inspect the separate Infrastructure STUN desired state and rollout; this repository only publishes and verifies artifacts.
EndlessNet clients continue to obtain endpoints such as stun1.endlessnet.ru:3478 from signed network maps or control-plane configuration. The client has no dependency on this repository's Go packages or release version.
Product E2E
CI runs stun-e2e-linux, stun-e2e-windows, and stun-e2e-container on
GitHub-hosted runners for PRs, main pushes, and manual runs. These required
checks exercise only this product, using independent UDP wire vectors and real
server/smoke processes. Linux containers use a local host network. No public
production endpoint, EndlessNet Client, or control plane is needed.
Run go test -tags=e2e -count=1 ./test/e2e. Local IPv6 is required; missing
capabilities fail instead of silently skipping. Optional STUN_E2E_EVIDENCE
selects an output directory for logs and binary identities; STUN_E2E_IMAGE
selects a prebuilt Linux image. CI preserves JSON test results and identities.
The publisher also verifies the exact commit's successful product E2E jobs.
This evidence does not constitute production or full EndlessNet acceptance.