Native Xray
This commit is contained in:
+728
@@ -0,0 +1,728 @@
|
||||
package main
|
||||
|
||||
// Pure-Go VMess (AEAD) server, part of the in-process Xray emulator.
|
||||
//
|
||||
// This implements the modern "VMess AEAD" protocol (alterId = 0) exactly as
|
||||
// spoken by current Xray/v2ray clients: AEAD-authenticated request header,
|
||||
// AES-128-GCM / ChaCha20-Poly1305 chunked body with SHAKE-masked lengths,
|
||||
// optional global padding and authenticated length, and the AEAD response
|
||||
// header + body. Byte offsets, KDF labels and orderings follow the v2fly/xray
|
||||
// reference (proxy/vmess/{aead,encoding}). Legacy MD5-auth VMess (alterId > 0)
|
||||
// is intentionally not supported.
|
||||
//
|
||||
// TCP and UDP commands are served. Mux is intentionally not supported in the
|
||||
// native emulator yet.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// ---------- constants ----------
|
||||
|
||||
const vmessCmdKeyMagic = "c48619fe-8f02-49e0-b9e9-edf763e17e21"
|
||||
|
||||
const (
|
||||
kdfSaltVMessAEADKDF = "VMess AEAD KDF"
|
||||
|
||||
kdfLabelAuthIDEncryptionKey = "AES Auth ID Encryption"
|
||||
|
||||
kdfLabelReqHeaderLenKey = "VMess Header AEAD Key_Length"
|
||||
kdfLabelReqHeaderLenIV = "VMess Header AEAD Nonce_Length"
|
||||
kdfLabelReqHeaderKey = "VMess Header AEAD Key"
|
||||
kdfLabelReqHeaderIV = "VMess Header AEAD Nonce"
|
||||
|
||||
kdfLabelRespHeaderLenKey = "AEAD Resp Header Len Key"
|
||||
kdfLabelRespHeaderLenIV = "AEAD Resp Header Len IV"
|
||||
kdfLabelRespHeaderKey = "AEAD Resp Header Key"
|
||||
kdfLabelRespHeaderIV = "AEAD Resp Header IV"
|
||||
|
||||
kdfLabelAuthLen = "auth_len"
|
||||
)
|
||||
|
||||
// VMess request option flags (header byte 34).
|
||||
const (
|
||||
vmessOptChunkStream = 0x01
|
||||
vmessOptChunkMasking = 0x04
|
||||
vmessOptGlobalPadding = 0x08
|
||||
vmessOptAuthenticatedLength = 0x10
|
||||
)
|
||||
|
||||
// VMess security types (low nibble of header byte 35).
|
||||
const (
|
||||
vmessSecAES128GCM = 3
|
||||
vmessSecChaCha20Poly1305 = 4
|
||||
vmessSecNone = 5
|
||||
)
|
||||
|
||||
// VMess commands (header byte 37).
|
||||
const (
|
||||
vmessCmdTCP = 1
|
||||
vmessCmdUDP = 2
|
||||
vmessCmdMux = 3
|
||||
)
|
||||
|
||||
const vmessTimeWindowSeconds = 120
|
||||
|
||||
// ---------- KDF ("VMess AEAD KDF", nested HMAC-SHA256) ----------
|
||||
|
||||
type hmacCreator struct {
|
||||
parent *hmacCreator
|
||||
value []byte
|
||||
}
|
||||
|
||||
func newHMAC(f func() hash.Hash, key []byte) hash.Hash {
|
||||
return hmac.New(f, key)
|
||||
}
|
||||
|
||||
func (h *hmacCreator) create() hash.Hash {
|
||||
if h.parent == nil {
|
||||
return newHMAC(sha256.New, h.value)
|
||||
}
|
||||
return newHMAC(h.parent.create, h.value)
|
||||
}
|
||||
|
||||
func vmessKDF(key []byte, path ...string) []byte {
|
||||
c := &hmacCreator{value: []byte(kdfSaltVMessAEADKDF)}
|
||||
for _, p := range path {
|
||||
c = &hmacCreator{value: []byte(p), parent: c}
|
||||
}
|
||||
h := c.create()
|
||||
h.Write(key)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func vmessKDF16(key []byte, path ...string) []byte {
|
||||
return vmessKDF(key, path...)[:16]
|
||||
}
|
||||
|
||||
// ---------- command key / crypto helpers ----------
|
||||
|
||||
func vmessCmdKey(uuid [16]byte) [16]byte {
|
||||
h := md5.New()
|
||||
h.Write(uuid[:])
|
||||
h.Write([]byte(vmessCmdKeyMagic))
|
||||
var out [16]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return out
|
||||
}
|
||||
|
||||
func newAESGCM(key []byte) cipher.AEAD {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err) // only happens on wrong key length — a programmer error
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gcm
|
||||
}
|
||||
|
||||
// vmessChaChaKey expands a 16-byte key into the 32-byte ChaCha20 key VMess uses:
|
||||
// MD5(key) || MD5(MD5(key)).
|
||||
func vmessChaChaKey(key []byte) []byte {
|
||||
h1 := md5.Sum(key)
|
||||
h2 := md5.Sum(h1[:])
|
||||
out := make([]byte, 32)
|
||||
copy(out[0:16], h1[:])
|
||||
copy(out[16:32], h2[:])
|
||||
return out
|
||||
}
|
||||
|
||||
func absInt64(v int64) int64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func shakeNext(s sha3.ShakeHash) uint16 {
|
||||
var b [2]byte
|
||||
_, _ = s.Read(b[:])
|
||||
return binary.BigEndian.Uint16(b[:])
|
||||
}
|
||||
|
||||
// ---------- auth ID matching ----------
|
||||
|
||||
// matchVMess tries every VMess client's auth-ID cipher against the 16-byte
|
||||
// auth ID, returning the client whose key decrypts to a CRC-valid, in-window
|
||||
// timestamp. This is O(clients) AES blocks per connection.
|
||||
func (ib *nativeInbound) matchVMess(authid [16]byte, now int64) *nativeXrayClient {
|
||||
ib.clientMu.RLock()
|
||||
defer ib.clientMu.RUnlock()
|
||||
for _, c := range ib.clientsByID {
|
||||
if c.authIDCipher == nil {
|
||||
continue
|
||||
}
|
||||
var dec [16]byte
|
||||
c.authIDCipher.Decrypt(dec[:], authid[:])
|
||||
if crc32.ChecksumIEEE(dec[0:12]) != binary.BigEndian.Uint32(dec[12:16]) {
|
||||
continue
|
||||
}
|
||||
t := int64(binary.BigEndian.Uint64(dec[0:8]))
|
||||
if t < 0 || absInt64(t-now) > vmessTimeWindowSeconds {
|
||||
continue
|
||||
}
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- request header ----------
|
||||
|
||||
type vmessRequest struct {
|
||||
bodyIV [16]byte
|
||||
bodyKey [16]byte
|
||||
respV byte
|
||||
option byte
|
||||
security byte
|
||||
command byte
|
||||
host string
|
||||
port uint16
|
||||
}
|
||||
|
||||
// openVMessHeader reads and decrypts the AEAD request header from r, given the
|
||||
// user's command key and the already-read 16-byte auth ID. r must be positioned
|
||||
// immediately after the auth ID.
|
||||
func openVMessHeader(cmdKey [16]byte, authid [16]byte, r io.Reader) ([]byte, error) {
|
||||
var lenBlock [18]byte // 2-byte length + 16-byte tag
|
||||
if _, err := io.ReadFull(r, lenBlock[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var connNonce [8]byte
|
||||
if _, err := io.ReadFull(r, connNonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aid := string(authid[:])
|
||||
cn := string(connNonce[:])
|
||||
|
||||
lenGCM := newAESGCM(vmessKDF16(cmdKey[:], kdfLabelReqHeaderLenKey, aid, cn))
|
||||
lenNonce := vmessKDF(cmdKey[:], kdfLabelReqHeaderLenIV, aid, cn)[:12]
|
||||
lenPlain, err := lenGCM.Open(nil, lenNonce, lenBlock[:], authid[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vmess: header length decrypt: %w", err)
|
||||
}
|
||||
headerLen := int(binary.BigEndian.Uint16(lenPlain))
|
||||
if headerLen < 38 || headerLen > 512 {
|
||||
return nil, fmt.Errorf("vmess: implausible header length %d", headerLen)
|
||||
}
|
||||
|
||||
payload := make([]byte, headerLen+16)
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payGCM := newAESGCM(vmessKDF16(cmdKey[:], kdfLabelReqHeaderKey, aid, cn))
|
||||
payNonce := vmessKDF(cmdKey[:], kdfLabelReqHeaderIV, aid, cn)[:12]
|
||||
header, err := payGCM.Open(nil, payNonce, payload, authid[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vmess: header payload decrypt: %w", err)
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// parseVMessHeader parses the decrypted request header plaintext.
|
||||
func parseVMessHeader(h []byte) (vmessRequest, error) {
|
||||
var req vmessRequest
|
||||
if len(h) < 40 {
|
||||
return req, errors.New("vmess: header too short")
|
||||
}
|
||||
if h[0] != 1 {
|
||||
return req, fmt.Errorf("vmess: unsupported version %d", h[0])
|
||||
}
|
||||
copy(req.bodyIV[:], h[1:17])
|
||||
copy(req.bodyKey[:], h[17:33])
|
||||
req.respV = h[33]
|
||||
req.option = h[34]
|
||||
req.security = h[35] & 0x0f
|
||||
paddingLen := int(h[35] >> 4)
|
||||
req.command = h[37]
|
||||
req.port = binary.BigEndian.Uint16(h[38:40])
|
||||
|
||||
host, next, err := parseVMessAddress(h, 40)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.host = host
|
||||
|
||||
if next+paddingLen+4 != len(h) {
|
||||
return req, fmt.Errorf("vmess: header length mismatch (addr end %d + pad %d + 4 != %d)", next, paddingLen, len(h))
|
||||
}
|
||||
|
||||
f := fnv.New32a()
|
||||
f.Write(h[:len(h)-4])
|
||||
if binary.BigEndian.Uint32(h[len(h)-4:]) != f.Sum32() {
|
||||
return req, errors.New("vmess: header checksum mismatch")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseVMessAddress(h []byte, off int) (host string, next int, err error) {
|
||||
if off >= len(h) {
|
||||
return "", 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
atyp := h[off]
|
||||
off++
|
||||
switch atyp {
|
||||
case atypIPv4:
|
||||
if off+4 > len(h) {
|
||||
return "", 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
host = net.IP(h[off : off+4]).String()
|
||||
off += 4
|
||||
case atypIPv6:
|
||||
if off+16 > len(h) {
|
||||
return "", 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
host = net.IP(h[off : off+16]).String()
|
||||
off += 16
|
||||
case atypDomain:
|
||||
if off >= len(h) {
|
||||
return "", 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
l := int(h[off])
|
||||
off++
|
||||
if off+l > len(h) {
|
||||
return "", 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
host = string(h[off : off+l])
|
||||
off += l
|
||||
default:
|
||||
return "", 0, fmt.Errorf("vmess: unknown address type %d", atyp)
|
||||
}
|
||||
return host, off, nil
|
||||
}
|
||||
|
||||
// ---------- response header ----------
|
||||
|
||||
func writeVMessResponseHeader(w io.Writer, respBodyKey, respBodyIV [16]byte, respV byte) error {
|
||||
header := []byte{respV, 0, 0, 0} // V echo, option 0, command 0, command-data-length 0
|
||||
|
||||
lenGCM := newAESGCM(vmessKDF16(respBodyKey[:], kdfLabelRespHeaderLenKey))
|
||||
lenNonce := vmessKDF(respBodyIV[:], kdfLabelRespHeaderLenIV)[:12]
|
||||
var lenPlain [2]byte
|
||||
binary.BigEndian.PutUint16(lenPlain[:], uint16(len(header)))
|
||||
lenSealed := lenGCM.Seal(nil, lenNonce, lenPlain[:], nil) // AAD nil
|
||||
|
||||
payGCM := newAESGCM(vmessKDF16(respBodyKey[:], kdfLabelRespHeaderKey))
|
||||
payNonce := vmessKDF(respBodyIV[:], kdfLabelRespHeaderIV)[:12]
|
||||
paySealed := payGCM.Seal(nil, payNonce, header, nil) // AAD nil
|
||||
|
||||
out := make([]byte, 0, len(lenSealed)+len(paySealed))
|
||||
out = append(out, lenSealed...)
|
||||
out = append(out, paySealed...)
|
||||
_, err := w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- authenticated-length size parser ----------
|
||||
|
||||
// vmessAuthLen encodes/decodes the 2-byte chunk length as an AEAD-sealed field
|
||||
// (option AuthenticatedLength). It always derives its key from the *request*
|
||||
// body key/IV, in both directions, per the reference.
|
||||
type vmessAuthLen struct {
|
||||
aead cipher.AEAD
|
||||
count uint16
|
||||
ivTail [10]byte
|
||||
nonce [12]byte
|
||||
}
|
||||
|
||||
func newVMessAuthLen(reqBodyKey, reqBodyIV [16]byte, chacha bool) *vmessAuthLen {
|
||||
keyMat := vmessKDF16(reqBodyKey[:], kdfLabelAuthLen)
|
||||
var aead cipher.AEAD
|
||||
if chacha {
|
||||
a, _ := chacha20poly1305.New(vmessChaChaKey(keyMat))
|
||||
aead = a
|
||||
} else {
|
||||
aead = newAESGCM(keyMat)
|
||||
}
|
||||
al := &vmessAuthLen{aead: aead}
|
||||
copy(al.ivTail[:], reqBodyIV[2:12])
|
||||
return al
|
||||
}
|
||||
|
||||
func (al *vmessAuthLen) nextNonce() []byte {
|
||||
binary.BigEndian.PutUint16(al.nonce[0:2], al.count)
|
||||
copy(al.nonce[2:12], al.ivTail[:])
|
||||
al.count++
|
||||
return al.nonce[:12]
|
||||
}
|
||||
|
||||
func (al *vmessAuthLen) decode(r io.Reader) (int, error) {
|
||||
var buf [18]byte
|
||||
if _, err := io.ReadFull(r, buf[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
plain, err := al.aead.Open(nil, al.nextNonce(), buf[:], nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("vmess: auth-len decrypt: %w", err)
|
||||
}
|
||||
return int(binary.BigEndian.Uint16(plain)) + 16, nil
|
||||
}
|
||||
|
||||
func (al *vmessAuthLen) encode(out *bytes.Buffer, size int) {
|
||||
var lenPlain [2]byte
|
||||
binary.BigEndian.PutUint16(lenPlain[:], uint16(size-16))
|
||||
out.Write(al.aead.Seal(nil, al.nextNonce(), lenPlain[:], nil))
|
||||
}
|
||||
|
||||
// ---------- body chunk reader/writer ----------
|
||||
|
||||
const vmessMaxChunk = 64*1024 + 64
|
||||
|
||||
type vmessChunkReader struct {
|
||||
r io.Reader
|
||||
aead cipher.AEAD // nil for security "none"
|
||||
overhead int
|
||||
ivTail [10]byte
|
||||
count uint16
|
||||
nonce [12]byte
|
||||
shake sha3.ShakeHash // non-nil when chunk masking is enabled
|
||||
authLen *vmessAuthLen // non-nil when authenticated length is enabled
|
||||
globalPad bool
|
||||
leftover []byte
|
||||
eof bool
|
||||
}
|
||||
|
||||
func (cr *vmessChunkReader) nextNonce() []byte {
|
||||
binary.BigEndian.PutUint16(cr.nonce[0:2], cr.count)
|
||||
copy(cr.nonce[2:12], cr.ivTail[:])
|
||||
cr.count++
|
||||
return cr.nonce[:12]
|
||||
}
|
||||
|
||||
func (cr *vmessChunkReader) readChunk() ([]byte, error) {
|
||||
// Padding length is always drawn from the SHAKE stream before the size.
|
||||
pad := 0
|
||||
if cr.shake != nil && cr.globalPad {
|
||||
pad = int(shakeNext(cr.shake) % 64)
|
||||
}
|
||||
|
||||
var size int
|
||||
switch {
|
||||
case cr.authLen != nil:
|
||||
s, err := cr.authLen.decode(cr.r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size = s
|
||||
case cr.shake != nil:
|
||||
var b [2]byte
|
||||
if _, err := io.ReadFull(cr.r, b[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size = int(shakeNext(cr.shake) ^ binary.BigEndian.Uint16(b[:]))
|
||||
default:
|
||||
var b [2]byte
|
||||
if _, err := io.ReadFull(cr.r, b[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size = int(binary.BigEndian.Uint16(b[:]))
|
||||
}
|
||||
|
||||
// size == overhead + pad means an empty (terminating) chunk.
|
||||
if size == cr.overhead+pad {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if size < cr.overhead+pad || size > vmessMaxChunk {
|
||||
return nil, fmt.Errorf("vmess: bad chunk size %d", size)
|
||||
}
|
||||
|
||||
data := make([]byte, size)
|
||||
if _, err := io.ReadFull(cr.r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sealed := data[:size-pad] // trailing pad bytes are clear-text, discarded
|
||||
if cr.aead == nil {
|
||||
return sealed, nil
|
||||
}
|
||||
plain, err := cr.aead.Open(nil, cr.nextNonce(), sealed, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vmess: body decrypt: %w", err)
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
|
||||
func (cr *vmessChunkReader) Read(p []byte) (int, error) {
|
||||
for len(cr.leftover) == 0 {
|
||||
if cr.eof {
|
||||
return 0, io.EOF
|
||||
}
|
||||
chunk, err := cr.readChunk()
|
||||
if err == io.EOF {
|
||||
cr.eof = true
|
||||
return 0, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cr.leftover = chunk
|
||||
}
|
||||
n := copy(p, cr.leftover)
|
||||
cr.leftover = cr.leftover[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type vmessChunkWriter struct {
|
||||
w io.Writer
|
||||
aead cipher.AEAD
|
||||
overhead int
|
||||
ivTail [10]byte
|
||||
count uint16
|
||||
nonce [12]byte
|
||||
shake sha3.ShakeHash
|
||||
authLen *vmessAuthLen
|
||||
globalPad bool
|
||||
}
|
||||
|
||||
func (cw *vmessChunkWriter) nextNonce() []byte {
|
||||
binary.BigEndian.PutUint16(cw.nonce[0:2], cw.count)
|
||||
copy(cw.nonce[2:12], cw.ivTail[:])
|
||||
cw.count++
|
||||
return cw.nonce[:12]
|
||||
}
|
||||
|
||||
func (cw *vmessChunkWriter) writeChunk(p []byte) error {
|
||||
var out bytes.Buffer
|
||||
|
||||
pad := 0
|
||||
if cw.shake != nil && cw.globalPad {
|
||||
pad = int(shakeNext(cw.shake) % 64)
|
||||
}
|
||||
size := len(p) + cw.overhead + pad
|
||||
|
||||
switch {
|
||||
case cw.authLen != nil:
|
||||
cw.authLen.encode(&out, size)
|
||||
case cw.shake != nil:
|
||||
var b [2]byte
|
||||
binary.BigEndian.PutUint16(b[:], shakeNext(cw.shake)^uint16(size))
|
||||
out.Write(b[:])
|
||||
default:
|
||||
var b [2]byte
|
||||
binary.BigEndian.PutUint16(b[:], uint16(size))
|
||||
out.Write(b[:])
|
||||
}
|
||||
|
||||
if cw.aead != nil {
|
||||
out.Write(cw.aead.Seal(nil, cw.nextNonce(), p, nil))
|
||||
} else {
|
||||
out.Write(p)
|
||||
}
|
||||
if pad > 0 {
|
||||
padBytes := make([]byte, pad)
|
||||
_, _ = rand.Read(padBytes)
|
||||
out.Write(padBytes)
|
||||
}
|
||||
_, err := cw.w.Write(out.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- vmessConn: net.Conn view of a decoded VMess session ----------
|
||||
|
||||
type nativeVMessStream interface {
|
||||
net.Conn
|
||||
ReadPacket() ([]byte, error)
|
||||
WritePacket([]byte) error
|
||||
}
|
||||
|
||||
type vmessConn struct {
|
||||
net.Conn
|
||||
reader *vmessChunkReader
|
||||
writer *vmessChunkWriter
|
||||
terminateOnClose bool
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (c *vmessConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
||||
|
||||
// ReadPacket returns exactly one decrypted VMess body chunk. UDP-over-VMess uses
|
||||
// one VMess chunk per UDP datagram, so packet handling must bypass the streamy
|
||||
// Read method that can merge/split chunks.
|
||||
func (c *vmessConn) ReadPacket() ([]byte, error) { return c.reader.readChunk() }
|
||||
|
||||
func (c *vmessConn) WritePacket(p []byte) error { return c.writer.writeChunk(p) }
|
||||
|
||||
func (c *vmessConn) Write(p []byte) (int, error) {
|
||||
// Bound each chunk well under the uint16 length field.
|
||||
const maxChunk = 16 * 1024
|
||||
total := 0
|
||||
for len(p) > 0 {
|
||||
n := len(p)
|
||||
if n > maxChunk {
|
||||
n = maxChunk
|
||||
}
|
||||
if err := c.writer.writeChunk(p[:n]); err != nil {
|
||||
return total, err
|
||||
}
|
||||
total += n
|
||||
p = p[n:]
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (c *vmessConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
if c.terminateOnClose {
|
||||
_ = c.writer.writeChunk(nil) // terminating empty chunk
|
||||
}
|
||||
})
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// vmessRawConn is used for VMess security=none when the client did not request
|
||||
// ChunkStream. Xray's own server returns a raw reader/writer in that exact case;
|
||||
// treating the following TLS ClientHello/HTTP bytes as a VMess chunk length makes
|
||||
// real clients authenticate but then pass no data.
|
||||
type vmessRawConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *vmessRawConn) ReadPacket() ([]byte, error) {
|
||||
buf := make([]byte, 64*1024)
|
||||
n, err := c.Conn.Read(buf)
|
||||
if n > 0 {
|
||||
return buf[:n], nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *vmessRawConn) WritePacket(p []byte) error {
|
||||
_, err := c.Conn.Write(p)
|
||||
return err
|
||||
}
|
||||
|
||||
func newVMessConn(stream net.Conn, req vmessRequest, respBodyKey, respBodyIV [16]byte) (nativeVMessStream, error) {
|
||||
chunkMask := req.option&vmessOptChunkMasking != 0
|
||||
globalPad := req.option&vmessOptGlobalPadding != 0
|
||||
authLen := req.option&vmessOptAuthenticatedLength != 0
|
||||
chacha := req.security == vmessSecChaCha20Poly1305
|
||||
|
||||
if req.security == vmessSecNone && req.option&vmessOptChunkStream == 0 {
|
||||
return &vmessRawConn{Conn: stream}, nil
|
||||
}
|
||||
|
||||
var readAEAD, writeAEAD cipher.AEAD
|
||||
overhead := 16
|
||||
switch req.security {
|
||||
case vmessSecAES128GCM:
|
||||
readAEAD = newAESGCM(req.bodyKey[:])
|
||||
writeAEAD = newAESGCM(respBodyKey[:])
|
||||
case vmessSecChaCha20Poly1305:
|
||||
ra, _ := chacha20poly1305.New(vmessChaChaKey(req.bodyKey[:]))
|
||||
wa, _ := chacha20poly1305.New(vmessChaChaKey(respBodyKey[:]))
|
||||
readAEAD, writeAEAD = ra, wa
|
||||
case vmessSecNone:
|
||||
overhead = 0
|
||||
default:
|
||||
return nil, fmt.Errorf("vmess: unsupported security %d", req.security)
|
||||
}
|
||||
|
||||
cr := &vmessChunkReader{r: stream, aead: readAEAD, overhead: overhead, globalPad: globalPad}
|
||||
copy(cr.ivTail[:], req.bodyIV[2:12])
|
||||
cw := &vmessChunkWriter{w: stream, aead: writeAEAD, overhead: overhead, globalPad: globalPad}
|
||||
copy(cw.ivTail[:], respBodyIV[2:12])
|
||||
|
||||
if chunkMask {
|
||||
rs := sha3.NewShake128()
|
||||
rs.Write(req.bodyIV[:])
|
||||
cr.shake = rs
|
||||
ws := sha3.NewShake128()
|
||||
ws.Write(respBodyIV[:])
|
||||
cw.shake = ws
|
||||
}
|
||||
if authLen {
|
||||
cr.authLen = newVMessAuthLen(req.bodyKey, req.bodyIV, chacha)
|
||||
cw.authLen = newVMessAuthLen(req.bodyKey, req.bodyIV, chacha)
|
||||
}
|
||||
|
||||
return &vmessConn{Conn: stream, reader: cr, writer: cw, terminateOnClose: req.option&vmessOptChunkStream != 0 || req.security != vmessSecNone}, nil
|
||||
}
|
||||
|
||||
// ---------- handler ----------
|
||||
|
||||
func (ib *nativeInbound) handleVMess(stream net.Conn, remote net.Addr) {
|
||||
_ = stream.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
var authid [16]byte
|
||||
if _, err := io.ReadFull(stream, authid[:]); err != nil {
|
||||
return
|
||||
}
|
||||
client := ib.matchVMess(authid, time.Now().Unix())
|
||||
if client == nil {
|
||||
log.Printf("native xray: inbound %q rejected unknown/expired VMess auth id from %s", ib.tag, remote)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := openVMessHeader(client.cmdKey, authid, stream)
|
||||
if err != nil {
|
||||
log.Printf("native xray: inbound %q VMess header open failed from %s: %v", ib.tag, remote, err)
|
||||
return
|
||||
}
|
||||
req, err := parseVMessHeader(header)
|
||||
if err != nil {
|
||||
log.Printf("native xray: inbound %q VMess header parse failed from %s: %v", ib.tag, remote, err)
|
||||
return
|
||||
}
|
||||
_ = stream.SetReadDeadline(time.Time{})
|
||||
|
||||
if req.command != vmessCmdTCP && req.command != vmessCmdUDP {
|
||||
log.Printf("native xray: inbound %q VMess command %d not supported yet", ib.tag, req.command)
|
||||
return
|
||||
}
|
||||
|
||||
respBodyKey := sha256.Sum256(req.bodyKey[:])
|
||||
respBodyIV := sha256.Sum256(req.bodyIV[:])
|
||||
var rk, riv [16]byte
|
||||
copy(rk[:], respBodyKey[:16])
|
||||
copy(riv[:], respBodyIV[:16])
|
||||
|
||||
if err := writeVMessResponseHeader(stream, rk, riv, req.respV); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vc, err := newVMessConn(stream, req, rk, riv)
|
||||
if err != nil {
|
||||
log.Printf("native xray: inbound %q VMess codec: %v", ib.tag, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.command {
|
||||
case vmessCmdTCP:
|
||||
backend, target, err := ib.nativeDialTCP(req.host, req.port)
|
||||
if err != nil {
|
||||
log.Printf("native xray: inbound %q VMess TCP dial %s failed: %v", ib.tag, target, err)
|
||||
return
|
||||
}
|
||||
log.Printf("native xray: vmess/tcp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
||||
nativeTunnel(vc, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
||||
case vmessCmdUDP:
|
||||
backend, target, err := ib.nativeDialUDP(req.host, req.port)
|
||||
if err != nil {
|
||||
log.Printf("native xray: inbound %q VMess UDP dial %s failed: %v", ib.tag, target, err)
|
||||
return
|
||||
}
|
||||
log.Printf("native xray: vmess/udp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
||||
nativeVMessUDPTunnel(vc, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user