From 1b0c72bfabfee44c88afca5077584d999af3cf6f Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 10 Aug 2025 22:16:25 +0800 Subject: [PATCH] feat: support vless encryption --- adapter/outbound/vless.go | 44 +++++ component/generater/cmd.go | 14 +- docs/config.yaml | 13 +- go.mod | 2 +- go.sum | 2 + listener/config/vless.go | 1 + listener/inbound/vless.go | 2 + listener/inbound/vless_test.go | 16 ++ listener/sing_vless/server.go | 54 +++++- transport/vless/encryption/client.go | 238 +++++++++++++++++++++++++ transport/vless/encryption/common.go | 65 +++++++ transport/vless/encryption/doc.go | 3 + transport/vless/encryption/key.go | 32 ++++ transport/vless/encryption/server.go | 250 +++++++++++++++++++++++++++ 14 files changed, 726 insertions(+), 10 deletions(-) create mode 100644 transport/vless/encryption/client.go create mode 100644 transport/vless/encryption/common.go create mode 100644 transport/vless/encryption/doc.go create mode 100644 transport/vless/encryption/key.go create mode 100644 transport/vless/encryption/server.go diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index decc32f1..47128541 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -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 diff --git a/component/generater/cmd.go b/component/generater/cmd.go index 96e62d7c..9df56a5a 100644 --- a/component/generater/cmd.go +++ b/component/generater/cmd.go @@ -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) } } diff --git a/docs/config.yaml b/docs/config.yaml index d458d60d..34af4a02 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -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 diff --git a/go.mod b/go.mod index 154dbb35..11d10c9a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e9a4a11d..2adede6c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/listener/config/vless.go b/listener/config/vless.go index 52ee3754..135747da 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -17,6 +17,7 @@ type VlessServer struct { Enable bool Listen string Users []VlessUser + Decryption string WsPath string GrpcServiceName string Certificate string diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 947e3a53..305df50a 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -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, diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index 1bec3d55..fdb8905c 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -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, diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 16aa1c65..06358824 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -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()), diff --git a/transport/vless/encryption/client.go b/transport/vless/encryption/client.go new file mode 100644 index 00000000..b0b44857 --- /dev/null +++ b/transport/vless/encryption/client.go @@ -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 +} diff --git a/transport/vless/encryption/common.go b/transport/vless/encryption/common.go new file mode 100644 index 00000000..6b02f54b --- /dev/null +++ b/transport/vless/encryption/common.go @@ -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() +} diff --git a/transport/vless/encryption/doc.go b/transport/vless/encryption/doc.go new file mode 100644 index 00000000..dbb00211 --- /dev/null +++ b/transport/vless/encryption/doc.go @@ -0,0 +1,3 @@ +// Package encryption copy and modify from xray-core +// https://github.com/XTLS/Xray-core/commit/f61c14e9c63dc41a8a09135db3aea337974f3f37 +package encryption diff --git a/transport/vless/encryption/key.go b/transport/vless/encryption/key.go new file mode 100644 index 00000000..46b53163 --- /dev/null +++ b/transport/vless/encryption/key.go @@ -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 +} diff --git a/transport/vless/encryption/server.go b/transport/vless/encryption/server.go new file mode 100644 index 00000000..91ec488f --- /dev/null +++ b/transport/vless/encryption/server.go @@ -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 +}