Add vless encryption

This commit is contained in:
Sergei Maklagin
2026-02-26 18:03:59 +03:00
parent 3d16078651
commit 69f6c75dd7
11 changed files with 1391 additions and 19 deletions

25
common/vision/hook.go Normal file
View File

@@ -0,0 +1,25 @@
package vision
import (
"context"
"net"
)
type Hook func(net.Conn)
type hookKey struct{}
func WithHook(ctx context.Context, hook Hook) context.Context {
if hook == nil {
return ctx
}
return context.WithValue(ctx, hookKey{}, hook)
}
func HookFromContext(ctx context.Context) (Hook, bool) {
if ctx == nil {
return nil, false
}
hook, ok := ctx.Value(hookKey{}).(Hook)
return hook, ok
}

View File

@@ -0,0 +1,18 @@
package cpuid
import (
"runtime"
"golang.org/x/sys/cpu"
)
var (
// Keep in sync with crypto/tls/cipher_suites.go.
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasGHASH
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
// HasAESGCM indicates whether the CPU has AES-GCM hardware acceleration.
HasAESGCM = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)

6
go.mod
View File

@@ -15,6 +15,7 @@ require (
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/klauspost/cpuid/v2 v2.2.10
github.com/libdns/alidns v1.0.5-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible
@@ -57,6 +58,7 @@ require (
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
howett.net/plist v1.0.1
lukechampine.com/blake3 v1.4.1
)
require (
@@ -107,7 +109,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
@@ -145,7 +146,6 @@ require (
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0
@@ -155,3 +155,5 @@ replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.80.3-s
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.6-extended-1.0.0
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
replace github.com/sagernet/sing-vmess => github.com/starifly/sing-vmess v0.2.7-mod.9

4
go.sum
View File

@@ -185,8 +185,6 @@ github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.11 h1:qB7jy8JKqXg73fYBsDkBSy4ulRSbLrFut0e+y+QPhqU=
github.com/sagernet/sing-tun v0.7.11/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@@ -201,6 +199,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/starifly/sing-vmess v0.2.7-mod.9 h1:xobAmejSbBQ0A3f/EtJ9cJd3m6gK7dDPccPdeGz7tXY=
github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=

View File

@@ -2,7 +2,8 @@ package option
type VLESSInboundOptions struct {
ListenOptions
Users []VLESSUser `json:"users,omitempty"`
Users []VLESSUser `json:"users,omitempty"`
Decryption string `json:"decryption,omitempty"`
InboundTLSOptionsContainer
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
@@ -17,9 +18,10 @@ type VLESSUser struct {
type VLESSOutboundOptions struct {
DialerOptions
ServerOptions
UUID string `json:"uuid"`
Flow string `json:"flow,omitempty"`
Network NetworkList `json:"network,omitempty"`
UUID string `json:"uuid"`
Flow string `json:"flow,omitempty"`
Encryption string `json:"encryption,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`

View File

@@ -0,0 +1,214 @@
package encryption
import (
"crypto/cipher"
"crypto/ecdh"
"crypto/mlkem"
"crypto/rand"
"io"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/cpuid"
E "github.com/sagernet/sing/common/exceptions"
"lukechampine.com/blake3"
)
type ClientInstance struct {
NfsPKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
Seconds uint32
PaddingLens [][3]int
PaddingGaps [][3]int
RWLock sync.RWMutex
Expire time.Time
PfsKey []byte
Ticket []byte
}
func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) {
if i.NfsPKeys != nil {
return E.New("already initialized")
}
l := len(nfsPKeysBytes)
if l == 0 {
return E.New("empty nfsPKeysBytes")
}
i.NfsPKeys = make([]any, l)
i.NfsPKeysBytes = nfsPKeysBytes
i.Hash32s = make([][32]byte, l)
for j, k := range nfsPKeysBytes {
if len(k) == 32 {
if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil {
return
}
i.RelaysLength += 32 + 32
} else {
if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil {
return
}
i.RelaysLength += 1088 + 32
}
i.Hash32s[j] = blake3.Sum256(k)
}
i.RelaysLength -= 32
i.XorMode = xorMode
i.Seconds = seconds
return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
}
func (i *ClientInstance) IsFullRandomXorMode() bool {
return i.XorMode == 2
}
func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
if i.NfsPKeys == nil {
return nil, E.New("uninitialized")
}
c := NewCommonConn(conn, cpuid.HasAESGCM)
ivAndRealysLength := 16 + i.RelaysLength
pfsKeyExchangeLength := 18 + 1184 + 32 + 16
paddingLength, paddingLens, paddingGaps := CreatePadding(i.PaddingLens, i.PaddingGaps)
clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength)
iv := clientHello[:16]
rand.Read(iv)
relays := clientHello[16:ivAndRealysLength]
var nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsPKeys {
var index = 32
if k, ok := k.(*ecdh.PublicKey); ok {
privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
copy(relays, privateKey.PublicKey().Bytes())
var err error
nfsKey, err = privateKey.ECDH(k)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.EncapsulationKey768); ok {
var ciphertext []byte
nfsKey, ciphertext = k.Encapsulate()
copy(relays, ciphertext)
index = 1088
}
if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes
}
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable
}
if j == len(i.NfsPKeys)-1 {
break
}
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:])
relays = relays[index+32:]
}
nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES)
if i.Seconds > 0 {
i.RWLock.RLock()
if time.Now().Before(i.Expire) {
c.Client = i
c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection
nfsAEAD.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil)
nfsAEAD.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil)
i.RWLock.RUnlock()
c.PreWrite = clientHello[:ivAndRealysLength+18+32]
c.AEAD = NewAEAD(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey, c.UseAES)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16)
}
return c, nil
}
i.RWLock.RUnlock()
}
pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength]
nfsAEAD.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil)
mlkem768DKey, _ := mlkem.GenerateKey768()
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...)
nfsAEAD.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil)
padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:]
nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0]
for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
if l > 0 {
if _, err := conn.Write(clientHello[:l]); err != nil {
return nil, err
}
clientHello = clientHello[l:]
}
if len(paddingGaps) > i {
time.Sleep(paddingGaps[i])
}
}
encryptedPfsPublicKey := make([]byte, 1088+32+16)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
nfsAEAD.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil)
mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088])
if err != nil {
return nil, err
}
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32])
if err != nil {
return nil, err
}
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
pfsKey := make([]byte, 32+32) // no more capacity
copy(pfsKey, mlkem768Key)
copy(pfsKey[32:], x25519Key)
c.UnitedKey = append(pfsKey, nfsKey...)
c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1088+32], c.UnitedKey, c.UseAES)
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
if _, err := c.PeerAEAD.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil {
return nil, err
}
seconds := DecodeLength(encryptedTicket)
if i.Seconds > 0 && seconds > 0 {
i.RWLock.Lock()
i.Expire = time.Now().Add(time.Duration(seconds) * time.Second)
i.PfsKey = pfsKey
i.Ticket = encryptedTicket[:16]
i.RWLock.Unlock()
}
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := c.PeerAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
length := DecodeLength(encryptedLength[:2])
c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length)
}
return c, nil
}

View File

@@ -0,0 +1,297 @@
package encryption
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/crypto/chacha20poly1305"
"lukechampine.com/blake3"
)
var OutBytesPool = sync.Pool{
New: func() any {
return make([]byte, 5+8192+16)
},
}
type EncryptionConn interface {
net.Conn
IsEncryptionLayer() bool
}
type CommonConn struct {
net.Conn
UseAES bool
Client *ClientInstance
UnitedKey []byte
PreWrite []byte
AEAD *AEAD
PeerAEAD *AEAD
PeerPadding []byte
rawInput bytes.Buffer
input bytes.Reader
}
func NewCommonConn(conn net.Conn, useAES bool) *CommonConn {
return &CommonConn{
Conn: conn,
UseAES: useAES,
}
}
func (c *CommonConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
outBytes := OutBytesPool.Get().([]byte)
defer OutBytesPool.Put(outBytes)
for n := 0; n < len(b); {
b := b[n:]
if len(b) > 8192 {
b = b[:8192] // for avoiding another copy() in peer's Read()
}
n += len(b)
headerAndData := outBytes[:5+len(b)+16]
EncodeHeader(headerAndData, len(b)+16)
max := false
if bytes.Equal(c.AEAD.Nonce[:], MaxNonce) {
max = true
}
c.AEAD.Seal(headerAndData[:5], nil, b, headerAndData[:5])
if max {
c.AEAD = NewAEAD(headerAndData, c.UnitedKey, c.UseAES)
}
if c.PreWrite != nil {
headerAndData = append(c.PreWrite, headerAndData...)
c.PreWrite = nil
}
if _, err := c.Conn.Write(headerAndData); err != nil {
return 0, err
}
}
return len(b), nil
}
func (c *CommonConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if c.PeerAEAD == nil { // client's 0-RTT
serverRandom := make([]byte, 16)
if _, err := io.ReadFull(c.Conn, serverRandom); err != nil {
return 0, err
}
c.PeerAEAD = NewAEAD(serverRandom, c.UnitedKey, c.UseAES)
if xorConn, ok := c.Conn.(*XorConn); ok {
xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom)
}
}
if c.PeerPadding != nil { // client's 1-RTT
if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil {
return 0, err
}
if _, err := c.PeerAEAD.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil {
return 0, err
}
c.PeerPadding = nil
}
if c.input.Len() > 0 {
return c.input.Read(b)
}
peerHeader := [5]byte{}
if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil {
return 0, err
}
l, err := DecodeHeader(peerHeader[:]) // l: 17~17000
if err != nil {
if c.Client != nil && errors.Is(err, ErrInvalidHeader) { // client's 0-RTT
c.Client.RWLock.Lock()
if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) {
c.Client.Expire = time.Now() // expired
}
c.Client.RWLock.Unlock()
return 0, E.New("new handshake needed")
}
return 0, err
}
c.Client = nil
if c.rawInput.Cap() < l {
c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading
}
peerData := c.rawInput.Bytes()[:l]
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:l-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // avoids another copy()
}
var newAEAD *AEAD
if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) {
newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES)
}
_, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:])
if newAEAD != nil {
c.PeerAEAD = newAEAD
}
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.input.Reset(dst[copy(b, dst):])
dst = b // for len(dst)
}
return len(dst), nil
}
// Upstream returns the underlying connection, allowing Vision to unwrap and access the TLS connection
func (c *CommonConn) Upstream() any {
return c.Conn
}
func (c *CommonConn) IsEncryptionLayer() bool {
return true
}
type AEAD struct {
cipher.AEAD
Nonce [12]byte
}
func NewAEAD(ctx, key []byte, useAES bool) *AEAD {
k := make([]byte, 32)
blake3.DeriveKey(k, string(ctx), key)
var aead cipher.AEAD
if useAES {
block, _ := aes.NewCipher(k)
aead, _ = cipher.NewGCM(block)
} else {
aead, _ = chacha20poly1305.New(k)
}
return &AEAD{AEAD: aead}
}
func (a *AEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Seal(dst, nonce, plaintext, additionalData)
}
func (a *AEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Open(dst, nonce, ciphertext, additionalData)
}
func IncreaseNonce(nonce []byte) []byte {
for i := range 12 {
nonce[11-i]++
if nonce[11-i] != 0 {
break
}
}
return nonce
}
var MaxNonce = bytes.Repeat([]byte{255}, 12)
func EncodeLength(l int) []byte {
return []byte{byte(l >> 8), byte(l)}
}
func DecodeLength(b []byte) int {
return int(b[0])<<8 | int(b[1])
}
func EncodeHeader(h []byte, l int) {
h[0] = 23
h[1] = 3
h[2] = 3
h[3] = byte(l >> 8)
h[4] = byte(l)
}
var ErrInvalidHeader = errors.New("invalid header")
func DecodeHeader(h []byte) (l int, err error) {
l = int(h[3])<<8 | int(h[4])
if h[0] != 23 || h[1] != 3 || h[2] != 3 {
l = 0
}
if l < 17 || l > 17000 { // TODO: TLSv1.3 max length
err = fmt.Errorf("%w: %v", ErrInvalidHeader, h[:5]) // DO NOT CHANGE: relied by client's Read()
}
return
}
func ParsePadding(padding string, paddingLens, paddingGaps *[][3]int) (err error) {
if padding == "" {
return
}
maxLen := 0
for i, s := range strings.Split(padding, ".") {
x := strings.Split(s, "-")
if len(x) < 3 || x[0] == "" || x[1] == "" || x[2] == "" {
return E.New("invalid padding lenth/gap parameter: " + s)
}
y := [3]int{}
if y[0], err = strconv.Atoi(x[0]); err != nil {
return
}
if y[1], err = strconv.Atoi(x[1]); err != nil {
return
}
if y[2], err = strconv.Atoi(x[2]); err != nil {
return
}
if i == 0 && (y[0] < 100 || y[1] < 18+17 || y[2] < 18+17) {
return E.New("first padding length must not be smaller than 35")
}
if i%2 == 0 {
*paddingLens = append(*paddingLens, y)
maxLen += max(y[1], y[2])
} else {
*paddingGaps = append(*paddingGaps, y)
}
}
if maxLen > 18+65535 {
return E.New("total padding length must not be larger than 65553")
}
return
}
func CreatePadding(paddingLens, paddingGaps [][3]int) (length int, lens []int, gaps []time.Duration) {
if len(paddingLens) == 0 {
paddingLens = [][3]int{{100, 111, 1111}, {50, 0, 3333}}
paddingGaps = [][3]int{{75, 0, 111}}
}
for _, y := range paddingLens {
l := 0
if y[0] >= int(crypto.RandBetween(0, 100)) {
l = int(crypto.RandBetween(int64(y[1]), int64(y[2])))
}
lens = append(lens, l)
length += l
}
for _, y := range paddingGaps {
g := 0
if y[0] >= int(crypto.RandBetween(0, 100)) {
g = int(crypto.RandBetween(int64(y[1]), int64(y[2])))
}
gaps = append(gaps, time.Duration(g)*time.Millisecond)
}
return
}

View File

@@ -0,0 +1,336 @@
package encryption
import (
"bytes"
"crypto/cipher"
"crypto/ecdh"
"crypto/mlkem"
"crypto/rand"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
"lukechampine.com/blake3"
)
type ServerSession struct {
PfsKey []byte
NfsKeys sync.Map
}
type ServerInstance struct {
NfsSKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
SecondsFrom int64
SecondsTo int64
PaddingLens [][3]int
PaddingGaps [][3]int
RWLock sync.RWMutex
Closed bool
Lasts map[int64][16]byte
Tickets [][16]byte
Sessions map[[16]byte]*ServerSession
}
func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) {
if i.NfsSKeys != nil {
return E.New("already initialized")
}
l := len(nfsSKeysBytes)
if l == 0 {
return E.New("empty nfsSKeysBytes")
}
i.NfsSKeys = make([]any, l)
i.NfsPKeysBytes = make([][]byte, l)
i.Hash32s = make([][32]byte, l)
for j, k := range nfsSKeysBytes {
if len(k) == 32 {
if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes()
i.RelaysLength += 32 + 32
} else {
if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes()
i.RelaysLength += 1088 + 32
}
i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j])
}
i.RelaysLength -= 32
i.XorMode = xorMode
i.SecondsFrom = secondsFrom
i.SecondsTo = secondsTo
err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
if err != nil {
return
}
if i.SecondsFrom > 0 || i.SecondsTo > 0 {
i.Lasts = make(map[int64][16]byte)
i.Tickets = make([][16]byte, 0, 1024)
i.Sessions = make(map[[16]byte]*ServerSession)
go func() {
for {
time.Sleep(time.Minute)
i.RWLock.Lock()
if i.Closed {
i.RWLock.Unlock()
return
}
minute := time.Now().Unix() / 60
last := i.Lasts[minute]
delete(i.Lasts, minute)
delete(i.Lasts, minute-1) // for insurance
if last != [16]byte{} {
for j, ticket := range i.Tickets {
delete(i.Sessions, ticket)
if ticket == last {
i.Tickets = i.Tickets[j+1:]
break
}
}
}
i.RWLock.Unlock()
}
}()
}
return
}
func (i *ServerInstance) Close() (err error) {
i.RWLock.Lock()
i.Closed = true
i.RWLock.Unlock()
return
}
func (i *ServerInstance) IsXorMode() bool {
return i.XorMode > 0
}
func (i *ServerInstance) IsFullRandomXorMode() bool {
return i.XorMode == 2
}
func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) {
if i.NfsSKeys == nil {
return nil, E.New("uninitialized")
}
c := NewCommonConn(conn, true)
ivAndRelays := make([]byte, 16+i.RelaysLength)
if _, err := io.ReadFull(conn, ivAndRelays); err != nil {
return nil, err
}
if fallback != nil {
*fallback = append(*fallback, ivAndRelays...)
}
iv := ivAndRelays[:16]
relays := ivAndRelays[16:]
var nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsSKeys {
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay
}
var index = 32
if _, ok := k.(*mlkem.DecapsulationKey768); ok {
index = 1088
}
if i.XorMode > 0 {
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator2, because we have PSK :)
}
if k, ok := k.(*ecdh.PrivateKey); ok {
publicKey, err := ecdh.X25519().NewPublicKey(relays[:index])
if err != nil {
return nil, err
}
if publicKey.Bytes()[31] > 127 { // we just don't want the observer can change even one bit without breaking the connection, though it has nothing to do with security
return nil, E.New("the highest bit of the last byte of the peer-sent X25519 public key is not 0")
}
nfsKey, err = k.ECDH(publicKey)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.DecapsulationKey768); ok {
var err error
nfsKey, err = k.Decapsulate(relays[:index])
if err != nil {
return nil, err
}
}
if j == len(i.NfsSKeys)-1 {
break
}
relays = relays[index:]
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays, relays[:32])
if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) {
return nil, E.New("unexpected hash32: " + fmt.Sprintf("%v", relays[:32]))
}
relays = relays[32:]
}
nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES)
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if fallback != nil {
*fallback = append(*fallback, encryptedLength...)
}
decryptedLength := make([]byte, 2)
if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil {
c.UseAES = !c.UseAES
nfsAEAD = NewAEAD(iv, nfsKey, c.UseAES)
if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
}
if fallback != nil {
*fallback = nil
}
length := DecodeLength(decryptedLength)
if length == 32 {
if i.SecondsFrom == 0 && i.SecondsTo == 0 {
return nil, E.New("0-RTT is not allowed")
}
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
ticket, err := nfsAEAD.Open(nil, nil, encryptedTicket, nil)
if err != nil {
return nil, err
}
i.RWLock.RLock()
s := i.Sessions[[16]byte(ticket)]
i.RWLock.RUnlock()
if s == nil {
noises := make([]byte, crypto.RandBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example
var err error
for err == nil {
rand.Read(noises)
_, err = DecodeHeader(noises)
}
conn.Write(noises) // make client do new handshake
return nil, E.New("expired ticket")
}
if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also
return nil, E.New("replay detected")
}
c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request)
c.PreWrite = make([]byte, 16)
rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub")
c.AEAD = NewAEAD(c.PreWrite, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedTicket, c.UnitedKey, c.UseAES) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client
}
return c, nil
}
if length < 1184+32+16 { // client may send more public keys in the future's version
return nil, E.New("too short length")
}
encryptedPfsPublicKey := make([]byte, length)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil {
return nil, err
}
mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184])
if err != nil {
return nil, err
}
mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate()
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32])
if err != nil {
return nil, err
}
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
pfsKey := make([]byte, 32+32) // no more capacity
copy(pfsKey, mlkem768Key)
copy(pfsKey[32:], x25519Key)
pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...)
c.UnitedKey = append(pfsKey, nfsKey...)
c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES)
ticket := [16]byte{}
rand.Read(ticket[:])
var seconds int64
if i.SecondsTo == 0 {
seconds = i.SecondsFrom * crypto.RandBetween(50, 100) / 100
} else {
seconds = crypto.RandBetween(i.SecondsFrom, i.SecondsTo)
}
copy(ticket[:], EncodeLength(int(seconds)))
if seconds > 0 {
i.RWLock.Lock()
i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket
i.Tickets = append(i.Tickets, ticket)
i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey}
i.RWLock.Unlock()
}
pfsKeyExchangeLength := 1088 + 32 + 16
encryptedTicketLength := 32
paddingLength, paddingLens, paddingGaps := CreatePadding(i.PaddingLens, i.PaddingGaps)
serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength)
nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil)
c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil)
padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:]
c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0]
for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
if l > 0 {
if _, err := conn.Write(serverHello[:l]); err != nil {
return nil, err
}
serverHello = serverHello[l:]
}
if len(paddingGaps) > i {
time.Sleep(paddingGaps[i])
}
}
// important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2]))
if _, err := io.ReadFull(conn, encryptedPadding); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil {
return nil, err
}
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0)
}
return c, nil
}

View File

@@ -0,0 +1,101 @@
package encryption
import (
"crypto/aes"
"crypto/cipher"
"net"
"lukechampine.com/blake3"
)
func NewCTR(key, iv []byte) cipher.Stream {
k := make([]byte, 32)
blake3.DeriveKey(k, "VLESS", key) // avoids using key directly
block, _ := aes.NewCipher(k)
return cipher.NewCTR(block, iv)
}
type XorConn struct {
net.Conn
CTR cipher.Stream
PeerCTR cipher.Stream
OutSkip int
OutHeader []byte
InSkip int
InHeader []byte
}
func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn {
return &XorConn{
Conn: conn,
CTR: ctr,
PeerCTR: peerCTR,
OutSkip: outSkip,
OutHeader: make([]byte, 0, 5), // important
InSkip: inSkip,
InHeader: make([]byte, 0, 5), // important
}
}
func (c *XorConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
for p := b; ; {
if len(p) <= c.OutSkip {
c.OutSkip -= len(p)
break
}
p = p[c.OutSkip:]
c.OutSkip = 0
need := 5 - len(c.OutHeader)
if len(p) < need {
c.OutHeader = append(c.OutHeader, p...)
c.CTR.XORKeyStream(p, p)
break
}
c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...))
c.OutHeader = c.OutHeader[:0]
c.CTR.XORKeyStream(p[:need], p[:need])
p = p[need:]
}
if _, err := c.Conn.Write(b); err != nil {
return 0, err
}
return len(b), nil
}
func (c *XorConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
n, err := c.Conn.Read(b)
for p := b[:n]; ; {
if len(p) <= c.InSkip {
c.InSkip -= len(p)
break
}
p = p[c.InSkip:]
c.InSkip = 0
need := 5 - len(c.InHeader)
if len(p) < need {
c.PeerCTR.XORKeyStream(p, p)
c.InHeader = append(c.InHeader, p...)
break
}
c.PeerCTR.XORKeyStream(p[:need], p[:need])
c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...))
c.InHeader = c.InHeader[:0]
p = p[need:]
}
return n, err
}
// Upstream returns the underlying connection, allowing Vision to unwrap and access the TLS connection
func (c *XorConn) Upstream() any {
return c.Conn
}
func (c *XorConn) IsEncryptionLayer() bool {
return true
}

View File

@@ -2,8 +2,11 @@ package vless
import (
"context"
"encoding/base64"
"net"
"os"
"strconv"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
@@ -14,6 +17,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vless/encryption"
"github.com/sagernet/sing-box/transport/v2ray"
"github.com/sagernet/sing-vmess/packetaddr"
"github.com/sagernet/sing-vmess/vless"
@@ -35,14 +39,15 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
type Inbound struct {
inbound.Adapter
ctx context.Context
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
users []option.VLESSUser
service *vless.Service[int]
tlsConfig tls.ServerConfig
transport adapter.V2RayServerTransport
ctx context.Context
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
users []option.VLESSUser
service *vless.Service[int]
tlsConfig tls.ServerConfig
transport adapter.V2RayServerTransport
decryption *encryption.ServerInstance
}
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
@@ -79,6 +84,18 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
}
}
// Parse decryption configuration
if options.Decryption != "" && options.Decryption != "none" {
decryptionConfig, err := parseServerDecryption(options.Decryption)
if err != nil {
return nil, E.Cause(err, "parse decryption")
}
inbound.decryption = &encryption.ServerInstance{}
if err := inbound.decryption.Init(decryptionConfig.keys, decryptionConfig.xorMode, decryptionConfig.secondsFrom, decryptionConfig.secondsTo, decryptionConfig.padding); err != nil {
return nil, E.Cause(err, "initialize decryption")
}
logger.Debug("decryption initialized with ", len(decryptionConfig.keys), " keys xorMode=", decryptionConfig.xorMode, " secondsFrom=", decryptionConfig.secondsFrom, " secondsTo=", decryptionConfig.secondsTo, " padding=", decryptionConfig.padding)
}
inbound.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
@@ -130,6 +147,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
}
func (h *Inbound) Close() error {
if h.decryption != nil {
h.decryption.Close()
}
return common.Close(
h.service,
h.listener,
@@ -139,6 +159,14 @@ func (h *Inbound) Close() error {
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
canSplice := h.transport == nil
if canSplice && h.decryption != nil && h.decryption.IsFullRandomXorMode() {
canSplice = false
}
h.newConnectionExInternal(ctx, conn, metadata, onClose, canSplice)
}
func (h *Inbound) newConnectionExInternal(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc, canSplice bool) {
if h.tlsConfig != nil && h.transport == nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
if err != nil {
@@ -148,7 +176,17 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
}
conn = tlsConn
}
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
// Apply decryption if configured
if h.decryption != nil {
encConn, err := h.decryption.Handshake(conn, nil)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": encryption handshake"))
return
}
conn = encConn
}
err := h.service.NewConnectionWithOptions(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose, canSplice)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
@@ -197,6 +235,90 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
type serverDecryptionConfig struct {
keys [][]byte
xorMode uint32
secondsFrom int64
secondsTo int64
padding string
}
func parseServerDecryption(raw string) (serverDecryptionConfig, error) {
var cfg serverDecryptionConfig
raw = strings.TrimSpace(raw)
if raw == "" {
return cfg, E.New("empty decryption string")
}
parts := strings.Split(raw, ".")
if len(parts) < 4 {
return cfg, E.New("invalid decryption string: missing components")
}
if parts[0] != "mlkem768x25519plus" {
return cfg, E.New("unsupported decryption prefix: ", parts[0])
}
switch parts[1] {
case "native":
cfg.xorMode = 0
case "xorpub":
cfg.xorMode = 1
case "random":
cfg.xorMode = 2
default:
return cfg, E.New("unknown decryption mode: ", parts[1])
}
secondsToken := strings.TrimSpace(parts[2])
if secondsToken == "" {
return cfg, E.New("invalid decryption seconds segment")
}
trimmed := strings.TrimSuffix(secondsToken, "s")
if trimmed == "" {
return cfg, E.New("invalid decryption seconds segment")
}
values := strings.SplitN(trimmed, "-", 2)
secondsFrom, err := strconv.ParseInt(values[0], 10, 64)
if err != nil {
return cfg, E.Cause(err, "parse decryption seconds_from")
}
cfg.secondsFrom = secondsFrom
if len(values) == 2 && values[1] != "" {
secondsTo, err := strconv.ParseInt(values[1], 10, 64)
if err != nil {
return cfg, E.Cause(err, "parse decryption seconds_to")
}
cfg.secondsTo = secondsTo
}
paddingPhase := true
var paddingParts []string
for _, segment := range parts[3:] {
segment = strings.TrimSpace(segment)
if segment == "" {
return cfg, E.New("invalid empty segment in decryption string")
}
if data, err := base64.RawURLEncoding.DecodeString(segment); err == nil {
if len(data) == 32 || len(data) == 64 {
cfg.keys = append(cfg.keys, data)
paddingPhase = false
continue
}
return cfg, E.New("invalid decryption key length: ", len(data))
}
if paddingPhase {
paddingParts = append(paddingParts, segment)
continue
}
return cfg, E.New("invalid decryption key: ", segment)
}
if len(cfg.keys) == 0 {
return cfg, E.New("no valid decryption keys found in decryption string")
}
if len(paddingParts) > 0 {
cfg.padding = strings.Join(paddingParts, ".")
}
return cfg, nil
}
var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil)
type inboundTransportHandler Inbound
@@ -210,5 +332,5 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
//nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
(*Inbound)(h).newConnectionExInternal(ctx, conn, metadata, onClose, false)
}

View File

@@ -2,16 +2,23 @@ package vless
import (
"context"
stdtls "crypto/tls"
"encoding/base64"
"net"
"reflect"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/mux"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/vision"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vless/encryption"
"github.com/sagernet/sing-box/transport/v2ray"
"github.com/sagernet/sing-vmess/packetaddr"
"github.com/sagernet/sing-vmess/vless"
@@ -38,6 +45,8 @@ type Outbound struct {
transport adapter.V2RayClientTransport
packetAddr bool
xudp bool
encryption *encryption.ClientInstance
vision bool
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) {
@@ -50,6 +59,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
logger: logger,
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
vision: strings.HasPrefix(options.Flow, "xtls-rprx-vision"),
}
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
@@ -76,11 +86,28 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
}
}
// Parse encryption configuration
if options.Encryption != "" && options.Encryption != "none" {
encryptionConfig, err := parseClientEncryption(options.Encryption)
if err != nil {
return nil, E.Cause(err, "parse encryption")
}
outbound.encryption = &encryption.ClientInstance{}
if err := outbound.encryption.Init(encryptionConfig.keys, encryptionConfig.xorMode, encryptionConfig.seconds, encryptionConfig.padding); err != nil {
return nil, E.Cause(err, "initialize encryption")
}
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
}
muxOpts := common.PtrValueOrDefault(options.Multiplex)
if muxOpts.Enabled {
options.Flow = ""
}
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
if err != nil {
return nil, err
}
outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, muxOpts)
if err != nil {
return nil, err
}
@@ -137,21 +164,92 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
metadata.Outbound = h.Tag()
metadata.Destination = destination
var conn net.Conn
var baseConn net.Conn
var hookOnce sync.Once
if h.vision {
ctx = vision.WithHook(ctx, func(tlsConn net.Conn) {
if tlsConn == nil || !isVisionTLSConn(tlsConn) {
return
}
hookOnce.Do(func() {
baseConn = tlsConn
})
})
}
var err error
if h.transport != nil {
conn, err = h.transport.DialContext(ctx)
if err == nil && h.vision {
if baseConn == nil {
// Only set baseConn if the transport delivered a TLS-capable connection
if isVisionTLSConn(conn) {
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
baseConn = conn
}
}
}
} else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
if err == nil && h.vision && baseConn == nil {
baseConn = conn
}
}
}
if err != nil {
return nil, err
}
// Apply encryption if configured
if h.encryption != nil {
conn, err = h.encryption.Handshake(conn)
if err != nil {
return nil, E.Cause(err, "encryption handshake")
}
}
// For Vision: wrap the connection to expose the TLS/encryption connection for vless client
var visionBaseConn net.Conn // The connection to pass to Vision (TLS or encryption layer)
var visionCanSplice bool
if h.vision {
isRAWTransport := h.transport == nil
if baseConn != nil && !isVisionTLSConn(baseConn) {
baseConn = nil
}
if baseConn != nil {
// Has TLS/Reality: use baseConn (TLS connection)
visionBaseConn = baseConn
visionCanSplice = isRAWTransport
conn = newVisionConnWrapper(conn, baseConn)
} else if h.encryption != nil {
// Only has encryption (no TLS/Reality): use encryption layer itself
encConn := findEncryptionLayer(conn)
if encConn != nil {
visionBaseConn = encConn
if h.encryption.IsFullRandomXorMode() {
visionCanSplice = false
} else {
visionCanSplice = isRAWTransport
}
conn = newVisionConnWrapper(conn, encConn)
} else {
return nil, E.New("Vision: failed to find encryption layer")
}
} else {
return nil, E.New("Vision requires either TLS/Reality or Encryption")
}
}
switch N.NetworkName(network) {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
if h.vision && visionBaseConn != nil {
// For Vision, we need to pass the base connection (TLS or encryption layer)
// to prepareConn so it can properly initialize VisionConn
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
}
return h.client.DialEarlyConn(conn, destination)
case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
@@ -193,6 +291,14 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
common.Close(conn)
return nil, err
}
// Apply encryption if configured
if h.encryption != nil {
conn, err = h.encryption.Handshake(conn)
if err != nil {
common.Close(conn)
return nil, E.Cause(err, "encryption handshake")
}
}
if h.xudp {
return h.client.DialEarlyXUDPPacketConn(conn, destination)
} else if h.packetAddr {
@@ -208,3 +314,152 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
return h.client.DialEarlyPacketConn(conn, destination)
}
}
type visionConnWrapper struct {
net.Conn
upstream net.Conn
}
var (
_ N.ReaderWithUpstream = (*visionConnWrapper)(nil)
_ N.WriterWithUpstream = (*visionConnWrapper)(nil)
_ common.WithUpstream = (*visionConnWrapper)(nil)
)
func newVisionConnWrapper(conn net.Conn, upstream net.Conn) net.Conn {
if upstream == nil || conn == nil || conn == upstream {
return conn
}
return &visionConnWrapper{
Conn: conn,
upstream: upstream,
}
}
func (c *visionConnWrapper) Upstream() any {
return c.upstream
}
func (c *visionConnWrapper) ReaderReplaceable() bool {
if replacer, ok := c.Conn.(N.ReaderWithUpstream); ok {
return replacer.ReaderReplaceable()
}
return true
}
func (c *visionConnWrapper) WriterReplaceable() bool {
if replacer, ok := c.Conn.(N.WriterWithUpstream); ok {
return replacer.WriterReplaceable()
}
return true
}
// isVisionTLSConn returns true when the provided connection exposes TLS semantics Vision expects.
func isVisionTLSConn(conn net.Conn) bool {
if conn == nil {
return false
}
if _, ok := conn.(interface{ ConnectionState() stdtls.ConnectionState }); ok {
return true
}
if _, ok := conn.(interface{ Handshake() error }); ok {
return true
}
connType := reflect.TypeOf(conn)
if connType == nil {
return false
}
if connType.Kind() == reflect.Ptr {
pkgPath := connType.Elem().PkgPath()
if pkgPath == "crypto/tls" || strings.Contains(pkgPath, "utls") || strings.Contains(pkgPath, "shadowtls") {
return true
}
}
return false
}
func findEncryptionLayer(conn net.Conn) net.Conn {
for conn != nil {
if enc, ok := conn.(encryption.EncryptionConn); ok && enc.IsEncryptionLayer() {
return conn
}
if upstream, ok := conn.(common.WithUpstream); ok {
if next := upstream.Upstream(); next != nil {
if nextConn, ok := next.(net.Conn); ok {
conn = nextConn
continue
}
}
}
break
}
return nil
}
type clientEncryptionConfig struct {
keys [][]byte
xorMode uint32
seconds uint32
padding string
}
func parseClientEncryption(raw string) (clientEncryptionConfig, error) {
var cfg clientEncryptionConfig
raw = strings.TrimSpace(raw)
if raw == "" {
return cfg, E.New("empty encryption string")
}
parts := strings.Split(raw, ".")
if len(parts) < 4 {
return cfg, E.New("invalid encryption string: missing components")
}
if parts[0] != "mlkem768x25519plus" {
return cfg, E.New("unsupported encryption prefix: ", parts[0])
}
switch parts[1] {
case "native":
cfg.xorMode = 0
case "xorpub":
cfg.xorMode = 1
case "random":
cfg.xorMode = 2
default:
return cfg, E.New("unknown encryption mode: ", parts[1])
}
switch parts[2] {
case "0rtt":
cfg.seconds = 1
case "1rtt":
cfg.seconds = 0
default:
return cfg, E.New("unsupported encryption RTT value: ", parts[2])
}
paddingPhase := true
var paddingParts []string
for _, segment := range parts[3:] {
segment = strings.TrimSpace(segment)
if segment == "" {
return cfg, E.New("invalid empty segment in encryption string")
}
if data, err := base64.RawURLEncoding.DecodeString(segment); err == nil {
if len(data) == 32 || len(data) == 1184 {
cfg.keys = append(cfg.keys, data)
paddingPhase = false
continue
}
return cfg, E.New("invalid encryption key length: ", len(data))
}
if paddingPhase {
paddingParts = append(paddingParts, segment)
continue
}
return cfg, E.New("invalid encryption key: ", segment)
}
if len(cfg.keys) == 0 {
return cfg, E.New("no valid encryption keys found in encryption string")
}
if len(paddingParts) > 0 {
cfg.padding = strings.Join(paddingParts, ".")
}
return cfg, nil
}