Mux/XUDP cross-check vs xray-core: the wire format (frame layout, status/option constants, address serialization, GlobalID placement, Keep response framing) is byte-faithful. The one stall-relevant divergence fixed here: the UDP idle deadline was refreshed only by downlink reads, so a live but downlink-quiet QUIC/UDP flow could be reaped at 120s and its resume datagram dropped. Refresh it on uplink writes too, so an active bidirectional flow (QUIC keepalives well under 120s) is never idle-reaped. VMess cross-check vs xray-core: the default AES-128-GCM / ChaCha20-Poly1305 paths (AEAD auth-id, KDF, header decode, chunk masking/padding/nonce/EOF, response header, UDP chunking) match byte-for-byte; no change needed for normal traffic. Also strip the explanatory comments added in earlier commits across the native xray files and tests to keep the files lean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
972 lines
25 KiB
Go
972 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
const (
|
|
nativeMuxStatusNew = 0x01
|
|
nativeMuxStatusKeep = 0x02
|
|
nativeMuxStatusEnd = 0x03
|
|
nativeMuxStatusKeepAlive = 0x04
|
|
|
|
nativeMuxOptionData = 0x01
|
|
nativeMuxOptionError = 0x02
|
|
|
|
nativeMuxNetworkTCP = 0x01
|
|
nativeMuxNetworkUDP = 0x02
|
|
|
|
// Keep packet buffers below the kernel max. Mux packets are length-prefixed and
|
|
// capped at nativeUDPMaxPacket, so larger buffers only increase memory pressure.
|
|
)
|
|
|
|
type nativeMuxMetadata struct {
|
|
sessionID uint16
|
|
status byte
|
|
option byte
|
|
network byte
|
|
host string
|
|
port uint16
|
|
globalID [8]byte
|
|
}
|
|
|
|
type nativeMuxPacket struct {
|
|
payload []byte
|
|
host string
|
|
port uint16
|
|
discard bool
|
|
}
|
|
|
|
type nativeMuxUplinkItem struct {
|
|
payload []byte
|
|
host string
|
|
port uint16
|
|
}
|
|
|
|
const nativeMuxUplinkQueue = 64
|
|
|
|
var nativeMuxFramePool = sync.Pool{
|
|
New: func() any {
|
|
b := make([]byte, 0, 2+512+2+nativeUDPMaxPacket)
|
|
return &b
|
|
},
|
|
}
|
|
|
|
type nativeMuxSession struct {
|
|
id uint16
|
|
network byte
|
|
xudp bool
|
|
|
|
tcp net.Conn
|
|
udp net.PacketConn
|
|
|
|
udpNetwork string
|
|
udpTarget net.Addr
|
|
|
|
lastUDPHost string
|
|
lastUDPPort uint16
|
|
lastUDPAddr net.Addr
|
|
|
|
writeMu *sync.Mutex
|
|
client io.Writer
|
|
uuid string
|
|
email string
|
|
|
|
upLimiter *rate.Limiter
|
|
downLimiter *rate.Limiter
|
|
upMeter *trafficMeter
|
|
downMeter *trafficMeter
|
|
|
|
uplink chan nativeMuxUplinkItem
|
|
closed chan struct{}
|
|
closeOnce sync.Once
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
onClose func(*nativeMuxSession)
|
|
releaseSlot func()
|
|
globalID [8]byte
|
|
}
|
|
|
|
var nativeMuxGlobalActive atomic.Int64
|
|
|
|
func acquireNativeMuxGlobalSlot() (func(), bool) {
|
|
limit := int64(nativeMuxGlobalSessionLimit())
|
|
if limit <= 0 {
|
|
return func() {}, true
|
|
}
|
|
for {
|
|
cur := nativeMuxGlobalActive.Load()
|
|
if cur >= limit {
|
|
return nil, false
|
|
}
|
|
if nativeMuxGlobalActive.CompareAndSwap(cur, cur+1) {
|
|
var once sync.Once
|
|
return func() { once.Do(func() { nativeMuxGlobalActive.Add(-1) }) }, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// nativeVLESSMuxTunnel implements the server side of Xray's Mux.Cool framing
|
|
// for VLESS CommandMux. CommandMux does not carry a VLESS target address; every
|
|
// child TCP/UDP request is described by mux frame metadata. UDP is treated as a
|
|
// packet protocol, not as a byte stream, and XUDP-style GlobalID/endpoint
|
|
// metadata is accepted for full-cone friendly clients.
|
|
func (ib *nativeInbound) nativeVLESSMuxTunnel(stream io.ReadWriteCloser, uuid, email string) {
|
|
defer xrayRecover(fmt.Sprintf("native xray VLESS mux user=%s", email))
|
|
xrayMgr.recordNativeConnect(uuid, email)
|
|
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
|
|
|
writeMu := &sync.Mutex{}
|
|
sessions := make(map[uint16]*nativeMuxSession)
|
|
xudpSessions := make(map[[8]byte]*nativeMuxSession)
|
|
readScratch := make([]byte, 0, nativeUDPMaxPacket)
|
|
var mu sync.Mutex
|
|
|
|
removeSession := func(s *nativeMuxSession) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
if cur := sessions[s.id]; cur == s {
|
|
delete(sessions, s.id)
|
|
}
|
|
if s.xudp && s.globalID != [8]byte{} {
|
|
if cur := xudpSessions[s.globalID]; cur == s {
|
|
delete(xudpSessions, s.globalID)
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
closeSession := func(id uint16) {
|
|
mu.Lock()
|
|
s := sessions[id]
|
|
if s != nil {
|
|
delete(sessions, id)
|
|
if s.xudp && s.globalID != [8]byte{} {
|
|
delete(xudpSessions, s.globalID)
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
if s != nil {
|
|
s.closeBackend()
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
mu.Lock()
|
|
all := make([]*nativeMuxSession, 0, len(sessions))
|
|
for _, s := range sessions {
|
|
all = append(all, s)
|
|
}
|
|
sessions = make(map[uint16]*nativeMuxSession)
|
|
xudpSessions = make(map[[8]byte]*nativeMuxSession)
|
|
mu.Unlock()
|
|
for _, s := range all {
|
|
s.closeBackend()
|
|
}
|
|
_ = stream.Close()
|
|
}()
|
|
|
|
for {
|
|
meta, err := readNativeMuxMetadata(stream)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
xrayLogf("native xray: VLESS mux metadata read failed: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
switch meta.status {
|
|
case nativeMuxStatusKeepAlive:
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_, readScratch, _ = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
}
|
|
|
|
case nativeMuxStatusEnd:
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_, readScratch, _ = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
}
|
|
closeSession(meta.sessionID)
|
|
|
|
case nativeMuxStatusNew:
|
|
if meta.network != nativeMuxNetworkTCP && meta.network != nativeMuxNetworkUDP {
|
|
xrayLogf("native xray: VLESS mux session %d unsupported network %d", meta.sessionID, meta.network)
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_, readScratch, _ = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
}
|
|
continue
|
|
}
|
|
|
|
mu.Lock()
|
|
tooManySessions := len(sessions) >= nativeMuxMaxSessionLimit()
|
|
mu.Unlock()
|
|
if tooManySessions {
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_ = discardNativeMuxDataBlock(stream)
|
|
}
|
|
xrayTracef("native xray: VLESS mux rejected new session=%d over limit=%d user=%s", meta.sessionID, nativeMuxMaxSessionLimit(), email)
|
|
writeMu.Lock()
|
|
_ = writeNativeMuxEnd(stream, meta.sessionID, true)
|
|
writeMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
isXUDP := meta.globalID != [8]byte{}
|
|
pkt := nativeMuxPacket{}
|
|
|
|
targetHost, targetPort := meta.host, meta.port
|
|
if isNativeDNSSinkTarget(targetHost) {
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_ = discardNativeMuxDataBlock(stream)
|
|
}
|
|
xrayTracef("native xray: VLESS mux fast-ignored DNS sink target session=%d network=%s host=%q port=%d xudp=%v", meta.sessionID, nativeMuxNetworkName(meta.network), targetHost, targetPort, isXUDP)
|
|
writeMu.Lock()
|
|
_ = writeNativeMuxEnd(stream, meta.sessionID, false)
|
|
writeMu.Unlock()
|
|
continue
|
|
}
|
|
if invalidNativeDestination(targetHost, targetPort) {
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_ = discardNativeMuxDataBlock(stream)
|
|
}
|
|
xrayTracef("native xray: VLESS mux rejected invalid target session=%d network=%s host=%q port=%d xudp=%v", meta.sessionID, nativeMuxNetworkName(meta.network), targetHost, targetPort, isXUDP)
|
|
writeMu.Lock()
|
|
_ = writeNativeMuxEnd(stream, meta.sessionID, true)
|
|
writeMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
pkt.payload, readScratch, err = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux first packet read failed session=%d: %v", meta.sessionID, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
s, target, err := ib.newNativeMuxSession(meta.sessionID, meta.network, targetHost, targetPort, isXUDP, meta.globalID, stream, writeMu, uuid, email, removeSession)
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux session %s setup failed: %v", target, err)
|
|
writeMu.Lock()
|
|
_ = writeNativeMuxEnd(stream, meta.sessionID, true)
|
|
writeMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
mu.Lock()
|
|
if old := sessions[meta.sessionID]; old != nil {
|
|
old.closeBackend()
|
|
}
|
|
sessions[meta.sessionID] = s
|
|
if isXUDP {
|
|
if old := xudpSessions[meta.globalID]; old != nil && old != s {
|
|
old.closeBackend()
|
|
}
|
|
xudpSessions[meta.globalID] = s
|
|
}
|
|
mu.Unlock()
|
|
|
|
xrayTracef("native xray: vless/mux %s user=%s -> %s session=%d xudp=%v", nativeMuxNetworkName(meta.network), email, target, meta.sessionID, isXUDP)
|
|
ib2, host2, port2 := ib, targetHost, targetPort
|
|
xrayGo(fmt.Sprintf("native xray mux session=%d", s.id), func() { s.run(ib2, host2, port2) })
|
|
if len(pkt.payload) > 0 {
|
|
s.enqueueUplink(pkt.payload, pkt.host, pkt.port)
|
|
}
|
|
|
|
case nativeMuxStatusKeep:
|
|
mu.Lock()
|
|
s := sessions[meta.sessionID]
|
|
if s == nil && meta.globalID != [8]byte{} {
|
|
s = xudpSessions[meta.globalID]
|
|
}
|
|
mu.Unlock()
|
|
if meta.option&nativeMuxOptionData == 0 {
|
|
continue
|
|
}
|
|
pkt := nativeMuxPacket{}
|
|
pkt.payload, readScratch, err = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux keep packet read failed session=%d: %v", meta.sessionID, err)
|
|
if s != nil {
|
|
closeSession(s.id)
|
|
}
|
|
continue
|
|
}
|
|
if s == nil {
|
|
writeMu.Lock()
|
|
_ = writeNativeMuxEnd(stream, meta.sessionID, false)
|
|
writeMu.Unlock()
|
|
continue
|
|
}
|
|
// Official Mux.Cool packet sessions read exactly one packet block here.
|
|
// XUDP is represented by GlobalID on the New frame and optional UDP
|
|
// endpoint metadata on Keep frames, not by auto-detecting metadata inside
|
|
// the UDP payload. Auto-detecting inside payload can block QUIC if a real
|
|
// datagram happens to look like XUDP control bytes.
|
|
if meta.host != "" {
|
|
pkt.host = meta.host
|
|
pkt.port = meta.port
|
|
}
|
|
if len(pkt.payload) > 0 {
|
|
s.enqueueUplink(pkt.payload, pkt.host, pkt.port)
|
|
}
|
|
|
|
default:
|
|
xrayLogf("native xray: VLESS mux unknown status %d", meta.status)
|
|
if meta.option&nativeMuxOptionData != 0 {
|
|
_, readScratch, _ = readNativeMuxDataBlockScratch(stream, readScratch)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ib *nativeInbound) newNativeMuxSession(id uint16, network byte, host string, port uint16, xudp bool, globalID [8]byte, client io.Writer, writeMu *sync.Mutex, uuid, email string, onClose func(*nativeMuxSession)) (*nativeMuxSession, string, error) {
|
|
target := net.JoinHostPort(normalizeNativeTargetHost(host), strconv.Itoa(int(port)))
|
|
if invalidNativeDestination(host, port) {
|
|
return nil, target, fmt.Errorf("invalid destination")
|
|
}
|
|
releaseSlot, ok := acquireNativeMuxGlobalSlot()
|
|
if !ok {
|
|
return nil, target, fmt.Errorf("global mux session limit reached")
|
|
}
|
|
s := &nativeMuxSession{
|
|
id: id,
|
|
network: network,
|
|
xudp: xudp,
|
|
writeMu: writeMu,
|
|
client: client,
|
|
uuid: uuid,
|
|
email: email,
|
|
upLimiter: ib.upLimiter(),
|
|
downLimiter: ib.downLimiter(),
|
|
upMeter: &trafficMeter{uuid: uuid, email: email, uplink: true},
|
|
downMeter: &trafficMeter{uuid: uuid, email: email, uplink: false},
|
|
uplink: make(chan nativeMuxUplinkItem, nativeMuxUplinkQueue),
|
|
closed: make(chan struct{}),
|
|
onClose: onClose,
|
|
releaseSlot: releaseSlot,
|
|
globalID: globalID,
|
|
}
|
|
s.ctx, s.cancel = context.WithCancel(context.Background())
|
|
return s, target, nil
|
|
}
|
|
|
|
func (s *nativeMuxSession) run(ib *nativeInbound, host string, port uint16) {
|
|
defer xrayRecover(fmt.Sprintf("native xray mux run session=%d", s.id))
|
|
|
|
select {
|
|
case <-s.closed:
|
|
s.failInit(false)
|
|
return
|
|
default:
|
|
}
|
|
|
|
if s.network == nativeMuxNetworkTCP {
|
|
backend, target, err := ib.nativeDialTCP(host, port)
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux TCP dial %s failed session=%d: %v", target, s.id, err)
|
|
s.failInit(true)
|
|
return
|
|
}
|
|
s.tcp = backend
|
|
} else {
|
|
pc, udpNetwork, udpTarget, target, err := ib.nativeOpenMuxUDP(host, port)
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux UDP open %s failed session=%d: %v", target, s.id, err)
|
|
s.failInit(true)
|
|
return
|
|
}
|
|
s.udp = pc
|
|
s.udpNetwork = udpNetwork
|
|
s.udpTarget = udpTarget
|
|
}
|
|
|
|
select {
|
|
case <-s.closed:
|
|
s.closeBackend()
|
|
return
|
|
default:
|
|
}
|
|
|
|
xrayGo(fmt.Sprintf("native xray mux backend session=%d", s.id), func() { s.readBackendLoop() })
|
|
s.uplinkLoop()
|
|
}
|
|
|
|
func (s *nativeMuxSession) failInit(notifyClient bool) {
|
|
if notifyClient {
|
|
s.writeMu.Lock()
|
|
_ = writeNativeMuxEnd(s.client, s.id, true)
|
|
s.writeMu.Unlock()
|
|
}
|
|
if s.onClose != nil {
|
|
s.onClose(s)
|
|
}
|
|
s.closeBackend()
|
|
}
|
|
|
|
func (s *nativeMuxSession) enqueueUplink(payload []byte, host string, port uint16) {
|
|
if len(payload) == 0 {
|
|
return
|
|
}
|
|
cp := make([]byte, len(payload))
|
|
copy(cp, payload)
|
|
select {
|
|
case s.uplink <- nativeMuxUplinkItem{payload: cp, host: host, port: port}:
|
|
case <-s.closed:
|
|
}
|
|
}
|
|
|
|
func (s *nativeMuxSession) uplinkLoop() {
|
|
defer s.upMeter.flush()
|
|
for {
|
|
select {
|
|
case <-s.closed:
|
|
return
|
|
case item := <-s.uplink:
|
|
if !s.writeBackendItem(item) {
|
|
s.closeBackend()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ib *nativeInbound) nativeOpenMuxUDP(host string, port uint16) (net.PacketConn, string, net.Addr, string, error) {
|
|
targetHost := normalizeNativeTargetHost(host)
|
|
target := net.JoinHostPort(targetHost, strconv.Itoa(int(port)))
|
|
udpNetwork := nativeDialNetwork("udp", targetHost)
|
|
udpTarget, err := net.ResolveUDPAddr(udpNetwork, target)
|
|
if err != nil {
|
|
return nil, udpNetwork, nil, target, err
|
|
}
|
|
|
|
var local *net.UDPAddr
|
|
if addr := nativeLocalAddrForDial(udpNetwork, targetHost, ib.listen); addr != nil {
|
|
if udpAddr, ok := addr.(*net.UDPAddr); ok {
|
|
local = udpAddr
|
|
}
|
|
}
|
|
pc, err := net.ListenUDP(udpNetwork, local)
|
|
if err != nil {
|
|
return nil, udpNetwork, nil, target, err
|
|
}
|
|
if nativeMuxUDPReadBufferSize() > 0 {
|
|
_ = pc.SetReadBuffer(nativeMuxUDPReadBufferSize())
|
|
}
|
|
if nativeMuxUDPWriteBufferSize() > 0 {
|
|
_ = pc.SetWriteBuffer(nativeMuxUDPWriteBufferSize())
|
|
}
|
|
return pc, udpNetwork, udpTarget, target, nil
|
|
}
|
|
|
|
func (s *nativeMuxSession) writeBackendItem(item nativeMuxUplinkItem) bool {
|
|
payload := item.payload
|
|
if len(payload) == 0 {
|
|
return true
|
|
}
|
|
if s.upLimiter != nil {
|
|
if err := s.upLimiter.WaitN(s.ctx, len(payload)); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var n int
|
|
var err error
|
|
if s.network == nativeMuxNetworkTCP {
|
|
n, err = s.tcp.Write(payload)
|
|
} else {
|
|
target := s.udpTarget
|
|
if item.host != "" && item.port != 0 {
|
|
if isNativeDNSSinkTarget(item.host) || invalidNativeDestination(item.host, item.port) {
|
|
// AdGuard/blocked endpoints must be ignored at the cheapest possible
|
|
// point. Do not resolve, dial, log loudly, or keep the mux child busy.
|
|
xrayTracef("native xray: VLESS mux UDP fast-ignored override sink session=%d target=%s:%d", s.id, item.host, item.port)
|
|
return true
|
|
}
|
|
if s.lastUDPAddr != nil && s.lastUDPHost == item.host && s.lastUDPPort == item.port {
|
|
target = s.lastUDPAddr
|
|
} else if addr, rerr := resolveNativeMuxUDPAddr(s.udpNetwork, item.host, item.port); rerr == nil {
|
|
target = addr
|
|
s.lastUDPHost = item.host
|
|
s.lastUDPPort = item.port
|
|
s.lastUDPAddr = addr
|
|
} else {
|
|
xrayTracef("native xray: VLESS mux UDP override resolve failed session=%d target=%s:%d: %v", s.id, item.host, item.port, rerr)
|
|
return true
|
|
}
|
|
}
|
|
n, err = s.udp.WriteTo(payload, target)
|
|
}
|
|
if s.network == nativeMuxNetworkUDP && err == nil {
|
|
_ = s.udp.SetReadDeadline(time.Now().Add(nativeMuxUDPIdleTimeout()))
|
|
}
|
|
if n > 0 {
|
|
s.upMeter.add(n)
|
|
}
|
|
if err != nil {
|
|
xrayLogf("native xray: VLESS mux backend write failed session=%d: %v", s.id, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *nativeMuxSession) readBackendLoop() {
|
|
defer xrayRecover(fmt.Sprintf("native xray mux backend loop session=%d", s.id))
|
|
sendEnd := true
|
|
defer func() {
|
|
s.downMeter.flush()
|
|
if sendEnd {
|
|
s.writeMu.Lock()
|
|
_ = writeNativeMuxEnd(s.client, s.id, false)
|
|
s.writeMu.Unlock()
|
|
}
|
|
if s.onClose != nil {
|
|
s.onClose(s)
|
|
}
|
|
s.closeBackend()
|
|
}()
|
|
|
|
if s.network == nativeMuxNetworkTCP {
|
|
s.readTCPBackendLoop()
|
|
return
|
|
}
|
|
// For UDP/QUIC, an idle backend timeout is only local cleanup. Sending an
|
|
// End frame on idle makes some clients close the whole video/QUIC flow after
|
|
// a short quiet period. Real errors still return true and notify the client.
|
|
sendEnd = s.readUDPBackendLoop()
|
|
}
|
|
|
|
func (s *nativeMuxSession) waitDownRate(n int) error {
|
|
if s.downLimiter == nil || n <= 0 {
|
|
return nil
|
|
}
|
|
return s.downLimiter.WaitN(s.ctx, n)
|
|
}
|
|
|
|
func (s *nativeMuxSession) readTCPBackendLoop() {
|
|
buf := make([]byte, 16*1024)
|
|
for {
|
|
n, err := s.tcp.Read(buf)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
xrayLogf("native xray: VLESS mux TCP backend read failed session=%d: %v", s.id, err)
|
|
}
|
|
return
|
|
}
|
|
if n <= 0 {
|
|
continue
|
|
}
|
|
if err := s.waitDownRate(n); err != nil {
|
|
return
|
|
}
|
|
s.downMeter.add(n)
|
|
s.writeMu.Lock()
|
|
werr := writeNativeMuxData(s.client, s.id, nativeMuxStatusKeep, buf[:n])
|
|
s.writeMu.Unlock()
|
|
if werr != nil {
|
|
xrayLogf("native xray: VLESS mux TCP client write failed session=%d: %v", s.id, werr)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *nativeMuxSession) readUDPBackendLoop() bool {
|
|
buf := make([]byte, nativeUDPBufferSize)
|
|
for {
|
|
_ = s.udp.SetReadDeadline(time.Now().Add(nativeMuxUDPIdleTimeout()))
|
|
n, addr, err := s.udp.ReadFrom(buf)
|
|
if err != nil {
|
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
|
return false
|
|
}
|
|
if err != io.EOF {
|
|
xrayLogf("native xray: VLESS mux UDP backend read failed session=%d: %v", s.id, err)
|
|
}
|
|
return true
|
|
}
|
|
if n <= 0 {
|
|
continue
|
|
}
|
|
if err := s.waitDownRate(n); err != nil {
|
|
return true
|
|
}
|
|
s.downMeter.add(n)
|
|
s.writeMu.Lock()
|
|
// Include the UDP source endpoint on XUDP responses so clients that rely on
|
|
// full-cone packet addressing can associate the datagram with the correct
|
|
// origin. Classic mux UDP also accepts this optional metadata in Xray.
|
|
werr := writeNativeMuxPacketData(s.client, s.id, nativeMuxStatusKeep, buf[:n], addr, s.xudp)
|
|
s.writeMu.Unlock()
|
|
if werr != nil {
|
|
xrayLogf("native xray: VLESS mux UDP client write failed session=%d: %v", s.id, werr)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *nativeMuxSession) closeBackend() {
|
|
s.closeOnce.Do(func() {
|
|
close(s.closed)
|
|
if s.cancel != nil {
|
|
s.cancel()
|
|
}
|
|
if s.releaseSlot != nil {
|
|
s.releaseSlot()
|
|
}
|
|
if s.tcp != nil {
|
|
_ = s.tcp.Close()
|
|
}
|
|
if s.udp != nil {
|
|
_ = s.udp.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
func nativeMuxNetworkName(network byte) string {
|
|
if network == nativeMuxNetworkUDP {
|
|
return "udp"
|
|
}
|
|
return "tcp"
|
|
}
|
|
|
|
func readNativeMuxMetadata(r io.Reader) (nativeMuxMetadata, error) {
|
|
var meta nativeMuxMetadata
|
|
var lenBuf [2]byte
|
|
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
|
|
return meta, err
|
|
}
|
|
metaLen := int(binary.BigEndian.Uint16(lenBuf[:]))
|
|
if metaLen < 4 || metaLen > 512 {
|
|
return meta, fmt.Errorf("invalid mux metadata length %d", metaLen)
|
|
}
|
|
var stack [512]byte
|
|
b := stack[:metaLen]
|
|
if _, err := io.ReadFull(r, b); err != nil {
|
|
return meta, err
|
|
}
|
|
meta.sessionID = binary.BigEndian.Uint16(b[0:2])
|
|
meta.status = b[2]
|
|
meta.option = b[3]
|
|
off := 4
|
|
if meta.status == nativeMuxStatusNew {
|
|
if off >= len(b) {
|
|
return meta, fmt.Errorf("mux new frame missing network")
|
|
}
|
|
meta.network = b[off]
|
|
off++
|
|
host, port, next, err := parseNativeMuxAddressPort(b, off)
|
|
if err != nil {
|
|
return meta, err
|
|
}
|
|
meta.host, meta.port, off = host, port, next
|
|
} else if meta.status == nativeMuxStatusKeep && off < len(b) && b[off] == nativeMuxNetworkUDP {
|
|
meta.network = b[off]
|
|
off++
|
|
host, port, next, err := parseNativeMuxAddressPort(b, off)
|
|
if err == nil {
|
|
meta.host, meta.port, off = host, port, next
|
|
}
|
|
}
|
|
if meta.status == nativeMuxStatusNew && meta.network == nativeMuxNetworkUDP && meta.option&nativeMuxOptionData != 0 && len(b)-off >= 8 {
|
|
copy(meta.globalID[:], b[len(b)-8:])
|
|
}
|
|
return meta, nil
|
|
}
|
|
|
|
func parseNativeMuxAddressPort(b []byte, off int) (string, uint16, int, error) {
|
|
if off+3 > len(b) {
|
|
return "", 0, off, io.ErrUnexpectedEOF
|
|
}
|
|
port := binary.BigEndian.Uint16(b[off : off+2])
|
|
off += 2
|
|
atyp := b[off]
|
|
off++
|
|
switch atyp {
|
|
case atypIPv4:
|
|
if off+4 > len(b) {
|
|
return "", 0, off, io.ErrUnexpectedEOF
|
|
}
|
|
host := net.IP(b[off : off+4]).String()
|
|
return host, port, off + 4, nil
|
|
case atypIPv6:
|
|
if off+16 > len(b) {
|
|
return "", 0, off, io.ErrUnexpectedEOF
|
|
}
|
|
host := net.IP(b[off : off+16]).String()
|
|
return host, port, off + 16, nil
|
|
case atypDomain:
|
|
if off >= len(b) {
|
|
return "", 0, off, io.ErrUnexpectedEOF
|
|
}
|
|
l := int(b[off])
|
|
off++
|
|
if off+l > len(b) {
|
|
return "", 0, off, io.ErrUnexpectedEOF
|
|
}
|
|
return string(b[off : off+l]), port, off + l, nil
|
|
default:
|
|
return "", 0, off, fmt.Errorf("unknown mux address type %d", atyp)
|
|
}
|
|
}
|
|
|
|
func appendNativeMuxAddressPort(dst []byte, host string, port uint16) []byte {
|
|
var p [2]byte
|
|
binary.BigEndian.PutUint16(p[:], port)
|
|
dst = append(dst, p[:]...)
|
|
ip := net.ParseIP(stripNativeIPZone(normalizeNativeTargetHost(host)))
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
dst = append(dst, atypIPv4)
|
|
dst = append(dst, ip4...)
|
|
return dst
|
|
}
|
|
if ip16 := ip.To16(); ip16 != nil {
|
|
dst = append(dst, atypIPv6)
|
|
dst = append(dst, ip16...)
|
|
return dst
|
|
}
|
|
if len(host) > 255 {
|
|
host = host[:255]
|
|
}
|
|
dst = append(dst, atypDomain, byte(len(host)))
|
|
dst = append(dst, []byte(host)...)
|
|
return dst
|
|
}
|
|
|
|
func readNativeMuxDataBlock(r io.Reader) ([]byte, error) {
|
|
payload, _, err := readNativeMuxDataBlockScratch(r, nil)
|
|
return payload, err
|
|
}
|
|
|
|
func readNativeMuxDataBlockScratch(r io.Reader, scratch []byte) ([]byte, []byte, error) {
|
|
var lenBuf [2]byte
|
|
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
|
|
return nil, scratch, err
|
|
}
|
|
n := int(binary.BigEndian.Uint16(lenBuf[:]))
|
|
if n > nativeUDPMaxPacket {
|
|
return nil, scratch, fmt.Errorf("mux payload too large: %d", n)
|
|
}
|
|
if cap(scratch) < n {
|
|
scratch = make([]byte, n)
|
|
}
|
|
payload := scratch[:n]
|
|
_, err := io.ReadFull(r, payload)
|
|
return payload, scratch, err
|
|
}
|
|
|
|
func discardNativeMuxDataBlock(r io.Reader) error {
|
|
var lenBuf [2]byte
|
|
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
|
|
return err
|
|
}
|
|
n := int(binary.BigEndian.Uint16(lenBuf[:]))
|
|
if n > nativeUDPMaxPacket {
|
|
return fmt.Errorf("mux payload too large: %d", n)
|
|
}
|
|
_, err := io.CopyN(io.Discard, r, int64(n))
|
|
return err
|
|
}
|
|
|
|
func readNativeMuxPacket(r io.Reader, allowXUDP bool) (nativeMuxPacket, error) {
|
|
var pkt nativeMuxPacket
|
|
for {
|
|
block, err := readNativeMuxDataBlock(r)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
if !allowXUDP || !isNativeXUDPMetadata(block) {
|
|
pkt.payload = block
|
|
return pkt, nil
|
|
}
|
|
inner, err := parseNativeXUDPMetadata(block)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
if inner.discard {
|
|
return inner, nil
|
|
}
|
|
if block[3]&1 == 0 {
|
|
continue
|
|
}
|
|
inner.payload, err = readNativeMuxDataBlock(r)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
return inner, nil
|
|
}
|
|
}
|
|
|
|
func appendUint16(dst []byte, v uint16) []byte {
|
|
return append(dst, byte(v>>8), byte(v))
|
|
}
|
|
|
|
func writeNativeMuxData(w io.Writer, sessionID uint16, status byte, payload []byte) error {
|
|
return writeNativeMuxPacketData(w, sessionID, status, payload, nil, false)
|
|
}
|
|
|
|
func writeNativeMuxPacketData(w io.Writer, sessionID uint16, status byte, payload []byte, udpAddr net.Addr, includeUDPAddr bool) error {
|
|
if len(payload) > nativeUDPMaxPacket {
|
|
return fmt.Errorf("mux payload too large: %d", len(payload))
|
|
}
|
|
bufp := nativeMuxFramePool.Get().(*[]byte)
|
|
frame := (*bufp)[:0]
|
|
frame = append(frame, 0, 0) // metadata length placeholder
|
|
metaStart := len(frame)
|
|
frame = append(frame, byte(sessionID>>8), byte(sessionID), status, nativeMuxOptionData)
|
|
if includeUDPAddr && udpAddr != nil {
|
|
if host, port, ok := nativeMuxAddrHostPort(udpAddr); ok {
|
|
frame = append(frame, nativeMuxNetworkUDP)
|
|
frame = appendNativeMuxAddressPort(frame, host, port)
|
|
}
|
|
}
|
|
metaLen := len(frame) - metaStart
|
|
binary.BigEndian.PutUint16(frame[:2], uint16(metaLen))
|
|
frame = appendUint16(frame, uint16(len(payload)))
|
|
frame = append(frame, payload...)
|
|
_, err := w.Write(frame)
|
|
*bufp = frame[:0]
|
|
nativeMuxFramePool.Put(bufp)
|
|
return err
|
|
}
|
|
|
|
func writeNativeMuxEnd(w io.Writer, sessionID uint16, hasError bool) error {
|
|
opt := byte(0)
|
|
if hasError {
|
|
opt = nativeMuxOptionError
|
|
}
|
|
var frame [6]byte
|
|
binary.BigEndian.PutUint16(frame[0:2], 4)
|
|
binary.BigEndian.PutUint16(frame[2:4], sessionID)
|
|
frame[4] = nativeMuxStatusEnd
|
|
frame[5] = opt
|
|
_, err := w.Write(frame[:])
|
|
return err
|
|
}
|
|
|
|
func isNativeXUDPMetadata(meta []byte) bool {
|
|
if len(meta) < 4 || len(meta) > 512 {
|
|
return false
|
|
}
|
|
// Xray's xudp.PacketWriter stores a two-byte mux session id at the start of
|
|
// the inner metadata. For client-generated packets this is normally zero.
|
|
if meta[0] != 0 || meta[1] != 0 {
|
|
return false
|
|
}
|
|
cmd := meta[2]
|
|
opt := meta[3]
|
|
if cmd != 1 && cmd != 2 && cmd != 4 {
|
|
return false
|
|
}
|
|
if opt != 0 && opt != 1 {
|
|
return false
|
|
}
|
|
if len(meta) == 4 {
|
|
return true
|
|
}
|
|
return meta[4] == nativeMuxNetworkUDP
|
|
}
|
|
|
|
func parseNativeXUDPMetadata(meta []byte) (nativeMuxPacket, error) {
|
|
var pkt nativeMuxPacket
|
|
if !isNativeXUDPMetadata(meta) {
|
|
return pkt, fmt.Errorf("invalid xudp metadata")
|
|
}
|
|
cmd := meta[2]
|
|
opt := meta[3]
|
|
if cmd == 4 {
|
|
pkt.discard = true
|
|
return pkt, nil
|
|
}
|
|
if len(meta) > 4 && meta[4] == nativeMuxNetworkUDP {
|
|
host, port, _, err := parseNativeMuxAddressPort(meta, 5)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
pkt.host = host
|
|
pkt.port = port
|
|
}
|
|
if opt&1 == 0 {
|
|
return pkt, nil
|
|
}
|
|
// Payload length and bytes follow in the outer stream, so the caller must
|
|
// fill pkt.payload. This path is only used by readNativeXUDPPacket below.
|
|
return pkt, nil
|
|
}
|
|
|
|
func readNativeXUDPPacket(r io.Reader) (nativeMuxPacket, error) {
|
|
var pkt nativeMuxPacket
|
|
for {
|
|
meta, err := readNativeMuxDataBlock(r)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
pkt, err = parseNativeXUDPMetadata(meta)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
if pkt.discard {
|
|
return pkt, nil
|
|
}
|
|
if meta[3]&1 == 0 {
|
|
continue
|
|
}
|
|
payload, err := readNativeMuxDataBlock(r)
|
|
if err != nil {
|
|
return pkt, err
|
|
}
|
|
pkt.payload = payload
|
|
return pkt, nil
|
|
}
|
|
}
|
|
|
|
func resolveNativeMuxUDPAddr(network, host string, port uint16) (net.Addr, error) {
|
|
targetHost := normalizeNativeTargetHost(host)
|
|
if network == "" {
|
|
network = nativeDialNetwork("udp", targetHost)
|
|
}
|
|
return net.ResolveUDPAddr(network, net.JoinHostPort(targetHost, strconv.Itoa(int(port))))
|
|
}
|
|
|
|
func nativeMuxAddrHostPort(addr net.Addr) (string, uint16, bool) {
|
|
switch a := addr.(type) {
|
|
case *net.UDPAddr:
|
|
if a == nil {
|
|
return "", 0, false
|
|
}
|
|
return a.IP.String(), uint16(a.Port), true
|
|
case *net.TCPAddr:
|
|
if a == nil {
|
|
return "", 0, false
|
|
}
|
|
return a.IP.String(), uint16(a.Port), true
|
|
}
|
|
host, portStr, err := net.SplitHostPort(addr.String())
|
|
if err != nil {
|
|
return "", 0, false
|
|
}
|
|
port64, err := strconv.ParseUint(portStr, 10, 16)
|
|
if err != nil {
|
|
return "", 0, false
|
|
}
|
|
return stripNativeIPZone(host), uint16(port64), true
|
|
}
|
|
|
|
func stripNativeIPZone(host string) string {
|
|
if i := strings.LastIndexByte(host, '%'); i >= 0 {
|
|
return host[:i]
|
|
}
|
|
return host
|
|
}
|