feat: support vless encryption

This commit is contained in:
wwqgtxx 2025-08-10 22:16:25 +08:00
parent e89af723cd
commit 1b0c72bfab
14 changed files with 726 additions and 10 deletions

View File

@ -3,10 +3,14 @@ package outbound
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/metacubex/mihomo/common/convert"
N "github.com/metacubex/mihomo/common/net"
@ -19,6 +23,7 @@ import (
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/vless"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/metacubex/mihomo/transport/vmess"
vmessSing "github.com/metacubex/sing-vmess"
@ -31,6 +36,8 @@ type Vless struct {
client *vless.Client
option *VlessOption
encryption *encryption.ClientInstance
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
@ -53,6 +60,7 @@ type VlessOption struct {
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Encryption string `proxy:"encryption,omitempty"`
Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
@ -164,6 +172,12 @@ func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if v.encryption != nil {
c, err = v.encryption.Handshake(c)
if err != nil {
return
}
}
if metadata.NetWork == C.UDP {
if v.option.PacketAddr {
metadata = &C.Metadata{
@ -442,6 +456,36 @@ func NewVless(option VlessOption) (*Vless, error) {
option: &option,
}
if s := strings.Split(option.Encryption, "-mlkem768client-"); len(s) == 2 {
var minutes uint32
if s[0] != "1rtt" {
t := strings.TrimSuffix(s[0], "min")
if t == s[0] {
return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption)
}
i, err := strconv.Atoi(t)
if err != nil {
return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption)
}
minutes = uint32(i)
}
b, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption)
}
if len(b) == 1184 {
v.encryption = &encryption.ClientInstance{}
if err := v.encryption.Init(b, time.Duration(minutes)*time.Minute); err != nil {
return nil, fmt.Errorf("failed to use mlkem768seed: %w", err)
}
} else {
return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption)
}
if option.Flow != "" {
return nil, errors.New(`VLESS users: "encryption" doesn't support "flow" yet`)
}
}
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err

View File

@ -5,13 +5,14 @@ import (
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair")
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair/vless-mlkem768")
}
switch args[0] {
case "uuid":
@ -45,5 +46,16 @@ func Main(args []string) {
}
fmt.Println("Config:", configBase64)
fmt.Println("Key:", keyPem)
case "vless-mlkem768":
var seed string
if len(args) > 1 {
seed = args[1]
}
seedBase64, pubBase64, err := encryption.GenMLKEM768(seed)
if err != nil {
panic(err)
}
fmt.Println("Seed: " + seedBase64)
fmt.Println("Client: " + pubBase64)
}
}

View File

@ -632,6 +632,16 @@ proxies: # socks5
# fingerprint: xxxx
# skip-cert-verify: true
- name: "vless-encryption"
type: vless
server: server
port: 443
uuid: uuid
network: tcp
encryption: "8min-mlkem768client-bas64RawURLEncoding" # 复用八分钟后协商新的 sharedKey需小于服务端的值
tls: false #可以不开启tls
udp: true
- name: "vless-reality-vision"
type: vless
server: server
@ -1336,6 +1346,7 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# decryption: "10min-mlkem768seed-bas64RawURLEncoding" # 同时允许 1-RTT 模式与十分钟复用的 0-RTT 模式
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
@ -1364,7 +1375,7 @@ listeners:
after-bytes: 0 # 传输指定字节后开始限速
bytes-per-sec: 0 # 基准速率(字节/秒)
burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
### 注意对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
### 注意对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “decryption” 的其中一项 ###
- name: anytls-in-1
type: anytls

2
go.mod
View File

@ -35,7 +35,7 @@ require (
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.8.0
github.com/metacubex/utls v1.8.1-0.20250810142204-d0e55ab2e852
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0

2
go.sum
View File

@ -141,6 +141,8 @@ github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nU
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac=
github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
github.com/metacubex/utls v1.8.1-0.20250810142204-d0e55ab2e852 h1:MLHUGmASNH7/AeoGmSrVM2RutRZAqIDSbQWBp0P7ItE=
github.com/metacubex/utls v1.8.1-0.20250810142204-d0e55ab2e852/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=

View File

@ -17,6 +17,7 @@ type VlessServer struct {
Enable bool
Listen string
Users []VlessUser
Decryption string
WsPath string
GrpcServiceName string
Certificate string

View File

@ -12,6 +12,7 @@ import (
type VlessOption struct {
BaseOption
Users []VlessUser `inbound:"users"`
Decryption string `inbound:"decryption,omitempty"`
WsPath string `inbound:"ws-path,omitempty"`
GrpcServiceName string `inbound:"grpc-service-name,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
@ -58,6 +59,7 @@ func NewVless(options *VlessOption) (*Vless, error) {
Enable: true,
Listen: base.RawAddress(),
Users: users,
Decryption: options.Decryption,
WsPath: options.WsPath,
GrpcServiceName: options.GrpcServiceName,
Certificate: options.Certificate,

View File

@ -7,6 +7,7 @@ import (
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/stretchr/testify/assert"
)
@ -87,6 +88,21 @@ func TestInboundVless_TLS(t *testing.T) {
})
}
func TestInboundVless_Encryption(t *testing.T) {
seedBase64, pubBase64, err := encryption.GenMLKEM768("")
if err != nil {
t.Fatal(err)
return
}
inboundOptions := inbound.VlessOption{
Decryption: "10min-mlkem768seed-" + seedBase64,
}
outboundOptions := outbound.VlessOption{
Encryption: "8min-mlkem768client-" + pubBase64,
}
testInboundVless(t, inboundOptions, outboundOptions)
}
func TestInboundVless_Wss1(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,

View File

@ -2,11 +2,15 @@ package sing_vless
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter/inbound"
@ -19,6 +23,7 @@ import (
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/vless/encryption"
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/sing-vmess/vless"
@ -45,10 +50,11 @@ func init() {
}
type Listener struct {
closed bool
config LC.VlessServer
listeners []net.Listener
service *vless.Service[string]
closed bool
config LC.VlessServer
listeners []net.Listener
service *vless.Service[string]
decryption *encryption.ServerInstance
}
func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
@ -80,7 +86,34 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
return it.Flow
}))
sl = &Listener{false, config, nil, service}
sl = &Listener{config: config, service: service}
if s := strings.Split(config.Decryption, "-mlkem768seed-"); len(s) == 2 {
var minutes uint32
if s[0] != "1rtt" {
t := strings.TrimSuffix(s[0], "min")
if t == s[0] {
return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption)
}
i, err := strconv.Atoi(t)
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption)
}
minutes = uint32(i)
}
b, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption)
}
if len(b) == 64 {
sl.decryption = &encryption.ServerInstance{}
if err = sl.decryption.Init(b, time.Duration(minutes)*time.Minute); err != nil {
return nil, fmt.Errorf("failed to use mlkem768seed: %w", err)
}
} else {
return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption)
}
}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
@ -149,8 +182,8 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
} else {
l = tlsC.NewListener(l, tlsConfig)
}
} else {
return nil, errors.New("disallow using Vless without both certificates/reality config")
} else if sl.decryption == nil {
return nil, errors.New("disallow using Vless without any certificates/reality/decryption config")
}
sl.listeners = append(sl.listeners, l)
@ -201,6 +234,13 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
ctx := sing.WithAdditions(context.TODO(), additions...)
if l.decryption != nil {
var err error
conn, err = l.decryption.Handshake(conn)
if err != nil {
return
}
}
err := l.service.NewConnection(ctx, conn, metadata.Metadata{
Protocol: "vless",
Source: metadata.SocksaddrFromNet(conn.RemoteAddr()),

View File

@ -0,0 +1,238 @@
package encryption
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"io"
"net"
"runtime"
"sync"
"time"
"github.com/metacubex/utls/mlkem"
"golang.org/x/crypto/hkdf"
"golang.org/x/sys/cpu"
)
var (
// Keep in sync with crypto/tls/cipher_suites.go.
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
HasAESGCMHardwareSupport = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)
var ClientCipher byte
func init() {
if !HasAESGCMHardwareSupport {
ClientCipher = 1
}
}
type ClientInstance struct {
sync.RWMutex
eKeyNfs *mlkem.EncapsulationKey768
minutes time.Duration
expire time.Time
baseKey []byte
reuse []byte
}
type ClientConn struct {
net.Conn
instance *ClientInstance
baseKey []byte
reuse []byte
random []byte
aead cipher.AEAD
nonce []byte
peerAead cipher.AEAD
peerNonce []byte
peerCache []byte
}
func (i *ClientInstance) Init(eKeyNfsData []byte, minutes time.Duration) (err error) {
i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData)
i.minutes = minutes
return
}
func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) {
if i.eKeyNfs == nil {
return nil, errors.New("uninitialized")
}
c := &ClientConn{Conn: conn}
if i.minutes > 0 {
i.RLock()
if time.Now().Before(i.expire) {
c.instance = i
c.baseKey = i.baseKey
c.reuse = i.reuse
i.RUnlock()
return c, nil
}
i.RUnlock()
}
nfsKey, encapsulatedNfsKey := i.eKeyNfs.Encapsulate()
seed := make([]byte, 64)
rand.Read(seed)
dKeyPfs, _ := mlkem.NewDecapsulationKey768(seed)
eKeyPfs := dKeyPfs.EncapsulationKey().Bytes()
padding := randBetween(100, 1000)
clientHello := make([]byte, 1088+1184+1+5+padding)
copy(clientHello, encapsulatedNfsKey)
copy(clientHello[1088:], eKeyPfs)
clientHello[2272] = ClientCipher
encodeHeader(clientHello[2273:], int(padding))
if _, err := c.Conn.Write(clientHello); err != nil {
return nil, err
}
// we can send more padding if needed
peerServerHello := make([]byte, 1088+21)
if _, err := io.ReadFull(c.Conn, peerServerHello); err != nil {
return nil, err
}
encapsulatedPfsKey := peerServerHello[:1088]
c.reuse = peerServerHello[1088:]
pfsKey, err := dKeyPfs.Decapsulate(encapsulatedPfsKey)
if err != nil {
return nil, err
}
c.baseKey = append(nfsKey, pfsKey...)
authKey := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfs).Read(authKey)
nonce := make([]byte, 12)
VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce, c.reuse, encapsulatedPfsKey)
if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more message
return nil, errors.New("invalid server")
}
if i.minutes > 0 {
i.Lock()
i.expire = time.Now().Add(i.minutes)
i.baseKey = c.baseKey
i.reuse = c.reuse
i.Unlock()
}
return c, nil
}
func (c *ClientConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
var data []byte
if c.aead == nil {
c.random = make([]byte, 32)
rand.Read(c.random)
key := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, c.random, c.reuse).Read(key)
c.aead = newAead(ClientCipher, key)
c.nonce = make([]byte, 12)
data = make([]byte, 21+32+5+len(b)+16)
copy(data, c.reuse)
copy(data[21:], c.random)
encodeHeader(data[53:], len(b)+16)
c.aead.Seal(data[:58], c.nonce, b, data[53:58])
} else {
data = make([]byte, 5+len(b)+16)
encodeHeader(data, len(b)+16)
c.aead.Seal(data[:5], c.nonce, b, data[:5])
}
increaseNonce(c.nonce)
if _, err := c.Conn.Write(data); err != nil {
return 0, err
}
return len(b), nil
}
func (c *ClientConn) Read(b []byte) (int, error) { // after first Write()
if len(b) == 0 {
return 0, nil
}
peerHeader := make([]byte, 5)
if c.peerAead == nil {
if c.instance == nil {
for {
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return 0, err
}
peerPadding, _ := decodeHeader(peerHeader)
if peerPadding == 0 {
break
}
if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil {
return 0, err
}
}
} else {
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return 0, err
}
}
peerRandom := make([]byte, 32)
copy(peerRandom, peerHeader)
if _, err := io.ReadFull(c.Conn, peerRandom[5:]); err != nil {
return 0, err
}
if c.random == nil {
return 0, errors.New("can not Read() first")
}
peerKey := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, peerRandom, c.random).Read(peerKey)
c.peerAead = newAead(ClientCipher, peerKey)
c.peerNonce = make([]byte, 12)
}
if len(c.peerCache) != 0 {
n := copy(b, c.peerCache)
c.peerCache = c.peerCache[n:]
return n, nil
}
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return 0, err
}
peerLength, err := decodeHeader(peerHeader) // 17~17000
if err != nil {
if c.instance != nil {
c.instance.Lock()
if bytes.Equal(c.reuse, c.instance.reuse) {
c.instance.expire = time.Now() // expired
}
c.instance.Unlock()
}
return 0, err
}
peerData := make([]byte, peerLength)
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:peerLength-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // max=8192 is recommended for peer
}
_, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader)
increaseNonce(c.peerNonce)
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.peerCache = dst[copy(b, dst):]
dst = b // for len(dst)
}
return len(dst), nil
}

View File

@ -0,0 +1,65 @@
package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"math/big"
"strconv"
"golang.org/x/crypto/chacha20poly1305"
)
func encodeHeader(b []byte, l int) {
b[0] = 23
b[1] = 3
b[2] = 3
b[3] = byte(l >> 8)
b[4] = byte(l)
}
func decodeHeader(b []byte) (int, error) {
if b[0] == 23 && b[1] == 3 && b[2] == 3 {
l := int(b[3])<<8 | int(b[4])
if l < 17 || l > 17000 { // TODO
return 0, errors.New("invalid length in record's header: " + strconv.Itoa(l))
}
return l, nil
}
return 0, errors.New("invalid record's header")
}
func newAead(c byte, k []byte) cipher.AEAD {
switch c {
case 0:
if block, err := aes.NewCipher(k); err == nil {
aead, _ := cipher.NewGCM(block)
return aead
}
case 1:
aead, _ := chacha20poly1305.New(k)
return aead
}
return nil
}
func increaseNonce(nonce []byte) {
for i := 0; i < 12; i++ {
nonce[11-i]++
if nonce[11-i] != 0 {
break
}
if i == 11 {
// TODO
}
}
}
func randBetween(from int64, to int64) int64 {
if from == to {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@ -0,0 +1,3 @@
// Package encryption copy and modify from xray-core
// https://github.com/XTLS/Xray-core/commit/f61c14e9c63dc41a8a09135db3aea337974f3f37
package encryption

View File

@ -0,0 +1,32 @@
package encryption
import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/metacubex/utls/mlkem"
)
func GenMLKEM768(seedStr string) (seedBase64, pubBase64 string, err error) {
var seed [64]byte
if len(seedStr) > 0 {
s, _ := base64.RawURLEncoding.DecodeString(seedStr)
if len(s) != 64 {
err = fmt.Errorf("invalid length of ML-KEM-768 seed: %s", seedStr)
return
}
seed = [64]byte(s)
} else {
_, err = rand.Read(seed[:])
if err != nil {
return
}
}
key, _ := mlkem.NewDecapsulationKey768(seed[:])
pub := key.EncapsulationKey()
seedBase64 = base64.RawURLEncoding.EncodeToString(seed[:])
pubBase64 = base64.RawURLEncoding.EncodeToString(pub.Bytes())
return
}

View File

@ -0,0 +1,250 @@
package encryption
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"io"
"net"
"sync"
"time"
"github.com/metacubex/utls/mlkem"
"golang.org/x/crypto/hkdf"
)
type ServerSession struct {
expire time.Time
cipher byte
baseKey []byte
randoms sync.Map
}
type ServerInstance struct {
sync.RWMutex
dKeyNfs *mlkem.DecapsulationKey768
minutes time.Duration
sessions map[[21]byte]*ServerSession
}
type ServerConn struct {
net.Conn
cipher byte
baseKey []byte
reuse []byte
peerRandom []byte
peerAead cipher.AEAD
peerNonce []byte
peerCache []byte
aead cipher.AEAD
nonce []byte
}
func (i *ServerInstance) Init(dKeyNfsData []byte, minutes time.Duration) (err error) {
i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData)
if minutes > 0 {
i.minutes = minutes
i.sessions = make(map[[21]byte]*ServerSession)
go func() {
for {
time.Sleep(time.Minute)
now := time.Now()
i.Lock()
for index, session := range i.sessions {
if now.After(session.expire) {
delete(i.sessions, index)
}
}
i.Unlock()
}
}()
}
return
}
func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) {
if i.dKeyNfs == nil {
return nil, errors.New("uninitialized")
}
c := &ServerConn{Conn: conn}
peerReuseHello := make([]byte, 21+32)
if _, err := io.ReadFull(c.Conn, peerReuseHello); err != nil {
return nil, err
}
if i.minutes > 0 {
i.RLock()
s := i.sessions[[21]byte(peerReuseHello)]
i.RUnlock()
if s != nil {
if _, replay := s.randoms.LoadOrStore([32]byte(peerReuseHello[21:]), true); !replay {
c.cipher = s.cipher
c.baseKey = s.baseKey
c.reuse = peerReuseHello[:21]
c.peerRandom = peerReuseHello[21:]
return c, nil
}
}
}
peerHeader := make([]byte, 5)
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return nil, err
}
if l, _ := decodeHeader(peerHeader); l != 0 {
c.Conn.Write(make([]byte, randBetween(100, 1000))) // make client do new handshake
return nil, errors.New("invalid reuse")
}
peerClientHello := make([]byte, 1088+1184+1)
copy(peerClientHello, peerReuseHello)
copy(peerClientHello[53:], peerHeader)
if _, err := io.ReadFull(c.Conn, peerClientHello[58:]); err != nil {
return nil, err
}
encapsulatedNfsKey := peerClientHello[:1088]
eKeyPfsData := peerClientHello[1088:2272]
c.cipher = peerClientHello[2272]
if c.cipher != 0 && c.cipher != 1 {
return nil, errors.New("invalid cipher")
}
nfsKey, err := i.dKeyNfs.Decapsulate(encapsulatedNfsKey)
if err != nil {
return nil, err
}
eKeyPfs, err := mlkem.NewEncapsulationKey768(eKeyPfsData)
if err != nil {
return nil, err
}
pfsKey, encapsulatedPfsKey := eKeyPfs.Encapsulate()
c.baseKey = append(nfsKey, pfsKey...)
authKey := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfsData).Read(authKey)
nonce := make([]byte, 12)
c.reuse = newAead(c.cipher, authKey).Seal(nil, nonce, []byte("VLESS"), encapsulatedPfsKey)
padding := randBetween(100, 1000)
serverHello := make([]byte, 1088+21+5+padding)
copy(serverHello, encapsulatedPfsKey)
copy(serverHello[1088:], c.reuse)
encodeHeader(serverHello[1109:], int(padding))
if _, err := c.Conn.Write(serverHello); err != nil {
return nil, err
}
if i.minutes > 0 {
i.Lock()
i.sessions[[21]byte(c.reuse)] = &ServerSession{
expire: time.Now().Add(i.minutes),
cipher: c.cipher,
baseKey: c.baseKey,
}
i.Unlock()
}
return c, nil
}
func (c *ServerConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
peerHeader := make([]byte, 5)
if c.peerAead == nil {
if c.peerRandom == nil {
for {
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return 0, err
}
peerPadding, _ := decodeHeader(peerHeader)
if peerPadding == 0 {
break
}
if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil {
return 0, err
}
}
peerIndex := make([]byte, 21)
copy(peerIndex, peerHeader)
if _, err := io.ReadFull(c.Conn, peerIndex[5:]); err != nil {
return 0, err
}
if !bytes.Equal(peerIndex, c.reuse) {
return 0, errors.New("naughty boy")
}
c.peerRandom = make([]byte, 32)
if _, err := io.ReadFull(c.Conn, c.peerRandom); err != nil {
return 0, err
}
}
peerKey := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, c.peerRandom, c.reuse).Read(peerKey)
c.peerAead = newAead(c.cipher, peerKey)
c.peerNonce = make([]byte, 12)
}
if len(c.peerCache) != 0 {
n := copy(b, c.peerCache)
c.peerCache = c.peerCache[n:]
return n, nil
}
if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
return 0, err
}
peerLength, err := decodeHeader(peerHeader) // 17~17000
if err != nil {
return 0, err
}
peerData := make([]byte, peerLength)
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:peerLength-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // max=8192 is recommended for peer
}
_, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader)
increaseNonce(c.peerNonce)
if err != nil {
return 0, errors.New("error")
}
if len(dst) > len(b) {
c.peerCache = dst[copy(b, dst):]
dst = b // for len(dst)
}
return len(dst), nil
}
func (c *ServerConn) Write(b []byte) (int, error) { // after first Read()
if len(b) == 0 {
return 0, nil
}
var data []byte
if c.aead == nil {
if c.peerRandom == nil {
return 0, errors.New("can not Write() first")
}
data = make([]byte, 32+5+len(b)+16)
rand.Read(data[:32])
key := make([]byte, 32)
hkdf.New(sha256.New, c.baseKey, data[:32], c.peerRandom).Read(key)
c.aead = newAead(c.cipher, key)
c.nonce = make([]byte, 12)
encodeHeader(data[32:], len(b)+16)
c.aead.Seal(data[:37], c.nonce, b, data[32:37])
} else {
data = make([]byte, 5+len(b)+16)
encodeHeader(data, len(b)+16)
c.aead.Seal(data[:5], c.nonce, b, data[:5])
}
increaseNonce(c.nonce)
if _, err := c.Conn.Write(data); err != nil {
return 0, err
}
return len(b), nil
}