mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add vless encryption
This commit is contained in:
25
common/vision/hook.go
Normal file
25
common/vision/hook.go
Normal 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
|
||||
}
|
||||
18
common/xray/cpuid/cpuid.go
Normal file
18
common/xray/cpuid/cpuid.go
Normal 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
6
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -3,6 +3,7 @@ package option
|
||||
type VLESSInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []VLESSUser `json:"users,omitempty"`
|
||||
Decryption string `json:"decryption,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
@@ -19,6 +20,7 @@ type VLESSOutboundOptions struct {
|
||||
ServerOptions
|
||||
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"`
|
||||
|
||||
214
protocol/vless/encryption/client.go
Normal file
214
protocol/vless/encryption/client.go
Normal 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
|
||||
}
|
||||
297
protocol/vless/encryption/common.go
Normal file
297
protocol/vless/encryption/common.go
Normal 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
|
||||
}
|
||||
336
protocol/vless/encryption/server.go
Normal file
336
protocol/vless/encryption/server.go
Normal 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
|
||||
}
|
||||
101
protocol/vless/encryption/xor.go
Normal file
101
protocol/vless/encryption/xor.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -43,6 +47,7 @@ type Inbound struct {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user