mirror of
https://github.com/oneclickvirt/gostun.git
synced 2025-09-28 20:22:21 +08:00
fix: 适配支持RFC 3489/5389/5780检测
This commit is contained in:
@@ -35,10 +35,6 @@ address and port dependent
|
|||||||
| Symmetric | ```address and port dependent``` | ```address and port dependent``` |
|
| Symmetric | ```address and port dependent``` | ```address and port dependent``` |
|
||||||
| Inconclusive | ```inconclusive``` | ```inconclusive``` |
|
| Inconclusive | ```inconclusive``` | ```inconclusive``` |
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- [ ] 加入UDP检测
|
|
||||||
|
|
||||||
## 使用说明[Usage]
|
## 使用说明[Usage]
|
||||||
|
|
||||||
下载、安装、更新
|
下载、安装、更新
|
||||||
@@ -71,6 +67,8 @@ Usage: gostun [options]
|
|||||||
Specify STUN server address (default "stun.voipgate.com:3478")
|
Specify STUN server address (default "stun.voipgate.com:3478")
|
||||||
-timeout int
|
-timeout int
|
||||||
Set timeout in seconds for STUN server response (default 3)
|
Set timeout in seconds for STUN server response (default 3)
|
||||||
|
-type string
|
||||||
|
Specify ip test version: ipv4, ipv6 or both (default "ipv4")
|
||||||
-v Display version information
|
-v Display version information
|
||||||
-verbose int
|
-verbose int
|
||||||
Set verbosity level
|
Set verbosity level
|
||||||
@@ -94,6 +92,8 @@ go get github.com/oneclickvirt/gostun@v0.0.3-20250629044639
|
|||||||
|
|
||||||
## 感谢[Thanks]
|
## 感谢[Thanks]
|
||||||
|
|
||||||
|
https://www.rfc-editor.org/info/rfc5389
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc3489#section-5
|
https://datatracker.ietf.org/doc/html/rfc3489#section-5
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc4787#section-5
|
https://datatracker.ietf.org/doc/html/rfc4787#section-5
|
||||||
|
101
cmd/main.go
101
cmd/main.go
@@ -12,6 +12,67 @@ import (
|
|||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// tryRFCMethod attempts NAT detection using specified RFC method
|
||||||
|
func tryRFCMethod(addrStr string, rfcMethod string) (bool, error) {
|
||||||
|
currentProtocol := "ipv4"
|
||||||
|
if model.IPVersion == "ipv6" || (model.IPVersion == "both" && strings.Contains(addrStr, "[") && strings.Contains(addrStr, "]")) {
|
||||||
|
currentProtocol = "ipv6"
|
||||||
|
}
|
||||||
|
var err1, err2 error
|
||||||
|
switch rfcMethod {
|
||||||
|
case "RFC5780":
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] Trying RFC 5780 method with server %s", currentProtocol, addrStr)
|
||||||
|
}
|
||||||
|
err1 = stuncheck.MappingTests(addrStr)
|
||||||
|
if err1 != nil {
|
||||||
|
model.NatMappingBehavior = "inconclusive"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5780 NAT mapping behavior: inconclusive", currentProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err2 = stuncheck.FilteringTests(addrStr)
|
||||||
|
if err2 != nil {
|
||||||
|
model.NatFilteringBehavior = "inconclusive"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5780 NAT filtering behavior: inconclusive", currentProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "RFC5389":
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] Trying RFC 5389/8489 method with server %s", currentProtocol, addrStr)
|
||||||
|
}
|
||||||
|
err1 = stuncheck.MappingTestsRFC5389(addrStr)
|
||||||
|
if err1 != nil {
|
||||||
|
model.NatMappingBehavior = "inconclusive"
|
||||||
|
model.NatFilteringBehavior = "inconclusive"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5389 NAT detection: inconclusive", currentProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "RFC3489":
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] Trying RFC 3489 method with server %s", currentProtocol, addrStr)
|
||||||
|
}
|
||||||
|
err1 = stuncheck.MappingTestsRFC3489(addrStr)
|
||||||
|
if err1 != nil {
|
||||||
|
model.NatMappingBehavior = "inconclusive"
|
||||||
|
model.NatFilteringBehavior = "inconclusive"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489 NAT detection: inconclusive", currentProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if model.NatMappingBehavior != "inconclusive" && model.NatFilteringBehavior != "inconclusive" &&
|
||||||
|
model.NatMappingBehavior != "" && model.NatFilteringBehavior != "" {
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] Successfully determined NAT type using %s with server %s", currentProtocol, rfcMethod, addrStr)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var showVersion, help bool
|
var showVersion, help bool
|
||||||
gostunFlag := flag.NewFlagSet("gostun", flag.ContinueOnError)
|
gostunFlag := flag.NewFlagSet("gostun", flag.ContinueOnError)
|
||||||
@@ -57,7 +118,13 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
addrStrList = model.GetDefaultServers(model.IPVersion)
|
addrStrList = model.GetDefaultServers(model.IPVersion)
|
||||||
}
|
}
|
||||||
// checkStatus := false
|
// RFC methods in order of preference: 5780 -> 5389 -> 3489
|
||||||
|
rfcMethods := []string{"RFC5780", "RFC5389", "RFC3489"}
|
||||||
|
successfulDetection := false
|
||||||
|
for _, rfcMethod := range rfcMethods {
|
||||||
|
if successfulDetection {
|
||||||
|
break
|
||||||
|
}
|
||||||
for _, addrStr := range addrStrList {
|
for _, addrStr := range addrStrList {
|
||||||
model.NatMappingBehavior = ""
|
model.NatMappingBehavior = ""
|
||||||
model.NatFilteringBehavior = ""
|
model.NatFilteringBehavior = ""
|
||||||
@@ -75,32 +142,22 @@ func main() {
|
|||||||
currentProtocol = originalIPVersion
|
currentProtocol = originalIPVersion
|
||||||
}
|
}
|
||||||
if model.EnableLoger {
|
if model.EnableLoger {
|
||||||
model.Log.Infof("Testing server %s with protocol %s", addrStr, currentProtocol)
|
model.Log.Infof("Testing server %s with protocol %s using %s", addrStr, currentProtocol, rfcMethod)
|
||||||
}
|
}
|
||||||
err1 := stuncheck.MappingTests(addrStr)
|
success, err := tryRFCMethod(addrStr, rfcMethod)
|
||||||
if err1 != nil {
|
if err != nil && model.EnableLoger {
|
||||||
model.NatMappingBehavior = "inconclusive"
|
model.Log.Warnf("[%s] Error with %s method: %v", currentProtocol, rfcMethod, err)
|
||||||
if model.EnableLoger {
|
|
||||||
model.Log.Warnf("[%s] NAT mapping behavior: inconclusive", currentProtocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err2 := stuncheck.FilteringTests(addrStr)
|
|
||||||
if err2 != nil {
|
|
||||||
model.NatFilteringBehavior = "inconclusive"
|
|
||||||
if model.EnableLoger {
|
|
||||||
model.Log.Warnf("[%s] NAT filtering behavior: inconclusive", currentProtocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if model.NatMappingBehavior != "inconclusive" && model.NatFilteringBehavior != "inconclusive" &&
|
|
||||||
model.NatMappingBehavior != "" && model.NatFilteringBehavior != "" {
|
|
||||||
// checkStatus = true
|
|
||||||
if model.EnableLoger {
|
|
||||||
model.Log.Infof("[%s] Successfully determined NAT type with server %s", currentProtocol, addrStr)
|
|
||||||
}
|
}
|
||||||
|
if success {
|
||||||
|
successfulDetection = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if model.EnableLoger {
|
if model.EnableLoger {
|
||||||
model.Log.Warnf("[%s] Server %s failed to determine NAT type, trying next server", currentProtocol, addrStr)
|
model.Log.Warnf("[%s] Server %s failed to determine NAT type using %s, trying next server", currentProtocol, addrStr, rfcMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !successfulDetection && model.EnableLoger {
|
||||||
|
model.Log.Warnf("All servers failed with %s method, trying next RFC method", rfcMethod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.IPVersion = originalIPVersion
|
model.IPVersion = originalIPVersion
|
||||||
|
@@ -2,7 +2,7 @@ package model
|
|||||||
|
|
||||||
import "github.com/pion/logging"
|
import "github.com/pion/logging"
|
||||||
|
|
||||||
const GoStunVersion = "v0.0.4"
|
const GoStunVersion = "v0.0.5"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AddrStr = "stun.voipgate.com:3478"
|
AddrStr = "stun.voipgate.com:3478"
|
||||||
|
@@ -13,6 +13,8 @@ func CheckType() string {
|
|||||||
if model.NatMappingBehavior != "" && model.NatFilteringBehavior != "" {
|
if model.NatMappingBehavior != "" && model.NatFilteringBehavior != "" {
|
||||||
if model.NatMappingBehavior == "inconclusive" || model.NatFilteringBehavior == "inconclusive" {
|
if model.NatMappingBehavior == "inconclusive" || model.NatFilteringBehavior == "inconclusive" {
|
||||||
result = "Inconclusive"
|
result = "Inconclusive"
|
||||||
|
} else if model.NatMappingBehavior == "endpoint independent (no NAT)" && model.NatFilteringBehavior == "endpoint independent" {
|
||||||
|
result = "Full Cone"
|
||||||
} else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "endpoint independent" {
|
} else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "endpoint independent" {
|
||||||
result = "Full Cone"
|
result = "Full Cone"
|
||||||
} else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "address dependent" {
|
} else if model.NatMappingBehavior == "endpoint independent" && model.NatFilteringBehavior == "address dependent" {
|
||||||
|
@@ -68,6 +68,7 @@ func getCurrentProtocol(addrStr string) string {
|
|||||||
return "ipv4"
|
return "ipv4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 5780 implementation (current)
|
||||||
func MappingTests(addrStr string) error { //nolint:cyclop
|
func MappingTests(addrStr string) error { //nolint:cyclop
|
||||||
currentProtocol := getCurrentProtocol(addrStr)
|
currentProtocol := getCurrentProtocol(addrStr)
|
||||||
mapTestConn, err := connect(addrStr)
|
mapTestConn, err := connect(addrStr)
|
||||||
@@ -156,6 +157,7 @@ func MappingTests(addrStr string) error { //nolint:cyclop
|
|||||||
return mapTestConn.Close()
|
return mapTestConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 5780 implementation (current)
|
||||||
func FilteringTests(addrStr string) error { //nolint:cyclop
|
func FilteringTests(addrStr string) error { //nolint:cyclop
|
||||||
currentProtocol := getCurrentProtocol(addrStr)
|
currentProtocol := getCurrentProtocol(addrStr)
|
||||||
mapTestConn, err := connect(addrStr)
|
mapTestConn, err := connect(addrStr)
|
||||||
@@ -226,6 +228,141 @@ func FilteringTests(addrStr string) error { //nolint:cyclop
|
|||||||
return mapTestConn.Close()
|
return mapTestConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 5389/8489 implementation - basic STUN binding request
|
||||||
|
func MappingTestsRFC5389(addrStr string) error {
|
||||||
|
currentProtocol := getCurrentProtocol(addrStr)
|
||||||
|
mapTestConn, err := connect(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5389: Error creating STUN connection: %s", currentProtocol, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer mapTestConn.Close()
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC5389: Basic binding request", currentProtocol)
|
||||||
|
}
|
||||||
|
request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||||
|
resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resps := parse(resp)
|
||||||
|
if resps.xorAddr == nil {
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5389: No XOR-MAPPED-ADDRESS received", currentProtocol)
|
||||||
|
}
|
||||||
|
return errors.New("no mapped address")
|
||||||
|
}
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC5389: Received XOR-MAPPED-ADDRESS: %v", currentProtocol, resps.xorAddr)
|
||||||
|
}
|
||||||
|
// Simple classification based on whether we're behind NAT
|
||||||
|
if resps.xorAddr.String() == mapTestConn.LocalAddr.String() {
|
||||||
|
model.NatMappingBehavior = "endpoint independent (no NAT)"
|
||||||
|
model.NatFilteringBehavior = "endpoint independent"
|
||||||
|
} else {
|
||||||
|
// Can't determine exact type with RFC5389, so use conservative estimate
|
||||||
|
model.NatMappingBehavior = "address and port dependent"
|
||||||
|
model.NatFilteringBehavior = "address and port dependent"
|
||||||
|
}
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC5389: NAT mapping behavior: %s", currentProtocol, model.NatMappingBehavior)
|
||||||
|
model.Log.Warnf("[%s] RFC5389: NAT filtering behavior: %s", currentProtocol, model.NatFilteringBehavior)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 3489 implementation - classic STUN
|
||||||
|
func MappingTestsRFC3489(addrStr string) error {
|
||||||
|
currentProtocol := getCurrentProtocol(addrStr)
|
||||||
|
mapTestConn, err := connect(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: Error creating STUN connection: %s", currentProtocol, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer mapTestConn.Close()
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC3489: Test I - Basic binding request", currentProtocol)
|
||||||
|
}
|
||||||
|
// Test I: Basic binding request
|
||||||
|
request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||||
|
resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resps1 := parse(resp)
|
||||||
|
var mappedAddr *net.UDPAddr
|
||||||
|
// Try XOR-MAPPED-ADDRESS first, then MAPPED-ADDRESS
|
||||||
|
if resps1.xorAddr != nil {
|
||||||
|
mappedAddr, _ = net.ResolveUDPAddr("udp", resps1.xorAddr.String())
|
||||||
|
} else if resps1.mappedAddr != nil {
|
||||||
|
mappedAddr, _ = net.ResolveUDPAddr("udp", resps1.mappedAddr.String())
|
||||||
|
}
|
||||||
|
if mappedAddr == nil {
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: No mapped address received", currentProtocol)
|
||||||
|
}
|
||||||
|
return errors.New("no mapped address")
|
||||||
|
}
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC3489: Received mapped address: %v", currentProtocol, mappedAddr)
|
||||||
|
}
|
||||||
|
// Check if we're behind NAT
|
||||||
|
localUDP, _ := mapTestConn.LocalAddr.(*net.UDPAddr)
|
||||||
|
if mappedAddr.IP.Equal(localUDP.IP) && mappedAddr.Port == localUDP.Port {
|
||||||
|
// No NAT
|
||||||
|
model.NatMappingBehavior = "endpoint independent (no NAT)"
|
||||||
|
model.NatFilteringBehavior = "endpoint independent"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: No NAT detected", currentProtocol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Test II: Binding request with change IP and Port
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC3489: Test II - Request with change IP and Port", currentProtocol)
|
||||||
|
}
|
||||||
|
request2 := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||||
|
request2.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06}) // Change both IP and port
|
||||||
|
resp2, err2 := mapTestConn.roundTrip(request2, mapTestConn.RemoteAddr)
|
||||||
|
if err2 == nil && resp2 != nil {
|
||||||
|
// Full cone NAT
|
||||||
|
model.NatMappingBehavior = "endpoint independent"
|
||||||
|
model.NatFilteringBehavior = "endpoint independent"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: Full Cone NAT detected", currentProtocol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Test III: Binding request with change port only
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Infof("[%s] RFC3489: Test III - Request with change Port only", currentProtocol)
|
||||||
|
}
|
||||||
|
request3 := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||||
|
request3.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02}) // Change port only
|
||||||
|
resp3, err3 := mapTestConn.roundTrip(request3, mapTestConn.RemoteAddr)
|
||||||
|
if err3 == nil && resp3 != nil {
|
||||||
|
// Restricted cone NAT
|
||||||
|
model.NatMappingBehavior = "endpoint independent"
|
||||||
|
model.NatFilteringBehavior = "address dependent"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: Restricted Cone NAT detected", currentProtocol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// If we get here, we need to do additional tests for symmetric vs port restricted
|
||||||
|
// For simplicity in RFC3489, we'll classify remaining as Port Restricted or Symmetric
|
||||||
|
model.NatMappingBehavior = "address and port dependent"
|
||||||
|
model.NatFilteringBehavior = "address and port dependent"
|
||||||
|
if model.EnableLoger {
|
||||||
|
model.Log.Warnf("[%s] RFC3489: Symmetric NAT detected", currentProtocol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func parse(msg *stun.Message) (ret struct {
|
func parse(msg *stun.Message) (ret struct {
|
||||||
xorAddr *stun.XORMappedAddress
|
xorAddr *stun.XORMappedAddress
|
||||||
otherAddr *stun.OtherAddress
|
otherAddr *stun.OtherAddress
|
||||||
|
Reference in New Issue
Block a user