Skip to content

Instantly share code, notes, and snippets.

@Asutorufa
Last active January 8, 2024 02:07
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Asutorufa/4c03462386b313f6f760b52ae34a87bb to your computer and use it in GitHub Desktop.
Save Asutorufa/4c03462386b313f6f760b52ae34a87bb to your computer and use it in GitHub Desktop.
package main
import (
"errors"
"flag"
"fmt"
"net"
"time"
"github.com/pion/stun"
"github.com/sirupsen/logrus"
)
//go:generate go run ../errorgen
type stunServerConn struct {
conn net.PacketConn
LocalAddr net.Addr
RemoteAddr *net.UDPAddr
OtherAddr *net.UDPAddr
messageChan chan *stunResponse
}
type stunResponse struct {
*stun.Message
net.Addr
}
func (c *stunServerConn) Close() error {
return c.conn.Close()
}
var timeout = 15
const (
messageHeaderSize = 20
)
const (
NoResult = iota
EndpointIndependentNoNAT
EndpointIndependent
AddressDependent
AddressAndPortDependent
)
var (
errResponseMessage = errors.New("error reading from response message channel")
errTimedOut = errors.New("timed out waiting for response")
errNoOtherAddress = errors.New("no OTHER-ADDRESS in message")
)
func main() {
logrus.SetLevel(logrus.DebugLevel)
server := flag.String("s", "", "stun server")
timeou := flag.Int("t", 15, "timeout")
flag.Parse()
timeout = *timeou
natMapping, natFiltering, err := Test(*server)
if err != nil {
logrus.Panic(err)
}
switch natMapping {
case NoResult:
logrus.Info(`NAT Mapping: udp no response`)
case EndpointIndependentNoNAT:
logrus.Info(`NAT Mapping: Public Internet with No NAT`)
case EndpointIndependent:
logrus.Info(`NAT Mapping: Endpoint-Independent Mapping
The NAT reuses the port mapping for subsequent packets sent from
the same internal IP address and port (X:x) to any external IP
address and port.`)
case AddressDependent:
logrus.Info(`NAT Mapping: Address-Dependent Mapping
The NAT reuses the port mapping for subsequent packets sent from
the same internal IP address and port (X:x) to the same external
IP address, regardless of the external port.`)
case AddressAndPortDependent:
logrus.Info(`NAT Mapping: Address and Port-Dependent Mapping
The NAT reuses the port mapping for subsequent packets sent from
the same internal IP address and port (X:x) to the same external
IP address and port while the mapping is still active.`)
default:
logrus.Info(`NAT Mapping: Unknown NAT Mapping type:`, natMapping)
}
switch natFiltering {
case NoResult:
logrus.Info(`NAT Filtering: udp no response`)
case EndpointIndependentNoNAT:
logrus.Info(`NAT Filtering: Public Internet with No NAT`)
case EndpointIndependent:
logrus.Info(`NAT Filtering: Endpoint-Independent Filtering
The NAT filters out only packets not destined to the internal
address and port X:x, regardless of the external IP address and
port source (Z:z). The NAT forwards any packets destined to
X:x. In other words, sending packets from the internal side of
the NAT to any external IP address is sufficient to allow any
packets back to the internal endpoint.
A NAT device employing the combination of "Endpoint-Independent
Mapping" and "Endpoint-Independent Filtering" will accept incoming
traffic to a mapped public port from ANY external endpoint on the
public network.`)
case AddressDependent:
logrus.Info(`NAT Filtering: Address-Dependent Filtering
The NAT filters out packets not destined to the internal address
X:x. Additionally, the NAT will filter out packets from Y:y
destined for the internal endpoint X:x if X:x has not sent
packets to Y:any previously (independently of the port used by
Y). In other words, for receiving packets from a specific
external endpoint, it is necessary for the internal endpoint to
send packets first to that specific external endpoint's IP
address.`)
case AddressAndPortDependent:
logrus.Info(`NAT Filtering: Address and Port-Dependent Filtering
The NAT filters out packets not destined for the internal
address X:x. Additionally, the NAT will filter out packets from
Y:y destined for the internal endpoint X:x if X:x has not sent
packets to Y:y previously. In other words, for receiving
packets from a specific external endpoint, it is necessary for
the internal endpoint to send packets first to that external
endpoint's IP address and port.`)
default:
logrus.Info(`NAT Filtering: Unknown NAT Filtering type:`, natMapping)
}
}
func Test(addrStr string) (natMapping int, natFiltering int, err error) {
if addrStr == "" {
// addrStr = "stun.voip.blackberry.com:3478"
addrStr = "stun.stunprotocol.org:3478"
}
addr, err := net.ResolveUDPAddr("udp", addrStr)
if err != nil {
return 0, 0, newError("failed to resolve server address ", addrStr, err)
}
newConn := func() *stunServerConn {
conn, err := net.ListenPacket("udp", "")
if err != nil {
return nil
}
return &stunServerConn{
conn: conn,
messageChan: listen(conn),
LocalAddr: conn.LocalAddr(),
RemoteAddr: addr,
}
}
if mapTestConn := newConn(); mapTestConn != nil {
natMapping, err = mappingTests(mapTestConn)
}
if mapTestConn := newConn(); mapTestConn != nil {
natFiltering, err = filteringTests(mapTestConn)
}
return
}
// RFC5780: 4.3. Determining NAT Mapping Behavior
func mappingTests(mapTestConn *stunServerConn) (int, error) {
defer mapTestConn.Close()
// Test I: Regular binding request
logrus.Info(newError("mapping test I: regular binding request"))
request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
if err != nil {
return NoResult, err
}
// Parse response message for XOR-MAPPED-ADDRESS and make sure OTHER-ADDRESS valid
resps := parse(resp.Message)
if resps.xorAddr == nil || resps.otherAddr == nil {
err := newError("NAT discovery feature not supported by this server", errNoOtherAddress)
logrus.Warn(err)
return NoResult, err
}
addr, err := net.ResolveUDPAddr("udp4", resps.otherAddr.String())
if err != nil {
err := newError("failed resolving OTHER-ADDRESS: ", resps.otherAddr)
logrus.Warn(err)
return NoResult, err
}
mapTestConn.OtherAddr = addr
logrus.Info(newError("received XOR-MAPPED-ADDRESS: ", resps.xorAddr))
// Assert mapping behavior
if resps.xorAddr.String() == mapTestConn.LocalAddr.String() {
logrus.Info(newError("NAT mapping behavior: endpoint independent (no NAT)"))
return EndpointIndependentNoNAT, err
}
// Test II: Send binding request to the other address but primary port
logrus.Info(newError("mapping test II: Send binding request to the other address but primary port"))
oaddr := *mapTestConn.OtherAddr
oaddr.Port = mapTestConn.RemoteAddr.Port
resp, err = mapTestConn.roundTrip(request, &oaddr)
if err != nil {
if !errors.Is(err, errTimedOut) {
return NoResult, err
}
} else {
// Assert mapping behavior
resps2 := parse(resp.Message)
if resps2.respOrigin.String() != oaddr.String() {
logrus.Info(newError("NAT mapping behavior: address and port dependent"))
return AddressAndPortDependent, nil
}
logrus.Info(newError("received XOR-MAPPED-ADDRESS: ", resps2.xorAddr))
if resps2.xorAddr.String() == resps.xorAddr.String() {
logrus.Info(newError("NAT mapping behavior: endpoint independent"))
return EndpointIndependent, nil
}
resps = resps2
}
// Test III: Send binding request to the other address and port
logrus.Info(newError("mapping test III: Send binding request to the other address and port"))
resp, err = mapTestConn.roundTrip(request, mapTestConn.OtherAddr)
if err != nil {
if !errors.Is(err, errTimedOut) {
return NoResult, err
}
} else {
resps3 := parse(resp.Message)
logrus.Info(newError("received XOR-MAPPED-ADDRESS: ", resps3.xorAddr))
if resps3.xorAddr.String() == resps.xorAddr.String() {
logrus.Info(newError("NAT mapping behavior: address dependent"))
return AddressDependent, nil
}
}
logrus.Info(newError("NAT mapping behavior: address and port dependent"))
return AddressAndPortDependent, nil
}
// RFC5780: 4.4. Determining NAT Filtering Behavior
func filteringTests(mapTestConn *stunServerConn) (int, error) {
defer mapTestConn.Close()
// Test I: Regular binding request
logrus.Info(newError("filtering test I: regular binding request"))
request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
if err != nil {
return NoResult, err
}
resps := parse(resp.Message)
if resps.xorAddr == nil || resps.otherAddr == nil {
err := newError("NAT discovery feature not supported by this server", errNoOtherAddress)
logrus.Warn(err)
return NoResult, err
}
addr, err := net.ResolveUDPAddr("udp", resps.otherAddr.String())
if err != nil {
err := newError("failed resolving OTHER-ADDRESS: ", resps.otherAddr, err)
logrus.Warn(err)
return NoResult, err
}
mapTestConn.OtherAddr = addr
// Test II: Request to change both IP and port
logrus.Info(newError("filtering test II: request to change both IP and port"))
request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
if err == nil {
parse(resp.Message) // just to print out the resp
if resp.Addr.String() != mapTestConn.RemoteAddr.String() {
logrus.Info(newError("NAT filtering behavior: endpoint independent"))
return EndpointIndependent, nil
}
} else if !errors.Is(err, errTimedOut) {
return NoResult, err // something else went wrong
}
// Test III: Request to change port only
logrus.Info(newError("filtering test III: request to change port only"))
request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
if err == nil {
parse(resp.Message) // just to print out the resp
if resp.Addr.String() != mapTestConn.RemoteAddr.String() {
logrus.Info(newError("NAT filtering behavior: address dependent"))
return AddressDependent, nil
}
} else if !errors.Is(err, errTimedOut) {
return NoResult, err
}
logrus.Info(newError("NAT filtering behavior: address and port dependent"))
return AddressAndPortDependent, nil
}
// Parse a STUN message
func parse(msg *stun.Message) (ret struct {
xorAddr *stun.XORMappedAddress
otherAddr *stun.OtherAddress
respOrigin *stun.ResponseOrigin
mappedAddr *stun.MappedAddress
software *stun.Software
},
) {
ret.mappedAddr = &stun.MappedAddress{}
ret.xorAddr = &stun.XORMappedAddress{}
ret.respOrigin = &stun.ResponseOrigin{}
ret.otherAddr = &stun.OtherAddress{}
ret.software = &stun.Software{}
if ret.xorAddr.GetFrom(msg) != nil {
ret.xorAddr = nil
}
if ret.otherAddr.GetFrom(msg) != nil {
ret.otherAddr = nil
}
if ret.respOrigin.GetFrom(msg) != nil {
ret.respOrigin = nil
}
if ret.mappedAddr.GetFrom(msg) != nil {
ret.mappedAddr = nil
}
if ret.software.GetFrom(msg) != nil {
ret.software = nil
}
logrus.Debug(newError(msg))
logrus.Debug(newError("MAPPED-ADDRESS: ", ret.mappedAddr))
logrus.Debug(newError("XOR-MAPPED-ADDRESS: ", ret.xorAddr))
logrus.Debug(newError("RESPONSE-ORIGIN: ", ret.respOrigin))
logrus.Debug(newError("OTHER-ADDRESS: ", ret.otherAddr))
logrus.Debug(newError("SOFTWARE: ", ret.software))
for _, attr := range msg.Attributes {
switch attr.Type {
case
stun.AttrXORMappedAddress,
stun.AttrOtherAddress,
stun.AttrResponseOrigin,
stun.AttrMappedAddress,
stun.AttrSoftware:
default:
logrus.Debug(newErrorf("%v (l=%v)", attr, attr.Length))
}
}
return ret
}
// Send request and wait for response or timeout
func (c *stunServerConn) roundTrip(msg *stun.Message, addr net.Addr) (*stunResponse, error) {
_ = msg.NewTransactionID()
logrus.Debug(newErrorf("sending to %v: (%v bytes)", addr, msg.Length+messageHeaderSize))
logrus.Debug(newError(msg))
for _, attr := range msg.Attributes {
logrus.Debug(newErrorf("%v (l=%v)", attr, attr.Length))
}
_, err := c.conn.WriteTo(msg.Raw, addr)
if err != nil {
logrus.Warn(newError("error sending request to ", addr))
return nil, err
}
// Wait for response or timeout
select {
case r, ok := <-c.messageChan:
if !ok {
return nil, errResponseMessage
}
return r, nil
case <-time.After(time.Duration(timeout) * time.Second):
logrus.Info(newError("timed out waiting for response from server ", addr))
return nil, errTimedOut
}
}
// taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go
func listen(conn net.PacketConn) (messages chan *stunResponse) {
messages = make(chan *stunResponse)
go func() {
b := make([]byte, 65535)
for {
n, addr, err := conn.ReadFrom(b)
if err != nil {
close(messages)
return
}
logrus.Info(newErrorf("response from %v: (%d bytes)", addr, n))
b = b[:n]
r := &stunResponse{
Message: &stun.Message{
Raw: b,
},
Addr: addr,
}
err = r.Message.Decode()
if err != nil {
logrus.Warn(newError("error decoding message", err))
close(messages)
return
}
messages <- r
}
}()
return
}
func newError(values ...interface{}) error {
return errors.New(fmt.Sprint(values...))
}
func newErrorf(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment