Last active
January 8, 2024 02:07
-
-
Save Asutorufa/4c03462386b313f6f760b52ae34a87bb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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