diff --git a/docs/config.yaml b/docs/config.yaml index 8f3333c7..7a41f00c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -641,8 +641,14 @@ proxies: # socks5 # vless encryption客户端配置: # (native/xorpub 的 XTLS Vision 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用) # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 + # + # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "111-1111.111--66.3333--1234",它的含义是: + # 在 1-RTT client/server hello 后粘上随机 111 到 1111 字节的 padding + # 等待随机 111 到负 66 毫秒,若随机到了负值则不等待 + # 再次发送随机 3333 到负 1234 字节的 padding,若随机到了负值则不发送 + # 服务端、客户端可以设置不同的 padding 参数,正数写在左边,按 len、gap 的顺序无限串联,第一个 padding 需大于 16 字节 # ------------------------- - encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(X25519 Password).(ML-KEM-768 Client)..." + encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(padding len).(padding gap).(X25519 Password).(ML-KEM-768 Client)..." tls: false #可以不开启tls udp: true @@ -1366,8 +1372,14 @@ listeners: # vless encryption服务端配置: # (原生外观 / 只 XOR 公钥 / 全随机数。只允许 1-RTT 模式 / 同时允许 1-RTT 模式与 600 秒复用的 0-RTT 模式) # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 + # + # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "111-1111.111--66.3333--1234",它的含义是: + # 在 1-RTT client/server hello 后粘上随机 111 到 1111 字节的 padding + # 等待随机 111 到负 66 毫秒,若随机到了负值则不等待 + # 再次发送随机 3333 到负 1234 字节的 padding,若随机到了负值则不发送 + # 服务端、客户端可以设置不同的 padding 参数,正数写在左边,按 len、gap 的顺序无限串联,第一个 padding 需大于 16 字节 # ------------------------- - # decryption: "mlkem768x25519plus.native/xorpub/random.1rtt/600s.(X25519 PrivateKey).(ML-KEM-768 Seed)..." + # decryption: "mlkem768x25519plus.native/xorpub/random.1rtt/600s.(padding len).(padding gap).(X25519 PrivateKey).(ML-KEM-768 Seed)..." # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index 9a7fed59..e7dabee4 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -99,6 +99,15 @@ func TestInboundVless_Encryption(t *testing.T) { t.Fatal(err) return } + paddings := []struct { + name string + data string + }{ + {"unconfigured-padding", ""}, + {"default-padding", "111-1111.111--66.3333--1234."}, + {"old-padding", "100-1000."}, // Xray-core v25.8.29 + {"custom-padding", "7890-1234.1111--999.6666--3333.777--777."}, + } var modes = []string{ "native", "xorpub", @@ -107,19 +116,26 @@ func TestInboundVless_Encryption(t *testing.T) { for i := range modes { mode := modes[i] t.Run(mode, func(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Decryption: "mlkem768x25519plus." + mode + ".600s." + privateKeyBase64 + "." + seedBase64, + t.Parallel() + for i := range paddings { + padding := paddings[i].data + t.Run(paddings[i].name, func(t *testing.T) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus." + mode + ".600s." + padding + privateKeyBase64 + "." + seedBase64, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus." + mode + ".0rtt." + padding + passwordBase64 + "." + clientBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) } - outboundOptions := outbound.VlessOption{ - Encryption: "mlkem768x25519plus." + mode + ".0rtt." + passwordBase64 + "." + clientBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) }) + } } diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 5280aa13..3a5943b5 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -230,7 +230,7 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou ctx := sing.WithAdditions(context.TODO(), additions...) if l.decryption != nil { var err error - conn, err = l.decryption.Handshake(conn) + conn, err = l.decryption.Handshake(conn, nil) if err != nil { return } diff --git a/transport/vless/encryption/client.go b/transport/vless/encryption/client.go index ee73b63d..c7f4e256 100644 --- a/transport/vless/encryption/client.go +++ b/transport/vless/encryption/client.go @@ -33,6 +33,8 @@ type ClientInstance struct { RelaysLength int XorMode uint32 Seconds uint32 + PaddingLens [][2]int + PaddingGaps [][2]int RWLock sync.RWMutex Expire time.Time @@ -40,15 +42,13 @@ type ClientInstance struct { Ticket []byte } -func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) { +func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) { if i.NfsPKeys != nil { - err = errors.New("already initialized") - return + return errors.New("already initialized") } l := len(nfsPKeysBytes) if l == 0 { - err = errors.New("empty nfsPKeysBytes") - return + return errors.New("empty nfsPKeysBytes") } i.NfsPKeys = make([]any, l) i.NfsPKeysBytes = nfsPKeysBytes @@ -70,7 +70,7 @@ func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) ( i.RelaysLength -= 32 i.XorMode = xorMode i.Seconds = seconds - return + return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) } func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { @@ -81,7 +81,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { ivAndRealysLength := 16 + i.RelaysLength pfsKeyExchangeLength := 18 + 1184 + 32 + 16 - paddingLength := int(randBetween(100, 1000)) + paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength) iv := clientHello[:16] @@ -150,10 +150,18 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) - if _, err := conn.Write(clientHello); err != nil { - return nil, err + 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]) + } } - // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control encryptedPfsPublicKey := make([]byte, 1088+32+16) if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { diff --git a/transport/vless/encryption/common.go b/transport/vless/encryption/common.go index 07ea57c3..e74af737 100644 --- a/transport/vless/encryption/common.go +++ b/transport/vless/encryption/common.go @@ -8,6 +8,8 @@ import ( "fmt" "io" "net" + "strconv" + "strings" "time" "github.com/metacubex/mihomo/common/pool" @@ -96,11 +98,11 @@ func (c *CommonConn) Read(b []byte) (int, error) { if c.input.Len() > 0 { return c.input.Read(b) } - peerHeader := make([]byte, 5) - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + peerHeader := [5]byte{} + if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil { return 0, err } - l, err := DecodeHeader(peerHeader) // l: 17~17000 + 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() @@ -113,7 +115,9 @@ func (c *CommonConn) Read(b []byte) (int, error) { return 0, err } c.Client = nil - c.rawInput.Grow(l) + 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 @@ -124,9 +128,9 @@ func (c *CommonConn) Read(b []byte) (int, error) { } var newAEAD *AEAD if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) { - newAEAD = NewAEAD(append(peerHeader, peerData...), c.UnitedKey, c.UseAES) + newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES) } - _, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader) + _, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:]) if newAEAD != nil { c.PeerAEAD = newAEAD } @@ -213,9 +217,69 @@ func DecodeHeader(h []byte) (l int, err error) { return } +func ParsePadding(padding string, paddingLens, paddingGaps *[][2]int) (err error) { + if padding == "" { + return + } + maxLen := 0 + for i, s := range strings.Split(padding, ".") { + x := strings.SplitN(s, "-", 2) + if len(x) != 2 || x[0] == "" || x[1] == "" { + return errors.New("invalid padding lenth/gap parameter: " + s) + } + y := [2]int{} + if y[0], err = strconv.Atoi(x[0]); err != nil { + return + } + if y[1], err = strconv.Atoi(x[1]); err != nil { + return + } + if i == 0 && (y[0] < 17 || y[1] < 17) { + return errors.New("first padding length must be larger than 16") + } + if i%2 == 0 { + *paddingLens = append(*paddingLens, y) + maxLen += max(y[0], y[1]) + } else { + *paddingGaps = append(*paddingGaps, y) + } + } + if maxLen > 65535 { + return errors.New("total padding length must be smaller than 65536") + } + return +} + +func CreatPadding(paddingLens, paddingGaps [][2]int) (length int, lens []int, gaps []time.Duration) { + if len(paddingLens) == 0 { + paddingLens = [][2]int{{111, 1111}, {3333, -1234}} + paddingGaps = [][2]int{{111, -66}} + } + for _, l := range paddingLens { + lens = append(lens, int(max(0, randBetween(int64(l[0]), int64(l[1]))))) + length += lens[len(lens)-1] + } + for _, g := range paddingGaps { + gaps = append(gaps, time.Duration(max(0, randBetween(int64(g[0]), int64(g[1]))))*time.Millisecond) + } + return +} + +func max[T ~int | ~uint | ~int64 | ~uint64 | ~int32 | ~uint32 | ~int16 | ~uint16 | ~int8 | ~uint8](a, b T) T { + if a > b { + return a + } + return b +} + func randBetween(from int64, to int64) int64 { if from == to { return from } + + if to < from { + from, to = to, from + } + return from + randv2.Int64N(to-from) } diff --git a/transport/vless/encryption/doc.go b/transport/vless/encryption/doc.go index a9462a83..c9a92c34 100644 --- a/transport/vless/encryption/doc.go +++ b/transport/vless/encryption/doc.go @@ -22,4 +22,5 @@ // https://github.com/XTLS/Xray-core/commit/fce1195b60f48ca18a953dbd5c7d991869de9a5e // https://github.com/XTLS/Xray-core/commit/b0b220985c9c1bc832665458d5fd6e0c287b67ae // https://github.com/XTLS/Xray-core/commit/82ea7a3cc5ff23280b87e3052f0f83b04f0267fa +// https://github.com/XTLS/Xray-core/commit/e8b02cd6649f14889841e8ab8ee6b2acca71dbe6 package encryption diff --git a/transport/vless/encryption/factory.go b/transport/vless/encryption/factory.go index c344fbf1..246f49dd 100644 --- a/transport/vless/encryption/factory.go +++ b/transport/vless/encryption/factory.go @@ -34,7 +34,12 @@ func NewClient(encryption string) (*ClientInstance, error) { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } var nfsPKeysBytes [][]byte + var paddings []string for _, r := range s[3:] { + if len(r) < 20 { + paddings = append(paddings, r) + continue + } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) @@ -44,8 +49,9 @@ func NewClient(encryption string) (*ClientInstance, error) { } nfsPKeysBytes = append(nfsPKeysBytes, b) } + padding := strings.Join(paddings, ".") client := &ClientInstance{} - if err := client.Init(nfsPKeysBytes, xorMode, seconds); err != nil { + if err := client.Init(nfsPKeysBytes, xorMode, seconds, padding); err != nil { return nil, fmt.Errorf("failed to use encryption: %w", err) } return client, nil @@ -84,7 +90,12 @@ func NewServer(decryption string) (*ServerInstance, error) { seconds = uint32(i) } var nfsSKeysBytes [][]byte + var paddings []string for _, r := range s[3:] { + if len(r) < 20 { + paddings = append(paddings, r) + continue + } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) @@ -94,8 +105,9 @@ func NewServer(decryption string) (*ServerInstance, error) { } nfsSKeysBytes = append(nfsSKeysBytes, b) } + padding := strings.Join(paddings, ".") server := &ServerInstance{} - if err := server.Init(nfsSKeysBytes, xorMode, seconds); err != nil { + if err := server.Init(nfsSKeysBytes, xorMode, seconds, padding); err != nil { return nil, fmt.Errorf("failed to use decryption: %w", err) } return server, nil diff --git a/transport/vless/encryption/server.go b/transport/vless/encryption/server.go index 0dc6da15..05179aa8 100644 --- a/transport/vless/encryption/server.go +++ b/transport/vless/encryption/server.go @@ -29,21 +29,21 @@ type ServerInstance struct { RelaysLength int XorMode uint32 Seconds uint32 + PaddingLens [][2]int + PaddingGaps [][2]int RWLock sync.RWMutex Sessions map[[16]byte]*ServerSession Closed bool } -func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) { +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) { if i.NfsSKeys != nil { - err = errors.New("already initialized") - return + return errors.New("already initialized") } l := len(nfsSKeysBytes) if l == 0 { - err = errors.New("empty nfsSKeysBytes") - return + return errors.New("empty nfsSKeysBytes") } i.NfsSKeys = make([]any, l) i.NfsPKeysBytes = make([][]byte, l) @@ -87,7 +87,7 @@ func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) ( } }() } - return + return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) } func (i *ServerInstance) Close() (err error) { @@ -97,7 +97,7 @@ func (i *ServerInstance) Close() (err error) { return } -func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { +func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) { if i.NfsSKeys == nil { return nil, errors.New("uninitialized") } @@ -107,6 +107,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { 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 @@ -156,6 +159,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { 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 @@ -164,6 +170,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { return nil, err } } + if fallback != nil { + *fallback = nil + } length := DecodeLength(decryptedLength) if length == 32 { @@ -182,7 +191,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { s := i.Sessions[[16]byte(ticket)] i.RWLock.RUnlock() if s == nil { - noises := make([]byte, randBetween(1268, 2268)) // matches 1-RTT's server hello length for "random", though it is not important, just for example + noises := make([]byte, 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) @@ -236,25 +245,10 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { c.UnitedKey = append(pfsKey, nfsKey...) c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES) + ticket := make([]byte, 16) rand.Read(ticket) copy(ticket, EncodeLength(int(i.Seconds*4/5))) - - pfsKeyExchangeLength := 1088 + 32 + 16 - encryptedTicketLength := 32 - paddingLength := int(randBetween(100, 1000)) - 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) - - if _, err := conn.Write(serverHello); err != nil { - return nil, err - } - // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control - if i.Seconds > 0 { i.RWLock.Lock() i.Sessions[[16]byte(ticket)] = &ServerSession{ @@ -264,6 +258,29 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { i.RWLock.Unlock() } + pfsKeyExchangeLength := 1088 + 32 + 16 + encryptedTicketLength := 32 + paddingLength, paddingLens, paddingGaps := CreatPadding(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