mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
tools: Network Quality & STUN
This commit is contained in:
142
common/networkquality/http.go
Normal file
142
common/networkquality/http.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
sBufio "github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func FormatBitrate(bps int64) string {
|
||||
switch {
|
||||
case bps >= 1_000_000_000:
|
||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
||||
case bps >= 1_000_000:
|
||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
||||
case bps >= 1_000:
|
||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
||||
default:
|
||||
return fmt.Sprintf("%d bps", bps)
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPClient(dialer N.Dialer) *http.Client {
|
||||
transport := &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
}
|
||||
if dialer != nil {
|
||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
|
||||
if client == nil {
|
||||
return nil, E.New("http client is nil")
|
||||
}
|
||||
if client.Transport == nil {
|
||||
return http.DefaultTransport.(*http.Transport).Clone(), nil
|
||||
}
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, E.New("http client transport must be *http.Transport")
|
||||
}
|
||||
return transport.Clone(), nil
|
||||
}
|
||||
|
||||
func newMeasurementClient(
|
||||
baseClient *http.Client,
|
||||
connectEndpoint string,
|
||||
singleConnection bool,
|
||||
disableKeepAlives bool,
|
||||
readCounters []N.CountFunc,
|
||||
writeCounters []N.CountFunc,
|
||||
) (*http.Client, error) {
|
||||
transport, err := baseTransportFromClient(baseClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.DisableCompression = true
|
||||
transport.DisableKeepAlives = disableKeepAlives
|
||||
if singleConnection {
|
||||
transport.MaxConnsPerHost = 1
|
||||
transport.MaxIdleConnsPerHost = 1
|
||||
transport.MaxIdleConns = 1
|
||||
}
|
||||
|
||||
baseDialContext := transport.DialContext
|
||||
if baseDialContext == nil {
|
||||
dialer := &net.Dialer{}
|
||||
baseDialContext = dialer.DialContext
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
||||
}
|
||||
conn, dialErr := baseDialContext(ctx, network, dialAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
||||
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: baseClient.CheckRedirect,
|
||||
Jar: baseClient.Jar,
|
||||
Timeout: baseClient.Timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MeasurementClientFactory func(
|
||||
connectEndpoint string,
|
||||
singleConnection bool,
|
||||
disableKeepAlives bool,
|
||||
readCounters []N.CountFunc,
|
||||
writeCounters []N.CountFunc,
|
||||
) (*http.Client, error)
|
||||
|
||||
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
|
||||
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
|
||||
}
|
||||
}
|
||||
|
||||
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
|
||||
if !useHTTP3 {
|
||||
return nil, nil
|
||||
}
|
||||
return NewHTTP3MeasurementClientFactory(dialer)
|
||||
}
|
||||
|
||||
func rewriteDialAddress(addr string, connectEndpoint string) string {
|
||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
|
||||
if err == nil {
|
||||
host = endpointHost
|
||||
if endpointPort != "" {
|
||||
port = endpointPort
|
||||
}
|
||||
} else if connectEndpoint != "" {
|
||||
host = connectEndpoint
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
55
common/networkquality/http3.go
Normal file
55
common/networkquality/http3.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
sBufio "github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
// singleConnection and disableKeepAlives are not applied:
|
||||
// HTTP/3 multiplexes streams over a single QUIC connection by default.
|
||||
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
transport := &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
||||
}
|
||||
destination := M.ParseSocksaddr(dialAddr)
|
||||
var udpConn net.Conn
|
||||
var dialErr error
|
||||
if dialer != nil {
|
||||
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
} else {
|
||||
var netDialer net.Dialer
|
||||
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
|
||||
}
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
var wrappedConn net.Conn = udpConn
|
||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
||||
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
|
||||
}
|
||||
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
|
||||
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||
if dialErr != nil {
|
||||
udpConn.Close()
|
||||
return nil, dialErr
|
||||
}
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}, nil
|
||||
}
|
||||
12
common/networkquality/http3_stub.go
Normal file
12
common/networkquality/http3_stub.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
return nil, C.ErrQUICNotIncluded
|
||||
}
|
||||
1413
common/networkquality/networkquality.go
Normal file
1413
common/networkquality/networkquality.go
Normal file
File diff suppressed because it is too large
Load Diff
607
common/stun/stun.go
Normal file
607
common/stun/stun.go
Normal file
@@ -0,0 +1,607 @@
|
||||
package stun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultServer = "stun.voipgate.com:3478"
|
||||
|
||||
magicCookie = 0x2112A442
|
||||
headerSize = 20
|
||||
|
||||
bindingRequest = 0x0001
|
||||
bindingSuccessResponse = 0x0101
|
||||
bindingErrorResponse = 0x0111
|
||||
|
||||
attrMappedAddress = 0x0001
|
||||
attrChangeRequest = 0x0003
|
||||
attrErrorCode = 0x0009
|
||||
attrXORMappedAddress = 0x0020
|
||||
attrOtherAddress = 0x802c
|
||||
|
||||
familyIPv4 = 0x01
|
||||
familyIPv6 = 0x02
|
||||
|
||||
changeIP = 0x04
|
||||
changePort = 0x02
|
||||
|
||||
defaultRTO = 500 * time.Millisecond
|
||||
minRTO = 250 * time.Millisecond
|
||||
maxRetransmit = 2
|
||||
)
|
||||
|
||||
type Phase int32
|
||||
|
||||
const (
|
||||
PhaseBinding Phase = iota
|
||||
PhaseNATMapping
|
||||
PhaseNATFiltering
|
||||
PhaseDone
|
||||
)
|
||||
|
||||
type NATMapping int32
|
||||
|
||||
const (
|
||||
NATMappingUnknown NATMapping = iota
|
||||
_ // reserved
|
||||
NATMappingEndpointIndependent
|
||||
NATMappingAddressDependent
|
||||
NATMappingAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (m NATMapping) String() string {
|
||||
switch m {
|
||||
case NATMappingEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATMappingAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATMappingAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type NATFiltering int32
|
||||
|
||||
const (
|
||||
NATFilteringUnknown NATFiltering = iota
|
||||
NATFilteringEndpointIndependent
|
||||
NATFilteringAddressDependent
|
||||
NATFilteringAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (f NATFiltering) String() string {
|
||||
switch f {
|
||||
case NATFilteringEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATFilteringAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATFilteringAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionID [12]byte
|
||||
|
||||
type Options struct {
|
||||
Server string
|
||||
Dialer N.Dialer
|
||||
Context context.Context
|
||||
OnProgress func(Progress)
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Phase Phase
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
NATTypeSupported bool
|
||||
}
|
||||
|
||||
type parsedResponse struct {
|
||||
xorMappedAddr netip.AddrPort
|
||||
mappedAddr netip.AddrPort
|
||||
otherAddr netip.AddrPort
|
||||
}
|
||||
|
||||
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
||||
if r.xorMappedAddr.IsValid() {
|
||||
return r.xorMappedAddr, true
|
||||
}
|
||||
if r.mappedAddr.IsValid() {
|
||||
return r.mappedAddr, true
|
||||
}
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
type stunAttribute struct {
|
||||
typ uint16
|
||||
value []byte
|
||||
}
|
||||
|
||||
func newTransactionID() TransactionID {
|
||||
var id TransactionID
|
||||
_, _ = rand.Read(id[:])
|
||||
return id
|
||||
}
|
||||
|
||||
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
||||
attrLen := 0
|
||||
for _, attr := range attrs {
|
||||
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
buf := make([]byte, headerSize+attrLen)
|
||||
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
||||
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
||||
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
||||
copy(buf[8:20], txID[:])
|
||||
|
||||
offset := headerSize
|
||||
for _, attr := range attrs {
|
||||
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
||||
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
||||
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
||||
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func changeRequestAttr(flags byte) stunAttribute {
|
||||
return stunAttribute{
|
||||
typ: attrChangeRequest,
|
||||
value: []byte{0, 0, 0, flags},
|
||||
}
|
||||
}
|
||||
|
||||
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
||||
if len(data) < headerSize {
|
||||
return nil, E.New("response too short")
|
||||
}
|
||||
|
||||
msgType := binary.BigEndian.Uint16(data[0:2])
|
||||
if msgType&0xC000 != 0 {
|
||||
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
||||
}
|
||||
|
||||
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||
if cookie != magicCookie {
|
||||
return nil, E.New("invalid magic cookie")
|
||||
}
|
||||
|
||||
var txID TransactionID
|
||||
copy(txID[:], data[8:20])
|
||||
if txID != expectedTxID {
|
||||
return nil, E.New("transaction ID mismatch")
|
||||
}
|
||||
|
||||
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
||||
if msgLen > len(data)-headerSize {
|
||||
return nil, E.New("message length exceeds data")
|
||||
}
|
||||
|
||||
attrData := data[headerSize : headerSize+msgLen]
|
||||
|
||||
if msgType == bindingErrorResponse {
|
||||
return nil, parseErrorResponse(attrData)
|
||||
}
|
||||
if msgType != bindingSuccessResponse {
|
||||
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
||||
}
|
||||
|
||||
resp := &parsedResponse{}
|
||||
offset := 0
|
||||
for offset+4 <= len(attrData) {
|
||||
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(attrData) {
|
||||
break
|
||||
}
|
||||
attrValue := attrData[offset+4 : offset+4+attrLen]
|
||||
|
||||
switch attrType {
|
||||
case attrXORMappedAddress:
|
||||
addr, err := parseXORMappedAddress(attrValue, txID)
|
||||
if err == nil {
|
||||
resp.xorMappedAddr = addr
|
||||
}
|
||||
case attrMappedAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.mappedAddr = addr
|
||||
}
|
||||
case attrOtherAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.otherAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseErrorResponse(data []byte) error {
|
||||
offset := 0
|
||||
for offset+4 <= len(data) {
|
||||
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(data) {
|
||||
break
|
||||
}
|
||||
if attrType == attrErrorCode && attrLen >= 4 {
|
||||
attrValue := data[offset+4 : offset+4+attrLen]
|
||||
class := int(attrValue[2] & 0x07)
|
||||
number := int(attrValue[3])
|
||||
code := class*100 + number
|
||||
if attrLen > 4 {
|
||||
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
||||
}
|
||||
return E.New("STUN error ", code)
|
||||
}
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
return E.New("STUN error response")
|
||||
}
|
||||
|
||||
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
xPort := binary.BigEndian.Uint16(data[2:4])
|
||||
port := xPort ^ uint16(magicCookie>>16)
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
var ip [4]byte
|
||||
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
||||
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
var xorKey [16]byte
|
||||
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
||||
copy(xorKey[4:16], txID[:])
|
||||
for i := range 16 {
|
||||
ip[i] = data[4+i] ^ xorKey[i]
|
||||
}
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
port := binary.BigEndian.Uint16(data[2:4])
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
return netip.AddrPortFrom(
|
||||
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
||||
), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
copy(ip[:], data[4:20])
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
||||
request := buildBindingRequest(txID, attrs...)
|
||||
currentRTO := rto
|
||||
retransmitCount := 0
|
||||
|
||||
sendTime := time.Now()
|
||||
_, err := conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "send STUN request")
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "set read deadline")
|
||||
}
|
||||
|
||||
n, _, readErr := conn.ReadFrom(buf)
|
||||
if readErr != nil {
|
||||
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
||||
retransmitCount++
|
||||
currentRTO *= 2
|
||||
sendTime = time.Now()
|
||||
_, err = conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "retransmit STUN request")
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, 0, E.Cause(readErr, "read STUN response")
|
||||
}
|
||||
|
||||
if n < headerSize || buf[0]&0xC0 != 0 ||
|
||||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
||||
continue
|
||||
}
|
||||
var receivedTxID TransactionID
|
||||
copy(receivedTxID[:], buf[8:20])
|
||||
if receivedTxID != txID {
|
||||
continue
|
||||
}
|
||||
|
||||
latency := time.Since(sendTime)
|
||||
|
||||
resp, parseErr := parseResponse(buf[:n], txID)
|
||||
if parseErr != nil {
|
||||
return nil, 0, parseErr
|
||||
}
|
||||
|
||||
return resp, latency, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Run(options Options) (*Result, error) {
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
server := options.Server
|
||||
if server == "" {
|
||||
server = DefaultServer
|
||||
}
|
||||
serverSocksaddr := M.ParseSocksaddr(server)
|
||||
if serverSocksaddr.Port == 0 {
|
||||
serverSocksaddr.Port = 3478
|
||||
}
|
||||
|
||||
reportProgress := options.OnProgress
|
||||
if reportProgress == nil {
|
||||
reportProgress = func(Progress) {}
|
||||
}
|
||||
|
||||
var (
|
||||
packetConn net.PacketConn
|
||||
serverAddr net.Addr
|
||||
err error
|
||||
)
|
||||
|
||||
if options.Dialer != nil {
|
||||
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverSocksaddr
|
||||
} else {
|
||||
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
||||
if resolveErr != nil {
|
||||
return nil, E.Cause(resolveErr, "resolve STUN server")
|
||||
}
|
||||
packetConn, err = net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverUDPAddr
|
||||
}
|
||||
defer func() {
|
||||
_ = packetConn.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
rto := defaultRTO
|
||||
|
||||
// Phase 1: Binding
|
||||
reportProgress(Progress{Phase: PhaseBinding})
|
||||
|
||||
txID := newTransactionID()
|
||||
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "binding request")
|
||||
}
|
||||
|
||||
rto = max(minRTO, 3*latency)
|
||||
|
||||
externalAddr, ok := resp.externalAddr()
|
||||
if !ok {
|
||||
return nil, E.New("no mapped address in response")
|
||||
}
|
||||
|
||||
result := &Result{
|
||||
ExternalAddr: externalAddr.String(),
|
||||
LatencyMs: int32(latency.Milliseconds()),
|
||||
}
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseBinding,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
otherAddr := resp.otherAddr
|
||||
if !otherAddr.IsValid() {
|
||||
result.NATTypeSupported = false
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
result.NATTypeSupported = true
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
result.NATMapping = detectNATMapping(
|
||||
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
||||
)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATFiltering,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
NATFiltering: result.NATFiltering,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func detectNATMapping(
|
||||
conn net.PacketConn,
|
||||
serverPort uint16,
|
||||
externalAddr netip.AddrPort,
|
||||
otherAddr netip.AddrPort,
|
||||
rto time.Duration,
|
||||
) NATMapping {
|
||||
// Mapping Test II: Send to other_ip:server_port
|
||||
testIIAddr := net.UDPAddrFromAddrPort(
|
||||
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
||||
)
|
||||
txID2 := newTransactionID()
|
||||
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr2, ok := resp2.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr == externalAddr2 {
|
||||
return NATMappingEndpointIndependent
|
||||
}
|
||||
|
||||
// Mapping Test III: Send to other_ip:other_port
|
||||
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
||||
txID3 := newTransactionID()
|
||||
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr3, ok := resp3.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr2 == externalAddr3 {
|
||||
return NATMappingAddressDependent
|
||||
}
|
||||
return NATMappingAddressAndPortDependent
|
||||
}
|
||||
|
||||
func detectNATFiltering(
|
||||
conn net.PacketConn,
|
||||
serverAddr net.Addr,
|
||||
rto time.Duration,
|
||||
) NATFiltering {
|
||||
// Filtering Test II: Request response from different IP and port
|
||||
txID := newTransactionID()
|
||||
_, _, err := roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringEndpointIndependent
|
||||
}
|
||||
|
||||
// Filtering Test III: Request response from different port only
|
||||
txID = newTransactionID()
|
||||
_, _, err = roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringAddressDependent
|
||||
}
|
||||
|
||||
return NATFilteringAddressAndPortDependent
|
||||
}
|
||||
|
||||
func paddingLen(n int) int {
|
||||
if n%4 == 0 {
|
||||
return 0
|
||||
}
|
||||
return 4 - n%4
|
||||
}
|
||||
Reference in New Issue
Block a user