Files
sing-box/common/stun/stun.go
2026-04-10 16:24:27 +08:00

608 lines
14 KiB
Go

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
}