go-stun finds your public address and your NAT type, and stops there
A go implementation of the STUN client (RFC 3489 and RFC 5389)
At a glance
- What is it?
- ccding/go-stun is a Go library and command line client for STUN over UDP and TCP, with RFC 3489 NAT type discovery and RFC 5780 behaviour tests. The README is explicit that it is one building block for NAT traversal and not a hole punching, ICE or TURN implementation, and the tagged release is behind the master branch.
- Who is it for?
- Adopt go-stun if you are writing a Go application that needs to learn its own public address or classify its NAT, and you want the RFC 3489 and RFC 5780 behaviour logic rather than a single Binding request. Do not adopt it expecting connectivity, since the README states it does not implement UDP hole punching, ICE or TURN.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One building block, and the README says so first
STUN is how a host behind a NAT finds out what its address looks like from the outside. go-stun is a Go library and command line client for STUN over UDP and TCP, and it does two things: it discovers the public IP address and port assigned to a socket, and, for UDP when the server supports the required probes, it classifies the client's NAT behaviour.
The scoping statement appears in the second paragraph of the README and is the most important line in the project for anyone evaluating it. STUN is one building block for NAT traversal, and this project does not implement a complete UDP hole-punching, ICE, or TURN solution.
That is a genuinely useful boundary to have written down. A lot of STUN client libraries are described in terms of the use case their users have in mind, and the reader is left to discover the missing half. Here the library does address discovery and NAT classification, and everything after that, actually opening a path through the NAT for media, is somebody else's job.
The audience is Go developers building peer to peer applications, particularly WebRTC related ones, who need to know their reflexive address and the shape of their NAT before choosing a traversal strategy. The repository topics list webrtc and nat-traversal alongside stun and rfc-5389, which matches.
The licence is Apache-2.0, the module path is github.com/ccding/go-stun, and the code is split into a stun package and a main package with the command line tool.
Install from master, because the tag is behind the code
The install instructions are unusual, and the reason is worth reading before you copy the command.
The README documents the current master branch, and with Go 1.16 or newer it tells you to install that branch explicitly until the next release is tagged:
go install github.com/ccding/go-stun@masterIt then names what you lose by using the released version instead. The newest tag, v0.1.5, predates RFC 3489 compatibility mode, the -legacy flag, and ErrBehaviorDiscoveryUnsupported, so @latest is only worth choosing if you specifically need the older released interface.
That is a real maintenance gap rather than a footnote. The release list shows v0.1.6 published on 2026-07-19, v0.1.5 on 2024-07-01 and v0.1.4 on 2022-03-04, while the README still points at v0.1.5 as the newest tag. Whichever you pick, the library API and the CLI flags differ between the two lines, so pin deliberately.
Building from a checkout works the same way as any Go command:
git clone https://github.com/ccding/go-stun.git
cd go-stun
go build .
./go-stunAfter an install, the binary lands in your Go binary directory, usually $GOBIN or $GOPATH/bin, which has to be on your PATH before `go-stun` will run.
One inconsistency to note. The go.mod in the repository declares go 1.14, while the README asks for Go 1.16 or newer to install the command. The module directive and the documented requirement do not agree, so take the README's number as the real floor.
What the output actually tells you
Running the tool with no options uses the default server and an automatically selected local address, and the output is short:
NAT Type: Full cone NAT
External IP Family: 1
External IP: 203.0.113.10
External Port: 54321The values depend on the network and the server, and the address family number is documented: 1 denotes IPv4 and 2 denotes IPv6.
The NAT type itself comes from the classic RFC 3489 discovery procedure, which classifies the device as one of the familiar cone, restricted and symmetric kinds. Full cone NAT is the most permissive case, where inbound traffic from any address is forwarded to the internal client.
Behaviour mode, enabled with the -b flag, produces a longer report:
External IP Family: 1
External IP: 203.0.113.10
External Port: 54321
Port Preservation: true
Mapping Behavior: EndpointIndependent
Filtering Behavior: EndpointIndependent
Normal NAT Type: Full cone NATThe mapping and filtering lines come from the RFC 5780 tests, and they are a more precise description than the RFC 3489 label. Port preservation compares the external port with the socket's actual local port, including a port chosen automatically when you pass -p 0.
The caveat the README attaches is important and easy to misread. Port Preservation describes the initial mapping to this server. Other destinations or later connections may use different ports, so a true value does not mean the mapping is stable across the whole internet.
Two smaller behaviours are worth knowing. The address and the port preservation result stay available if the server cannot support behaviour discovery or a later probe fails, and the port preservation line is simply omitted when the local port cannot be determined.
The flags that change what gets probed
The command line surface is a short table, and every entry maps to a specific behaviour.
-s host:port selects a specific STUN server, and -i ip binds requests to a local IP address. -p port binds requests to a local port, where 0 selects an available port. -b runs the RFC 5780 mapping and filtering behaviour tests and reports the external address and observed port preservation. -legacy omits the modern optional attributes for servers that only speak RFC 3489. -t transport selects udp, the default, or tcp, and the note in the table is that TCP performs basic Binding only. -v sets verbosity to 0 for quiet, 1 for a protocol trace, or 2 and 3 which also dump packets in hex, with values above 3 rejected.
The README suggests a few invocations:
go-stun -s stun.example.com:3478
go-stun -s stun.example.com:3478 -t tcp
go-stun -s stun.example.com:3478 -b
go-stun -v 1Two of those deserve a comment. The TCP transport is limited to a basic Binding transaction, so the NAT classification that the UDP path performs is not available there, which is consistent with the project's own statement that NAT behaviour classification is a UDP feature when the server supports the required probes. And the -legacy flag exists because a server that only implements RFC 3489 will not understand the SOFTWARE and FINGERPRINT attributes that the library sends by default, so the compatibility mode drops them.
For the current defaults, the README points you at `go-stun -h`.
Discover, DiscoverTCP, and taking over an existing socket
The library side is a small API surface, and the README's example is the whole of it in spirit:
package main
import (
"fmt"
"log"
"github.com/ccding/go-stun/stun"
)
func main() {
client := stun.NewClient()
natType, mappedAddr, err := client.Discover()The rest of the example prints the NAT type and, when the mapped address is not nil, the mapped address itself. If no server is configured, the client falls back to stun.DefaultServerAddr, and the other configuration methods are SetServerHost, SetLocalIP, SetLocalPort, SetSoftwareName and the verbosity setters.
The interesting part is what happens when you already have a socket. NewClientWithConnection accepts a net.PacketConn created by an applicable net.Listen* function, and the caller remains responsible for closing it. The library also exposes Keepalive, which can refresh the mapping, which matters for a NAT that expires idle flows.
TCP has its own path. DiscoverTCP performs a basic Binding transaction over TCP and opens and closes the connection around it. Because a reflexive TCP address remains useful only while its connection is open, the README is explicit that applications needing to retain the mapping should dial the server themselves and use NewClientWithTCPConnection, and can call DiscoverTCP again on the owned connection to refresh it. SetTCPTimeout replaces the RFC 8489 default response timeout of 39.5 seconds, and when DiscoverTCP opens the connection itself, that same duration also bounds the connection establishment.
For documentation matching your module version, the README points at `go doc github.com/ccding/go-stun/stun`, and warns that the linked package reference shows the latest tagged release and will not include master-only symbols.
When the server cannot do behaviour discovery
Discover does a standard Binding request first. If that succeeds but the server does not provide a usable alternate address, the library returns stun.NATUnknown together with the mapped address and a nil error.
That is a deliberate API decision. The mapped address is still valid even though the NAT type could not be determined, so the caller gets usable data plus an explicit unknown classification, rather than an error that discards both. If your code treats an error as fatal, this path will not trigger; if your code ignores the NAT type, you can proceed safely.
For the deeper test, the library exposes BehaviorTestWithDetails, which performs the RFC 5780 mapping and filtering tests together with the external address and port preservation observation. The error handling pattern the README shows separates the unsupported case from a genuine failure:
result, err := client.BehaviorTestWithDetails()
if errors.Is(err, stun.ErrBehaviorDiscoveryUnsupported) {
fmt.Println("The server does not support behavior discovery")
} else if err != nil {
fmt.Println("Behavior test failed:", err)
}The sentinel error ErrBehaviorDiscoveryUnsupported only exists on the master branch, which is another reason the README steers you away from the v0.1.5 tag. The result carries the mapping type, the filtering type and the mapped address, and each of the first two can be compared against stun.BehaviorTypeUnknown.
The practical consequence for an operator is that the classification you get depends on the server you query. A server that supports the RFC 5780 alternate server mechanism gives you the full picture; one that does not gives you an address and an honest unknown.
Maintenance, the release gap and the module directive
The repository is not archived, and the last push was on 2026-10-12, so the code is being worked on. The release history tells a different story about the published artefacts. v0.1.6 was published on 2026-07-19, v0.1.5 on 2024-07-01 and v0.1.4 on 2022-03-04, and the README still describes v0.1.5 as the newest tag, which does not line up with the release list.
That mismatch matters for a dependency. A project that imports github.com/ccding/go-stun/stun and pins a version gets the old API without the compatibility mode, the -legacy flag and the behaviour discovery error. The upstream project's own answer is to install from master, which in a Go module means depending on a moving target.
The repository is small in a way that is easy to audit. The top level holds the go.mod, a main.go and main_test.go for the command, the stun directory with the library, a SECURITY.md for responsible disclosure, and linter_config.json for the lint setup. The CI badge points at a go.yml workflow. There is also a .github directory, which is where that workflow lives.
The licence is Apache-2.0, with the text in the LICENSE file, which permits use in closed source products and carries the usual patent grant and notice obligations.
Where go-stun is the wrong tool
The first wrong case is expecting it to open a path through the NAT. The README says the project does not implement UDP hole punching, ICE or TURN, and that is not a missing feature so much as a different product. If you need media connectivity, you need the ICE agent that negotiates candidates and the relay that carries traffic when direct paths fail.
The second wrong case is TCP behaviour classification. The -t tcp transport performs a basic Binding only, and the NAT classification is described as a UDP capability. If your application runs over TCP and you need to know the NAT shape, this tool will not give it to you.
The third is a server that does not support the RFC 5780 mechanism. The README is candid that behaviour discovery needs a server that provides a usable alternate address, and when it does not, you get stun.NATUnknown and a mapped address. Code that treats the NAT type as essential should handle that path explicitly rather than assuming a classification.
The alternative approach for the same problem is a raw Binding request written by hand, which for a single address query is a few lines of code against any STUN server. The difference in approach is what this library adds: the RFC 3489 sequence of probes that classifies the NAT type, the RFC 5780 mapping and filtering tests, the optional attribute handling with a compatibility mode for old servers, and a command line tool that gives you the same information interactively while you debug a network. If you only need the reflexive address and your protocol already does the rest, the hand written request is enough. If you need to know what kind of NAT you are behind, the library saves you from implementing the probe sequence correctly, which is exactly the part that is easy to get subtly wrong.
Editorial conclusion
Adopt go-stun if you are writing a Go application that needs to learn its own public address or classify its NAT, and you want the RFC 3489 and RFC 5780 behaviour logic rather than a single Binding request. Do not adopt it expecting connectivity, since the README states it does not implement UDP hole punching, ICE or TURN. Before you depend on it, check whether you are on master or on the v0.1.5 tag, because the tag predates the RFC 3489 compatibility mode, the -legacy flag and ErrBehaviorDiscoveryUnsupported, and note that the go.mod still declares go 1.14 while the README asks for Go 1.16 or newer to install the command.
Frequently asked questions
What does go-stun do?
It is a Go library and command line client for STUN over UDP and TCP that discovers the public, server reflexive IP address and port of a socket and, for UDP with a supporting server, classifies the client's NAT behaviour using RFC 3489 and RFC 5780.
How do I install the go-stun command?
The README documents the master branch and asks for Go 1.16 or newer, with the install command go install github.com/ccding/go-stun@master, and explains that the newest tag v0.1.5 predates the RFC 3489 compatibility mode, the -legacy flag and ErrBehaviorDiscoveryUnsupported.
Does go-stun implement hole punching, ICE or TURN?
No. The README states that STUN is one building block for NAT traversal and that this project does not implement a complete UDP hole punching, ICE or TURN solution. It stops at address discovery and NAT classification.
What happens when the STUN server does not support behaviour discovery?
Discover returns stun.NATUnknown together with the mapped address and a nil error, so the address stays usable. The library also exposes ErrBehaviorDiscoveryUnsupported through BehaviorTestWithDetails, which lets a caller distinguish an unsupported server from a failed test.
How do I use go-stun from Go code?
Add the package with go get github.com/ccding/go-stun/stun@master, then create a client with stun.NewClient and call Discover for UDP, or DiscoverTCP for a basic Binding over TCP. For a socket you already own, use stun.NewClientWithConnection with a net.PacketConn, and keep the connection yourself if you need the mapping to persist.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ccding-go-stun)Was this useful?