diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c4c70d..392d73d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: run: | git config --global user.name 'github-actions' git config --global user.email 'github-actions@github.com' - TAG="v0.0.1-$(date +'%Y%m%d%H%M%S')" + TAG="v0.0.2-$(date +'%Y%m%d%H%M%S')" git tag $TAG git push origin $TAG env: diff --git a/cmd/main.go b/cmd/main.go index 3c2477c..28e7db0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,75 +1,33 @@ package main import ( - "errors" "flag" "fmt" - "net" "net/http" "os" - "time" + "github.com/oneclickvirt/gostun/model" + "github.com/oneclickvirt/gostun/stuncheck" "github.com/pion/logging" - "github.com/pion/stun/v2" -) - -// From https://github.com/pion/stun/blob/master/cmd/stun-nat-behaviour/main.go -// I only make changes to summarize the NAT type - -// my changes start -const GoStunVersion = "v0.0.1" -var ( - NatMappingBehavior string - NatFilteringBehavior string -) -// my changes end - -type stunServerConn struct { - conn net.PacketConn - LocalAddr net.Addr - RemoteAddr *net.UDPAddr - OtherAddr *net.UDPAddr - messageChan chan *stun.Message -} - -func (c *stunServerConn) Close() error { - return c.conn.Close() -} - -var ( - addrStrPtr = flag.String("server", "stun.voipgate.com:3478", "STUN server address") //nolint:gochecknoglobals - timeoutPtr = flag.Int("timeout", 3, "the number of seconds to wait for STUN server's response") //nolint:gochecknoglobals - verbose = flag.Int("verbose", 0, "the verbosity level") //nolint:gochecknoglobals // my changes - log logging.LeveledLogger //nolint:gochecknoglobals -) - -const ( - messageHeaderSize = 20 -) - -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() { - // flag.Parse() - // my changes start var showVersion bool flag.BoolVar(&showVersion, "v", false, "show version") + flag.IntVar(&model.Verbose, "verbose", 0, "the verbosity level") + flag.IntVar(&model.Timeout, "timeout", 3, "the number of seconds to wait for STUN server's response") + flag.StringVar(&model.AddrStr, "server", "stun.voipgate.com:3478", "STUN server address") flag.Parse() go func() { http.Get("https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Foneclickvirt%2Fgostun&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false") }() fmt.Println("项目地址:", "https://github.com/oneclickvirt/gostun") if showVersion { - fmt.Println(GoStunVersion) + fmt.Println(model.GoStunVersion) return } - // my changes end var logLevel logging.LogLevel - switch *verbose { + switch model.Verbose { case 0: logLevel = logging.LogLevelWarn // default // my changes case 1: @@ -79,16 +37,15 @@ func main() { case 3: logLevel = logging.LogLevelTrace } - log = logging.NewDefaultLeveledLoggerForScope("", logLevel, os.Stdout) - - if *addrStrPtr == "stun.voipgate.com:3478" { - if err := mappingTests(*addrStrPtr); err != nil { - NatMappingBehavior = "inconclusive" // my changes - log.Warn("NAT mapping behavior: inconclusive") + model.Log = logging.NewDefaultLeveledLoggerForScope("", logLevel, os.Stdout) + if model.AddrStr != "stun.voipgate.com:3478" { + if err := stuncheck.MappingTests(model.AddrStr); err != nil { + model.NatMappingBehavior = "inconclusive" // my changes + model.Log.Warn("NAT mapping behavior: inconclusive") } - if err := filteringTests(*addrStrPtr); err != nil { - NatFilteringBehavior = "inconclusive" // my changes - log.Warn("NAT filtering behavior: inconclusive") + if err := stuncheck.FilteringTests(model.AddrStr); err != nil { + model.NatFilteringBehavior = "inconclusive" // my changes + model.Log.Warn("NAT filtering behavior: inconclusive") } } else { addrStrPtrList := []string{ @@ -98,21 +55,21 @@ func main() { } checkStatus := true for _, addrStr := range addrStrPtrList { - err1 := mappingTests(addrStr) + err1 := stuncheck.MappingTests(addrStr) if err1 != nil { - NatMappingBehavior = "inconclusive" - log.Warn("NAT mapping behavior: inconclusive") + model.NatMappingBehavior = "inconclusive" + model.Log.Warn("NAT mapping behavior: inconclusive") checkStatus = false } - err2 := filteringTests(addrStr) + err2 := stuncheck.FilteringTests(addrStr) if err2 != nil { - NatFilteringBehavior = "inconclusive" - log.Warn("NAT filtering behavior: inconclusive") + model.NatFilteringBehavior = "inconclusive" + model.Log.Warn("NAT filtering behavior: inconclusive") checkStatus = false } - if NatMappingBehavior == "inconclusive" || NatFilteringBehavior == "inconclusive" { + if model.NatMappingBehavior == "inconclusive" || model.NatFilteringBehavior == "inconclusive" { checkStatus = false - } else if NatMappingBehavior != "inconclusive" && NatFilteringBehavior != "inconclusive" { + } else if model.NatMappingBehavior != "inconclusive" && model.NatFilteringBehavior != "inconclusive" { checkStatus = true } if checkStatus { @@ -120,294 +77,6 @@ func main() { } } } - // my changes start - if NatMappingBehavior != "" && NatFilteringBehavior != "" { - if NatMappingBehavior == "inconclusive" || NatFilteringBehavior == "inconclusive" { - fmt.Println("NAT Type: Inconclusive") - } else if NatMappingBehavior == "endpoint independent" && NatFilteringBehavior == "endpoint independent" { - fmt.Println("NAT Type: Full Cone") - } else if NatMappingBehavior == "endpoint independent" && NatFilteringBehavior == "address dependent" { - fmt.Println("NAT Type: Restricted Cone") - } else if NatMappingBehavior == "endpoint independent" && NatFilteringBehavior == "address and port dependent" { - fmt.Println("NAT Type: Port Restricted Cone") - } else if NatMappingBehavior == "address and port dependent" && NatFilteringBehavior == "address and port dependent" { - fmt.Println("NAT Type: Symmetric") - } else { - fmt.Printf("NAT Type: %v[NatMappingBehavior] %v[NatFilteringBehavior]\n", NatMappingBehavior, NatFilteringBehavior) - } - } else { - fmt.Println("NAT Type: Inconclusive") - } - // my changes end -} - -// RFC5780: 4.3. Determining NAT Mapping Behavior -func mappingTests(addrStr string) error { - mapTestConn, err := connect(addrStr) - if err != nil { - log.Warnf("Error creating STUN connection: %s", err) - return err - } - - // Test I: Regular binding request - log.Info("Mapping Test I: Regular binding request") - request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) - - resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) - if err != nil { - return err - } - - // Parse response message for XOR-MAPPED-ADDRESS and make sure OTHER-ADDRESS valid - resps1 := parse(resp) - if resps1.xorAddr == nil || resps1.otherAddr == nil { - log.Info("Error: NAT discovery feature not supported by this server") - return errNoOtherAddress - } - addr, err := net.ResolveUDPAddr("udp4", resps1.otherAddr.String()) - if err != nil { - log.Infof("Failed resolving OTHER-ADDRESS: %v", resps1.otherAddr) - return err - } - mapTestConn.OtherAddr = addr - log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps1.xorAddr) - - // Assert mapping behavior - if resps1.xorAddr.String() == mapTestConn.LocalAddr.String() { - NatMappingBehavior = "endpoint independent (no NAT)" // my changes - log.Warn("=> NAT mapping behavior: endpoint independent (no NAT)") - return nil - } - - // Test II: Send binding request to the other address but primary port - log.Info("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 { - return err - } - - // Assert mapping behavior - resps2 := parse(resp) - log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps2.xorAddr) - if resps2.xorAddr.String() == resps1.xorAddr.String() { - NatMappingBehavior = "endpoint independent" // my changes - log.Warn("=> NAT mapping behavior: endpoint independent") - return nil - } - - // Test III: Send binding request to the other address and port - log.Info("Mapping Test III: Send binding request to the other address and port") - resp, err = mapTestConn.roundTrip(request, mapTestConn.OtherAddr) - if err != nil { - return err - } - - // Assert mapping behavior - resps3 := parse(resp) - log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps3.xorAddr) - if resps3.xorAddr.String() == resps2.xorAddr.String() { - NatMappingBehavior = "address dependent" // my changes - log.Warn("=> NAT mapping behavior: address dependent") - } else { - NatMappingBehavior = "address and port dependent" // my changes - log.Warn("=> NAT mapping behavior: address and port dependent") - } - return mapTestConn.Close() -} - -// RFC5780: 4.4. Determining NAT Filtering Behavior -func filteringTests(addrStr string) error { - mapTestConn, err := connect(addrStr) - if err != nil { - log.Warnf("Error creating STUN connection: %s", err) - return err - } - - // Test I: Regular binding request - log.Info("Filtering Test I: Regular binding request") - request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) - - resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) - if err != nil || errors.Is(err, errTimedOut) { - return err - } - resps := parse(resp) - if resps.xorAddr == nil || resps.otherAddr == nil { - log.Warn("Error: NAT discovery feature not supported by this server") - return errNoOtherAddress - } - addr, err := net.ResolveUDPAddr("udp4", resps.otherAddr.String()) - if err != nil { - log.Infof("Failed resolving OTHER-ADDRESS: %v", resps.otherAddr) - return err - } - mapTestConn.OtherAddr = addr - - // Test II: Request to change both IP and port - log.Info("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) // just to print out the resp - NatFilteringBehavior = "endpoint independent" // my changes - log.Warn("=> NAT filtering behavior: endpoint independent") - return nil - } else if !errors.Is(err, errTimedOut) { - return err // something else went wrong - } - - // Test III: Request to change port only - log.Info("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) // just to print out the resp - NatFilteringBehavior = "address dependent" // my changes - log.Warn("=> NAT filtering behavior: address dependent") - } else if errors.Is(err, errTimedOut) { - NatFilteringBehavior = "address and port dependent" // my changes - log.Warn("=> NAT filtering behavior: address and port dependent") - } - - return mapTestConn.Close() -} - -// 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 - } - log.Debugf("%v", msg) - log.Debugf("\tMAPPED-ADDRESS: %v", ret.mappedAddr) - log.Debugf("\tXOR-MAPPED-ADDRESS: %v", ret.xorAddr) - log.Debugf("\tRESPONSE-ORIGIN: %v", ret.respOrigin) - log.Debugf("\tOTHER-ADDRESS: %v", ret.otherAddr) - log.Debugf("\tSOFTWARE: %v", ret.software) - for _, attr := range msg.Attributes { - switch attr.Type { - case - stun.AttrXORMappedAddress, - stun.AttrOtherAddress, - stun.AttrResponseOrigin, - stun.AttrMappedAddress, - stun.AttrSoftware: - break //nolint:staticcheck - default: - log.Debugf("\t%v (l=%v)", attr, attr.Length) - } - } - return ret -} - -// Given an address string, returns a StunServerConn -func connect(addrStr string) (*stunServerConn, error) { - log.Infof("Connecting to STUN server: %s", addrStr) - addr, err := net.ResolveUDPAddr("udp4", addrStr) - if err != nil { - log.Warnf("Error resolving address: %s", err) - return nil, err - } - - c, err := net.ListenUDP("udp4", nil) - if err != nil { - return nil, err - } - log.Infof("Local address: %s", c.LocalAddr()) - log.Infof("Remote address: %s", addr.String()) - - mChan := listen(c) - - return &stunServerConn{ - conn: c, - LocalAddr: c.LocalAddr(), - RemoteAddr: addr, - messageChan: mChan, - }, nil -} - -// Send request and wait for response or timeout -func (c *stunServerConn) roundTrip(msg *stun.Message, addr net.Addr) (*stun.Message, error) { - _ = msg.NewTransactionID() - log.Infof("Sending to %v: (%v bytes)", addr, msg.Length+messageHeaderSize) - log.Debugf("%v", msg) - for _, attr := range msg.Attributes { - log.Debugf("\t%v (l=%v)", attr, attr.Length) - } - _, err := c.conn.WriteTo(msg.Raw, addr) - if err != nil { - log.Warnf("Error sending request to %v", addr) - return nil, err - } - - // Wait for response or timeout - select { - case m, ok := <-c.messageChan: - if !ok { - return nil, errResponseMessage - } - return m, nil - case <-time.After(time.Duration(*timeoutPtr) * time.Second): - log.Infof("Timed out waiting for response from server %v", addr) - return nil, errTimedOut - } -} - -// taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go -func listen(conn *net.UDPConn) (messages chan *stun.Message) { - messages = make(chan *stun.Message) - go func() { - for { - buf := make([]byte, 1024) - - n, addr, err := conn.ReadFromUDP(buf) - if err != nil { - close(messages) - return - } - log.Infof("Response from %v: (%v bytes)", addr, n) - buf = buf[:n] - - m := new(stun.Message) - m.Raw = buf - err = m.Decode() - if err != nil { - log.Infof("Error decoding message: %v", err) - close(messages) - return - } - - messages <- m - } - }() - return + res := stuncheck.CheckType() + fmt.Printf("NAT Type: %s", res) } diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..72b5d22 --- /dev/null +++ b/model/model.go @@ -0,0 +1,14 @@ +package model + +import "github.com/pion/logging" + +const GoStunVersion = "v0.0.2" + +var ( + AddrStr = "stun.voipgate.com:3478" + Timeout = 3 + Verbose = 0 + Log logging.LeveledLogger + NatMappingBehavior string + NatFilteringBehavior string +) diff --git a/stuncheck/checktype.go b/stuncheck/checktype.go new file mode 100644 index 0000000..dd0a43a --- /dev/null +++ b/stuncheck/checktype.go @@ -0,0 +1,31 @@ +package stuncheck + +import ( + "fmt" + + "github.com/oneclickvirt/gostun/model" +) + +// CheckType +// Summarize the NAT type +func CheckType() string { + var result string + if model.NatMappingBehavior != "" && model.NatFilteringBehavior != "" { + if model.NatMappingBehavior == "inconclusive" || model.NatFilteringBehavior == "inconclusive" { + result = "Inconclusive" + } else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "endpoint independent" { + result = "Full Cone" + } else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "address dependent" { + result = "Restricted Cone" + } else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "address and port dependent" { + result = "Port Restricted Cone" + } else if model.NatMappingBehavior == "address and port dependent" && model.NatFilteringBehavior == "address and port dependent" { + result = "Symmetric" + } else { + result = fmt.Sprintf("%v[NatMappingBehavior] %v[NatFilteringBehavior]\n", model.NatMappingBehavior, model.NatFilteringBehavior) + } + } else { + result = "Inconclusive" + } + return result +} diff --git a/stuncheck/stuncheck.go b/stuncheck/stuncheck.go new file mode 100644 index 0000000..c82f43c --- /dev/null +++ b/stuncheck/stuncheck.go @@ -0,0 +1,305 @@ +package stuncheck + +import ( + "errors" + "net" + "time" + + "github.com/oneclickvirt/gostun/model" + "github.com/pion/stun/v2" +) + +// From https://github.com/pion/stun/blob/master/cmd/stun-nat-behaviour/main.go + +type stunServerConn struct { + conn net.PacketConn + LocalAddr net.Addr + RemoteAddr *net.UDPAddr + OtherAddr *net.UDPAddr + messageChan chan *stun.Message +} + +func (c *stunServerConn) Close() error { + return c.conn.Close() +} + +const ( + messageHeaderSize = 20 +) + +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") +) + +// RFC5780: 4.3. Determining NAT Mapping Behavior +func MappingTests(addrStr string) error { + mapTestConn, err := connect(addrStr) + if err != nil { + model.Log.Warnf("Error creating STUN connection: %s", err) + return err + } + + // Test I: Regular binding request + model.Log.Info("Mapping Test I: Regular binding request") + request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + + resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) + if err != nil { + return err + } + + // Parse response message for XOR-MAPPED-ADDRESS and make sure OTHER-ADDRESS valid + resps1 := parse(resp) + if resps1.xorAddr == nil || resps1.otherAddr == nil { + model.Log.Info("Error: NAT discovery feature not supported by this server") + return errNoOtherAddress + } + addr, err := net.ResolveUDPAddr("udp4", resps1.otherAddr.String()) + if err != nil { + model.Log.Infof("Failed resolving OTHER-ADDRESS: %v", resps1.otherAddr) + return err + } + mapTestConn.OtherAddr = addr + model.Log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps1.xorAddr) + + // Assert mapping behavior + if resps1.xorAddr.String() == mapTestConn.LocalAddr.String() { + model.NatMappingBehavior = "endpoint independent (no NAT)" // my changes + model.Log.Warn("=> NAT mapping behavior: endpoint independent (no NAT)") + return nil + } + + // Test II: Send binding request to the other address but primary port + model.Log.Info("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 { + return err + } + + // Assert mapping behavior + resps2 := parse(resp) + model.Log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps2.xorAddr) + if resps2.xorAddr.String() == resps1.xorAddr.String() { + model.NatMappingBehavior = "endpoint independent" // my changes + model.Log.Warn("=> NAT mapping behavior: endpoint independent") + return nil + } + + // Test III: Send binding request to the other address and port + model.Log.Info("Mapping Test III: Send binding request to the other address and port") + resp, err = mapTestConn.roundTrip(request, mapTestConn.OtherAddr) + if err != nil { + return err + } + + // Assert mapping behavior + resps3 := parse(resp) + model.Log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps3.xorAddr) + if resps3.xorAddr.String() == resps2.xorAddr.String() { + model.NatMappingBehavior = "address dependent" // my changes + model.Log.Warn("=> NAT mapping behavior: address dependent") + } else { + model.NatMappingBehavior = "address and port dependent" // my changes + model.Log.Warn("=> NAT mapping behavior: address and port dependent") + } + return mapTestConn.Close() +} + +// RFC5780: 4.4. Determining NAT Filtering Behavior +func FilteringTests(addrStr string) error { + mapTestConn, err := connect(addrStr) + if err != nil { + model.Log.Warnf("Error creating STUN connection: %s", err) + return err + } + + // Test I: Regular binding request + model.Log.Info("Filtering Test I: Regular binding request") + request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + + resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) + if err != nil || errors.Is(err, errTimedOut) { + return err + } + resps := parse(resp) + if resps.xorAddr == nil || resps.otherAddr == nil { + model.Log.Warn("Error: NAT discovery feature not supported by this server") + return errNoOtherAddress + } + addr, err := net.ResolveUDPAddr("udp4", resps.otherAddr.String()) + if err != nil { + model.Log.Infof("Failed resolving OTHER-ADDRESS: %v", resps.otherAddr) + return err + } + mapTestConn.OtherAddr = addr + + // Test II: Request to change both IP and port + model.Log.Info("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) // just to print out the resp + model.NatFilteringBehavior = "endpoint independent" // my changes + model.Log.Warn("=> NAT filtering behavior: endpoint independent") + return nil + } else if !errors.Is(err, errTimedOut) { + return err // something else went wrong + } + + // Test III: Request to change port only + model.Log.Info("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) // just to print out the resp + model.NatFilteringBehavior = "address dependent" // my changes + model.Log.Warn("=> NAT filtering behavior: address dependent") + } else if errors.Is(err, errTimedOut) { + model.NatFilteringBehavior = "address and port dependent" // my changes + model.Log.Warn("=> NAT filtering behavior: address and port dependent") + } + + return mapTestConn.Close() +} + +// 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 + } + model.Log.Debugf("%v", msg) + model.Log.Debugf("\tMAPPED-ADDRESS: %v", ret.mappedAddr) + model.Log.Debugf("\tXOR-MAPPED-ADDRESS: %v", ret.xorAddr) + model.Log.Debugf("\tRESPONSE-ORIGIN: %v", ret.respOrigin) + model.Log.Debugf("\tOTHER-ADDRESS: %v", ret.otherAddr) + model.Log.Debugf("\tSOFTWARE: %v", ret.software) + for _, attr := range msg.Attributes { + switch attr.Type { + case + stun.AttrXORMappedAddress, + stun.AttrOtherAddress, + stun.AttrResponseOrigin, + stun.AttrMappedAddress, + stun.AttrSoftware: + break //nolint:staticcheck + default: + model.Log.Debugf("\t%v (l=%v)", attr, attr.Length) + } + } + return ret +} + +// Given an address string, returns a StunServerConn +func connect(addrStr string) (*stunServerConn, error) { + model.Log.Infof("Connecting to STUN server: %s", addrStr) + addr, err := net.ResolveUDPAddr("udp4", addrStr) + if err != nil { + model.Log.Warnf("Error resolving address: %s", err) + return nil, err + } + + c, err := net.ListenUDP("udp4", nil) + if err != nil { + return nil, err + } + model.Log.Infof("Local address: %s", c.LocalAddr()) + model.Log.Infof("Remote address: %s", addr.String()) + + mChan := listen(c) + + return &stunServerConn{ + conn: c, + LocalAddr: c.LocalAddr(), + RemoteAddr: addr, + messageChan: mChan, + }, nil +} + +// Send request and wait for response or timeout +func (c *stunServerConn) roundTrip(msg *stun.Message, addr net.Addr) (*stun.Message, error) { + _ = msg.NewTransactionID() + model.Log.Infof("Sending to %v: (%v bytes)", addr, msg.Length+messageHeaderSize) + model.Log.Debugf("%v", msg) + for _, attr := range msg.Attributes { + model.Log.Debugf("\t%v (l=%v)", attr, attr.Length) + } + _, err := c.conn.WriteTo(msg.Raw, addr) + if err != nil { + model.Log.Warnf("Error sending request to %v", addr) + return nil, err + } + + // Wait for response or timeout + select { + case m, ok := <-c.messageChan: + if !ok { + return nil, errResponseMessage + } + return m, nil + case <-time.After(time.Duration(model.Timeout) * time.Second): + model.Log.Infof("Timed out waiting for response from server %v", addr) + return nil, errTimedOut + } +} + +// taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go +func listen(conn *net.UDPConn) (messages chan *stun.Message) { + messages = make(chan *stun.Message) + go func() { + for { + buf := make([]byte, 1024) + + n, addr, err := conn.ReadFromUDP(buf) + if err != nil { + close(messages) + return + } + model.Log.Infof("Response from %v: (%v bytes)", addr, n) + buf = buf[:n] + + m := new(stun.Message) + m.Raw = buf + err = m.Decode() + if err != nil { + model.Log.Infof("Error decoding message: %v", err) + close(messages) + return + } + + messages <- m + } + }() + return +}