mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-19 08:20:05 +08:00
feat: support vless encryption
This commit is contained in:
parent
e89af723cd
commit
1b0c72bfab
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
2
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@ -17,6 +17,7 @@ type VlessServer struct {
|
||||
Enable bool
|
||||
Listen string
|
||||
Users []VlessUser
|
||||
Decryption string
|
||||
WsPath string
|
||||
GrpcServiceName string
|
||||
Certificate string
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()),
|
||||
|
||||
238
transport/vless/encryption/client.go
Normal file
238
transport/vless/encryption/client.go
Normal 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
|
||||
}
|
||||
65
transport/vless/encryption/common.go
Normal file
65
transport/vless/encryption/common.go
Normal 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()
|
||||
}
|
||||
3
transport/vless/encryption/doc.go
Normal file
3
transport/vless/encryption/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package encryption copy and modify from xray-core
|
||||
// https://github.com/XTLS/Xray-core/commit/f61c14e9c63dc41a8a09135db3aea337974f3f37
|
||||
package encryption
|
||||
32
transport/vless/encryption/key.go
Normal file
32
transport/vless/encryption/key.go
Normal 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
|
||||
}
|
||||
250
transport/vless/encryption/server.go
Normal file
250
transport/vless/encryption/server.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user