# pion/stun: a STUN implementation with zero-allocation hot paths

> pion/stun is a Go implementation of the STUN protocol from RFC 5389 with no external dependencies and no allocations on its hot paths, plus a client that retransmits automatically. The repository lists six implemented RFCs, tests end to end against a coturn server, and guards its public API with an explicit backward compatibility check.

**pion/stun** — A Go implementation of STUN

- Repository: https://github.com/pion/stun
- Website: https://pion.ly/
- Stars: 864 · Forks: 121
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/pion-stun

## The address discovery problem, in one dependency

STUN is Session Traversal Utilities for NAT, and the package stun implements the protocol and a client, with no external dependencies and zero allocations in hot paths. The client supports automatic request retransmissions.

The task it solves is the one every application that sits behind a router hits: you want to know what address the rest of the world sees you as, and you want to know whether the NAT in front of you will let a peer reach you directly or whether the traffic has to be relayed. STUN gives you the first answer through a binding request and a binding response, and it is the foundation that the rest of the traversal machinery is built on.

The project belongs to the Pion family, whose homepage is pion.ly, and the repository topics place it firmly in that world: webrtc, ice, sip, voip, streaming, peer-to-peer, networking and stun. That context explains the engineering priorities. A STUN client in a WebRTC stack is called constantly, on a path where a garbage collection pause is visible, and it runs on servers as well as on phones, where the dependency footprint matters.

The licence is MIT, the default branch is main, and the last push was on 2026-10-17. The releases tell their own story: v4.0.0 on 2026-09-02, v4.0.1 on 2026-09-15, and v5.0.0-rc.1 on 2026-09-30, so a new major version is in release candidate while the v4 line is still being patched.

## The example is the whole API

The README's example is the API in miniature, and it is worth reading line by line because there are no hidden layers.

The first step is parsing a STUN URI, and the URI scheme is RFC 7064, which the repository lists as implemented:

```go
	u, err := stun.ParseURI("stun:stun.l.google.com:19302")
	if err != nil {
		panic(err)
	}

	// Creating a "connection" to STUN server.
	c, err := stun.DialURI(u)
```

The second step is building a binding request with a random transaction ID, which is stun.MustBuild(stun.TransactionID, stun.BindingRequest). The Must form panics on error, which is the right choice for a message you build by hand in an example.

The third step is sending it and handling the response in a callback, c.Do(message, func(res stun.Event)). The event carries an Error field for a failure response and a Message for the successful one, and the address is pulled out of the message with a typed attribute, XORMappedAddress, whose GetFrom method decodes the XOR-MAPPED-ADDRESS attribute and whose IP field is the address you came to learn.

The README also points at cmd/stun-client as the more idiomatic example, so the snippet in the README is the tutorial version and the command directory holds the version written the way a real program would be.

What is absent is as informative as what is present. There is no connection pool, no retry policy in the example, no context parameter and no error wrapping beyond the panic, because the client handles retransmission itself.

## The RFC list is the compatibility statement

The repository documents which specifications are implemented, and this is the section to read if you are choosing a library for a specific job.

Implemented are RFC 5389, the STUN protocol itself, RFC 5769 with its test vectors, RFC 6062 for TURN extensions for TCP allocations, RFC 7064 for the STUN URI scheme, RFC 7065 for TURN URIs, and RFC 5780 for NAT behaviour discovery, the last through a separate command, cmd/stun-nat-behaviour. The client also supports TLS over TCP.

The planned list is short: ALTERNATE-SERVER support from RFC 5389, tracked as issue 48.

Then there is the compatibility note, which is the kind of sentence that saves an afternoon. RFC 5389 obsoletes RFC 3489, so the implementation of the older specification was ignored by purpose, and the README adds that RFC 3489 can be easily implemented as separate package if you need it.

That is a deliberate scoping decision rather than an omission. A protocol library that implements both an obsolete and a current specification of the same thing has to pick a default and document it, and this one has picked the current one and said so plainly. The same attitude shows up in the stability section, which says the package is currently stable, that no backward incompatible changes are expected with the exception of critical bugs or security fixes, and that additional attributes are unlikely to be implemented in scope of the stun package, the only exception being constants for attribute or message types.

## Zero allocations, and a benchmark table that shows it

The README claims no external dependencies and zero allocations in hot paths, and the benchmark section is where the second half is demonstrated. The table is run on an Intel Core i7-8700K with Go 1.22.2 on linux and amd64.

The interesting rows are the attribute accessors and the message operations, because those are what a client calls in a loop:

```
benchmark                                         iter       time/iter      throughput   bytes alloc        allocs
---------                                         ----       ---------      ----------   -----------        ------
BenchmarkMappedAddress_AddTo-12               32489450     38.30 ns/op                        0 B/op   0 allocs/op
BenchmarkMessage_Get-12                      230520343      5.21 ns/op                        0 B/op   0 allocs/op
BenchmarkClient_Do-12                          1282231    943.00 ns/op                        0 B/op   0 allocs/op
```

Reading a message attribute costs about five nanoseconds, and the whole client round trip, which includes the network wait, is under a microsecond of CPU time. The zero in the bytes and allocs columns is the point: the library does not hand the garbage collector anything in these paths.

The Makefile shows how that property is kept honest. There is a bench target, a bench-record target that writes the results to a file named after the Go version, and a bench-compare target that runs the same benchmarks under two Go versions and diffs them with benchcmp, so a change in allocation behaviour shows up as a diff between two Go releases. There is also a target called escape, which compiles the package with the compiler's escape analysis output and greps for escapes while excluding autogenerated code, so an accidental heap allocation in a hot path fails a build rather than quietly costing throughput.

## Testing against a real TURN server, and an API compatibility gate

The testing section describes four layers, and two of them are unusual for a library of this size.

The first is end to end with long-term credentials against the coturn server, a real TURN implementation, on Linux. The second is a bunch of code static checkers, the linters, which the Makefile wires up as golangci-lint run ./... with a .golangci.yml in the root. The third is standard unit tests with coverage reporting, run on Linux for both amd64 and arm64, plus Windows and Darwin, with codecov.yml for the coverage reporting. The fourth is the one that stands out: an explicit API backward compatibility check, run against the gortc/api tool, with an api directory in the repository, and the README points at a TeamCity project and the e2e directory for more information. The Wireshark pcap files for the end to end test are available in the build artifacts, which means a failing network test can be replayed and inspected rather than only read as a log.

The integration target is a separate script, test-integration runs the e2e directory's own test.sh, and the prepush target chains test, lint and test-integration together, so a local push runs the same three things the project cares about.

The rest of the tree explains the shape of the library. The root is a flat set of files by subject, addr.go, agent.go, attributes.go, checks.go, client.go, errorcode.go, fingerprint.go, helpers.go, integrity.go, message.go, stun.go, textattrs.go, uattrs.go, uri.go and xoraddr.go, with a _debug.go variant beside several of them, which is a convention for compiling extra diagnostics only in a build tagged for it. There is a stuntest/ package, a testdata/ directory, an internal/ directory, and a cmd/ directory with the example commands. The .reuse/ directory and LICENSES/ directory are for REUSE compliance, the licence metadata format that projects adopt when they ship more than one licence artefact.

## The Go version requirement and the module path both need checking

Two details in the metadata are worth resolving before you add this to a project.

The first is the Go version. The README's Requirements section says Go 1.12 is currently supported and tested in CI, while the go.mod in the repository declares go 1.24.0, and the module path is github.com/pion/stun/v4. The go.mod is what the toolchain enforces, so a 1.12 era project cannot build the current code whatever the README says. The README sentence is a leftover from an earlier version of the project, and the module file is the one to trust.

The second is the version line. There is a v5.0.0-rc.1 tag published on 2026-09-30, and the go.mod still says /v4, which is what a release candidate looks like before the module path is finalised. Depending on a v5 release candidate means depending on a path that is about to change, so the safe choice today is the v4 line, which is still receiving patches, with v4.0.1 from 2026-09-15 as the current tag.

The dependency list is short and all from the same family, plus a test framework. The direct requirements are pion/dtls v4.0.0-rc.2, pion/logging v0.2.4, pion/transport v5.1.1 and stretchr/testify v1.12.1, with anet, yaml, x/crypto and x/sys as indirect. Two of those are release candidates or major-version bumps in flight, the DTLS package at an rc and transport at a v5, which is consistent with a project that develops its own components together.

The build tooling is also in the tree, a .goreleaser.yml for cutting the release binaries, renovate.json for dependency updates, and a Makefile that derives the version from git describe, so the tag is the single source of truth for the version string.

## Conclusion

Adopt pion/stun if you are writing anything that has to discover its own address behind a NAT, whether that is a WebRTC stack, a SIP client or a custom peer to peer system, because it implements the RFCs the job actually needs, ships a client with automatic retransmission, and has no dependency footprint to audit. Do not adopt it if you need a feature the README lists as planned, such as ALTERNATE-SERVER support, or if you need the older RFC 3489 behaviour, which is deliberately not implemented. Before you pin a version, read the module path and the tags together, because a v5 release candidate is published while the go.mod module path is still the v4 path, and check the Go requirement from go.mod rather than from the README, since the two statements disagree.

## FAQ

### How do I find my public IP address with pion/stun?

Parse a STUN URI with stun.ParseURI, dial it with stun.DialURI, build a binding request with stun.MustBuild(stun.TransactionID, stun.BindingRequest), and send it with c.Do, decoding the address from the XORMappedAddress attribute. The README also points at cmd/stun-client as a more idiomatic example.

### Which RFCs does pion/stun implement?

RFC 5389 for STUN itself, RFC 5769 for the test vectors, RFC 6062 for TURN extensions for TCP allocations, RFC 7064 for the STUN URI scheme, RFC 7065 for TURN URIs, and RFC 5780 for NAT behaviour discovery through cmd/stun-nat-behaviour. ALTERNATE-SERVER support is listed as planned, and RFC 3489 was deliberately not implemented because RFC 5389 obsoletes it.

### Does pion/stun have any dependencies?

The package itself has no external dependencies, and the client supports automatic request retransmissions. The repository's go.mod lists the other Pion components it builds against, including pion/transport and pion/dtls, plus testify for tests.

### How is pion/stun tested?

End to end with long-term credentials against a coturn server, several linters, unit tests with coverage across Linux on amd64 and arm64 plus Windows and Darwin, and an explicit API backward compatibility check against the gortc/api tool. The Wireshark pcap files for the end to end test are published in the build artifacts.

## Sources

- [License: MIT](https://github.com/pion/stun/blob/main/LICENSE)
- [pion/stun on GitHub](https://github.com/pion/stun)
- [Project website](https://pion.ly/)
- [README](https://github.com/pion/stun/blob/main/README.md)
- [Releases](https://github.com/pion/stun/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pion-stun
