Fix xray native
This commit is contained in:
@@ -0,0 +1,851 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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 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
|
||||
|
||||
closed chan struct{}
|
||||
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)
|
||||
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 {
|
||||
_, _ = readNativeMuxDataBlock(stream)
|
||||
}
|
||||
|
||||
case nativeMuxStatusEnd:
|
||||
if meta.option&nativeMuxOptionData != 0 {
|
||||
_, _ = readNativeMuxDataBlock(stream)
|
||||
}
|
||||
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 {
|
||||
_, _ = readNativeMuxDataBlock(stream)
|
||||
}
|
||||
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, err = readNativeMuxDataBlock(stream)
|
||||
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 dial %s failed: %v", target, err)
|
||||
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)
|
||||
xrayGo(fmt.Sprintf("native xray mux backend session=%d", s.id), func() { s.readBackendLoop() })
|
||||
if len(pkt.payload) > 0 {
|
||||
s.writeBackend(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, err = readNativeMuxDataBlock(stream)
|
||||
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.writeBackend(pkt.payload, pkt.host, pkt.port)
|
||||
}
|
||||
|
||||
default:
|
||||
xrayLogf("native xray: VLESS mux unknown status %d", meta.status)
|
||||
if meta.option&nativeMuxOptionData != 0 {
|
||||
_, _ = readNativeMuxDataBlock(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if invalidNativeDestination(host, port) {
|
||||
target := net.JoinHostPort(normalizeNativeTargetHost(host), strconv.Itoa(int(port)))
|
||||
return nil, target, fmt.Errorf("invalid destination")
|
||||
}
|
||||
releaseSlot, ok := acquireNativeMuxGlobalSlot()
|
||||
if !ok {
|
||||
target := net.JoinHostPort(normalizeNativeTargetHost(host), strconv.Itoa(int(port)))
|
||||
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},
|
||||
closed: make(chan struct{}),
|
||||
onClose: onClose,
|
||||
releaseSlot: releaseSlot,
|
||||
globalID: globalID,
|
||||
}
|
||||
|
||||
if network == nativeMuxNetworkTCP {
|
||||
backend, target, err := ib.nativeDialTCP(host, port)
|
||||
if err != nil {
|
||||
releaseSlot()
|
||||
return nil, target, err
|
||||
}
|
||||
s.tcp = backend
|
||||
return s, target, nil
|
||||
}
|
||||
|
||||
pc, udpNetwork, udpTarget, target, err := ib.nativeOpenMuxUDP(host, port)
|
||||
if err != nil {
|
||||
releaseSlot()
|
||||
return nil, target, err
|
||||
}
|
||||
s.udp = pc
|
||||
s.udpNetwork = udpNetwork
|
||||
s.udpTarget = udpTarget
|
||||
return s, target, nil
|
||||
}
|
||||
|
||||
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) writeBackend(payload []byte, overrideHost string, overridePort uint16) {
|
||||
if len(payload) == 0 {
|
||||
return
|
||||
}
|
||||
if err := waitNativeRate(s.upLimiter, len(payload)); err != nil {
|
||||
s.closeBackend()
|
||||
return
|
||||
}
|
||||
|
||||
var n int
|
||||
var err error
|
||||
if s.network == nativeMuxNetworkTCP {
|
||||
n, err = s.tcp.Write(payload)
|
||||
} else {
|
||||
target := s.udpTarget
|
||||
if overrideHost != "" && overridePort != 0 {
|
||||
if isNativeDNSSinkTarget(overrideHost) || invalidNativeDestination(overrideHost, overridePort) {
|
||||
// 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, overrideHost, overridePort)
|
||||
return
|
||||
}
|
||||
if s.lastUDPAddr != nil && s.lastUDPHost == overrideHost && s.lastUDPPort == overridePort {
|
||||
target = s.lastUDPAddr
|
||||
} else if addr, rerr := resolveNativeMuxUDPAddr(s.udpNetwork, overrideHost, overridePort); rerr == nil {
|
||||
target = addr
|
||||
s.lastUDPHost = overrideHost
|
||||
s.lastUDPPort = overridePort
|
||||
s.lastUDPAddr = addr
|
||||
} else {
|
||||
xrayTracef("native xray: VLESS mux UDP override resolve failed session=%d target=%s:%d: %v", s.id, overrideHost, overridePort, rerr)
|
||||
return
|
||||
}
|
||||
}
|
||||
n, err = s.udp.WriteTo(payload, target)
|
||||
}
|
||||
if n > 0 {
|
||||
s.upMeter.add(n)
|
||||
}
|
||||
if err != nil {
|
||||
xrayLogf("native xray: VLESS mux backend write failed session=%d: %v", s.id, err)
|
||||
s.closeBackend()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeMuxSession) readBackendLoop() {
|
||||
defer xrayRecover(fmt.Sprintf("native xray mux backend loop session=%d", s.id))
|
||||
defer func() {
|
||||
s.downMeter.flush()
|
||||
s.upMeter.flush()
|
||||
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
|
||||
}
|
||||
s.readUDPBackendLoop()
|
||||
}
|
||||
|
||||
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 := waitNativeRate(s.downLimiter, 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() {
|
||||
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
|
||||
}
|
||||
if err != io.EOF {
|
||||
xrayLogf("native xray: VLESS mux UDP backend read failed session=%d: %v", s.id, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n <= 0 {
|
||||
continue
|
||||
}
|
||||
if err := waitNativeRate(s.downLimiter, n); err != nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeMuxSession) closeBackend() {
|
||||
select {
|
||||
case <-s.closed:
|
||||
return
|
||||
default:
|
||||
close(s.closed)
|
||||
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)
|
||||
}
|
||||
b := make([]byte, 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) {
|
||||
var lenBuf [2]byte
|
||||
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(lenBuf[:]))
|
||||
if n > nativeUDPMaxPacket {
|
||||
return nil, fmt.Errorf("mux payload too large: %d", n)
|
||||
}
|
||||
payload := make([]byte, n)
|
||||
_, err := io.ReadFull(r, payload)
|
||||
return payload, 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))
|
||||
}
|
||||
meta := []byte{byte(sessionID >> 8), byte(sessionID), status, nativeMuxOptionData}
|
||||
if includeUDPAddr && udpAddr != nil {
|
||||
if host, port, ok := nativeMuxAddrHostPort(udpAddr); ok {
|
||||
meta = append(meta, nativeMuxNetworkUDP)
|
||||
meta = appendNativeMuxAddressPort(meta, host, port)
|
||||
}
|
||||
}
|
||||
// Keep every logical mux packet in one Write call. The XHTTP response writer
|
||||
// flushes after each Write, so splitting this into metadata/payload writes
|
||||
// multiplies flush/syscall work and can pin a CPU core under QUIC traffic.
|
||||
frame := make([]byte, 0, 2+len(meta)+2+len(payload))
|
||||
frame = appendUint16(frame, uint16(len(meta)))
|
||||
frame = append(frame, meta...)
|
||||
frame = appendUint16(frame, uint16(len(payload)))
|
||||
frame = append(frame, payload...)
|
||||
_, err := w.Write(frame)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeNativeMuxEnd(w io.Writer, sessionID uint16, hasError bool) error {
|
||||
opt := byte(0)
|
||||
if hasError {
|
||||
opt = nativeMuxOptionError
|
||||
}
|
||||
meta := []byte{0, 4, byte(sessionID >> 8), byte(sessionID), nativeMuxStatusEnd, opt}
|
||||
_, err := w.Write(meta)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import "runtime/debug"
|
||||
|
||||
// xrayRecover prevents a bad client packet, closed HTTP stream, or mux/session
|
||||
// race from taking down the whole sshpanel process. A panic should only kill the
|
||||
// current native Xray connection/session and must always leave a visible stack
|
||||
// trace in /api/xray/logs and journald.
|
||||
func xrayRecover(where string) {
|
||||
if r := recover(); r != nil {
|
||||
xrayLogf("native xray: panic recovered in %s: %v\n%s", where, r, debug.Stack())
|
||||
}
|
||||
}
|
||||
|
||||
func xrayGo(where string, fn func()) {
|
||||
go func() {
|
||||
defer xrayRecover(where)
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// XrayNativeTuning contains native-emulator performance limits that are edited
|
||||
// from the admin panel and saved in config.json under xray.native_tuning.
|
||||
// Values are process/runtime settings, not generated Xray JSON settings.
|
||||
type XrayNativeTuning struct {
|
||||
MuxMaxSessions int `json:"mux_max_sessions,omitempty"`
|
||||
MuxGlobalSessions int `json:"mux_global_sessions,omitempty"`
|
||||
MuxUDPIdleMS int `json:"mux_udp_idle_ms,omitempty"`
|
||||
MuxUDPReadBuffer int `json:"mux_udp_read_buffer,omitempty"`
|
||||
MuxUDPWriteBuffer int `json:"mux_udp_write_buffer,omitempty"`
|
||||
XHTTPMaxSessions int `json:"xhttp_max_sessions,omitempty"`
|
||||
XHTTPBufferedPosts int `json:"xhttp_buffered_posts,omitempty"`
|
||||
XHTTPQueueTimeoutMS int `json:"xhttp_queue_timeout_ms,omitempty"`
|
||||
XHTTPFlushMS int `json:"xhttp_flush_ms,omitempty"`
|
||||
XHTTPFlushBytes int `json:"xhttp_flush_bytes,omitempty"`
|
||||
H2MaxConcurrentStreams int `json:"h2_max_concurrent_streams,omitempty"`
|
||||
H2UploadBufferConn int `json:"h2_upload_buffer_conn,omitempty"`
|
||||
H2UploadBufferStream int `json:"h2_upload_buffer_stream,omitempty"`
|
||||
TracePackets bool `json:"trace_packets,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultNativeMuxMaxSessions = 128
|
||||
defaultNativeMuxGlobalSessions = 32768
|
||||
defaultNativeMuxUDPIdleMS = 15000
|
||||
defaultNativeMuxUDPReadBuffer = 256 * 1024
|
||||
defaultNativeMuxUDPWriteBuffer = 256 * 1024
|
||||
defaultNativeXHTTPMaxSessions = 16384
|
||||
defaultNativeXHTTPBufferedPosts = 64
|
||||
defaultNativeXHTTPQueueTimeoutMS = 250
|
||||
defaultNativeXHTTPFlushMS = 5
|
||||
defaultNativeXHTTPFlushBytes = 128 * 1024
|
||||
defaultNativeH2MaxConcurrentStreams = 1024
|
||||
defaultNativeH2UploadBufferConn = 1 * 1024 * 1024
|
||||
defaultNativeH2UploadBufferStream = 256 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
nativeTuneMuxMaxSessions atomic.Int64
|
||||
nativeTuneMuxGlobalSessions atomic.Int64
|
||||
nativeTuneMuxUDPIdleMS atomic.Int64
|
||||
nativeTuneMuxUDPReadBuffer atomic.Int64
|
||||
nativeTuneMuxUDPWriteBuffer atomic.Int64
|
||||
nativeTuneXHTTPMaxSessions atomic.Int64
|
||||
nativeTuneXHTTPBufferedPosts atomic.Int64
|
||||
nativeTuneXHTTPQueueTimeoutMS atomic.Int64
|
||||
nativeTuneXHTTPFlushMS atomic.Int64
|
||||
nativeTuneXHTTPFlushBytes atomic.Int64
|
||||
nativeTuneH2MaxConcurrentStreams atomic.Int64
|
||||
nativeTuneH2UploadBufferConn atomic.Int64
|
||||
nativeTuneH2UploadBufferStream atomic.Int64
|
||||
nativeTuneTracePackets atomic.Bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
applyNativeXrayTuning(nil)
|
||||
}
|
||||
|
||||
func normalizeNativeXrayTuning(t *XrayNativeTuning) XrayNativeTuning {
|
||||
if t == nil {
|
||||
t = &XrayNativeTuning{}
|
||||
}
|
||||
out := *t
|
||||
if out.MuxMaxSessions <= 0 {
|
||||
out.MuxMaxSessions = defaultNativeMuxMaxSessions
|
||||
}
|
||||
if out.MuxGlobalSessions <= 0 {
|
||||
out.MuxGlobalSessions = defaultNativeMuxGlobalSessions
|
||||
}
|
||||
if out.MuxUDPIdleMS <= 0 {
|
||||
out.MuxUDPIdleMS = defaultNativeMuxUDPIdleMS
|
||||
}
|
||||
if out.MuxUDPReadBuffer <= 0 {
|
||||
out.MuxUDPReadBuffer = defaultNativeMuxUDPReadBuffer
|
||||
}
|
||||
if out.MuxUDPWriteBuffer <= 0 {
|
||||
out.MuxUDPWriteBuffer = defaultNativeMuxUDPWriteBuffer
|
||||
}
|
||||
if out.XHTTPMaxSessions <= 0 {
|
||||
out.XHTTPMaxSessions = defaultNativeXHTTPMaxSessions
|
||||
}
|
||||
if out.XHTTPBufferedPosts <= 0 {
|
||||
out.XHTTPBufferedPosts = defaultNativeXHTTPBufferedPosts
|
||||
}
|
||||
if out.XHTTPQueueTimeoutMS <= 0 {
|
||||
out.XHTTPQueueTimeoutMS = defaultNativeXHTTPQueueTimeoutMS
|
||||
}
|
||||
if out.XHTTPFlushMS <= 0 {
|
||||
out.XHTTPFlushMS = defaultNativeXHTTPFlushMS
|
||||
}
|
||||
if out.XHTTPFlushBytes <= 0 {
|
||||
out.XHTTPFlushBytes = defaultNativeXHTTPFlushBytes
|
||||
}
|
||||
if out.H2MaxConcurrentStreams <= 0 {
|
||||
out.H2MaxConcurrentStreams = defaultNativeH2MaxConcurrentStreams
|
||||
}
|
||||
if out.H2UploadBufferConn <= 0 {
|
||||
out.H2UploadBufferConn = defaultNativeH2UploadBufferConn
|
||||
}
|
||||
if out.H2UploadBufferStream <= 0 {
|
||||
out.H2UploadBufferStream = defaultNativeH2UploadBufferStream
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func applyNativeXrayTuning(t *XrayNativeTuning) XrayNativeTuning {
|
||||
out := normalizeNativeXrayTuning(t)
|
||||
nativeTuneMuxMaxSessions.Store(int64(out.MuxMaxSessions))
|
||||
nativeTuneMuxGlobalSessions.Store(int64(out.MuxGlobalSessions))
|
||||
nativeTuneMuxUDPIdleMS.Store(int64(out.MuxUDPIdleMS))
|
||||
nativeTuneMuxUDPReadBuffer.Store(int64(out.MuxUDPReadBuffer))
|
||||
nativeTuneMuxUDPWriteBuffer.Store(int64(out.MuxUDPWriteBuffer))
|
||||
nativeTuneXHTTPMaxSessions.Store(int64(out.XHTTPMaxSessions))
|
||||
nativeTuneXHTTPBufferedPosts.Store(int64(out.XHTTPBufferedPosts))
|
||||
nativeTuneXHTTPQueueTimeoutMS.Store(int64(out.XHTTPQueueTimeoutMS))
|
||||
nativeTuneXHTTPFlushMS.Store(int64(out.XHTTPFlushMS))
|
||||
nativeTuneXHTTPFlushBytes.Store(int64(out.XHTTPFlushBytes))
|
||||
nativeTuneH2MaxConcurrentStreams.Store(int64(out.H2MaxConcurrentStreams))
|
||||
nativeTuneH2UploadBufferConn.Store(int64(out.H2UploadBufferConn))
|
||||
nativeTuneH2UploadBufferStream.Store(int64(out.H2UploadBufferStream))
|
||||
nativeTuneTracePackets.Store(out.TracePackets)
|
||||
return out
|
||||
}
|
||||
|
||||
func nativeMuxMaxSessionLimit() int { return int(nativeTuneMuxMaxSessions.Load()) }
|
||||
func nativeMuxGlobalSessionLimit() int { return int(nativeTuneMuxGlobalSessions.Load()) }
|
||||
func nativeMuxUDPIdleTimeout() time.Duration {
|
||||
return time.Duration(nativeTuneMuxUDPIdleMS.Load()) * time.Millisecond
|
||||
}
|
||||
func nativeMuxUDPReadBufferSize() int { return int(nativeTuneMuxUDPReadBuffer.Load()) }
|
||||
func nativeMuxUDPWriteBufferSize() int { return int(nativeTuneMuxUDPWriteBuffer.Load()) }
|
||||
func nativeXHTTPMaxSessionLimit() int { return int(nativeTuneXHTTPMaxSessions.Load()) }
|
||||
func nativeXHTTPBufferedPostLimit() int { return int(nativeTuneXHTTPBufferedPosts.Load()) }
|
||||
func nativeXHTTPQueuePushTimeoutDuration() time.Duration {
|
||||
return time.Duration(nativeTuneXHTTPQueueTimeoutMS.Load()) * time.Millisecond
|
||||
}
|
||||
func nativeXHTTPFlushIntervalDuration() time.Duration {
|
||||
return time.Duration(nativeTuneXHTTPFlushMS.Load()) * time.Millisecond
|
||||
}
|
||||
func nativeXHTTPFlushByteLimit() int { return int(nativeTuneXHTTPFlushBytes.Load()) }
|
||||
func nativeH2MaxConcurrentStreams() int { return int(nativeTuneH2MaxConcurrentStreams.Load()) }
|
||||
func nativeH2UploadBufferConn() int { return int(nativeTuneH2UploadBufferConn.Load()) }
|
||||
func nativeH2UploadBufferStream() int { return int(nativeTuneH2UploadBufferStream.Load()) }
|
||||
func nativeTracePacketsEnabled() bool { return nativeTuneTracePackets.Load() }
|
||||
Reference in New Issue
Block a user