mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-22 01:04:24 +00:00
Packets of one client session can be processed by two inbound workers at the same time (e.g. the client's old and new source address after a NAT rebinding). The sliding window was checked and updated in two separate critical sections, and clientBodyCipher and the session's User were read without the lock, so the same packet ID could be accepted more than once and go test -race reported data races. Re-check the window and publish the cached body cipher in one critical section after the packet is fully validated, and always initialize the session's User under the lock.
556 lines
16 KiB
Go
556 lines
16 KiB
Go
package shadowsocks_2022_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"io"
|
|
gonet "net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/xtls/xray-core/common"
|
|
"github.com/xtls/xray-core/common/buf"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/protocol"
|
|
"github.com/xtls/xray-core/common/serial"
|
|
"github.com/xtls/xray-core/common/session"
|
|
"github.com/xtls/xray-core/core"
|
|
. "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
|
)
|
|
|
|
func newTestContext() context.Context {
|
|
v, err := core.New(&core.Config{})
|
|
common.Must(err)
|
|
ctx := context.WithValue(context.Background(), core.XrayKey(1), v)
|
|
ctx = session.ContextWithInbound(ctx, &session.Inbound{})
|
|
return ctx
|
|
}
|
|
|
|
func generateRandomKey(size int) string {
|
|
b := make([]byte, size)
|
|
_, _ = rand.Read(b)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
func TestKDF(t *testing.T) {
|
|
// Test ParseKey
|
|
if _, err := ParseKey("", 16); err != ErrBadKey {
|
|
t.Fatalf("expected ErrBadKey for empty key, got %v", err)
|
|
}
|
|
|
|
shortKey := base64.StdEncoding.EncodeToString([]byte("short"))
|
|
if _, err := ParseKey(shortKey, 16); err != ErrBadKey {
|
|
t.Fatalf("expected ErrBadKey for short key, got %v", err)
|
|
}
|
|
|
|
exactKey := []byte("0123456789abcdef")
|
|
exactKeyB64 := base64.StdEncoding.EncodeToString(exactKey)
|
|
normExact, err := ParseKey(exactKeyB64, 16)
|
|
if err != nil || !bytes.Equal(normExact, exactKey) {
|
|
t.Fatalf("unexpected parsed exact key: %v, err: %v", normExact, err)
|
|
}
|
|
|
|
longKey := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef_longer_key_for_testing"))
|
|
if _, err := ParseKey(longKey, 16); err != ErrBadKey {
|
|
t.Fatalf("expected ErrBadKey for long key, got %v", err)
|
|
}
|
|
|
|
// Test Session Subkey determinism
|
|
salt := []byte("random_salt_1234")
|
|
k1 := DeriveSessionSubKey(normExact, salt, 16)
|
|
k2 := DeriveSessionSubKey(normExact, salt, 16)
|
|
if !bytes.Equal(k1, k2) {
|
|
t.Fatal("DeriveSessionSubKey should be deterministic")
|
|
}
|
|
|
|
// Identity subkey must differ from session subkey with same inputs
|
|
idKey := DeriveIdentitySubKey(normExact, salt, 16)
|
|
if bytes.Equal(k1, idKey) {
|
|
t.Fatal("DeriveIdentitySubKey must differ from DeriveSessionSubKey")
|
|
}
|
|
|
|
// User PSK hash
|
|
h1 := DeriveUserPSKHash(normExact)
|
|
h2 := DeriveUserPSKHash(normExact)
|
|
if h1 != h2 {
|
|
t.Fatal("DeriveUserPSKHash should be deterministic")
|
|
}
|
|
}
|
|
|
|
func TestSlidingWindow(t *testing.T) {
|
|
var window SlidingWindow
|
|
if !window.Check(1) {
|
|
t.Fatal("packet 1 should be accepted")
|
|
}
|
|
window.Add(1)
|
|
|
|
if window.Check(1) {
|
|
t.Fatal("duplicate packet 1 should be rejected")
|
|
}
|
|
|
|
if !window.Check(100) {
|
|
t.Fatal("packet 100 should be accepted")
|
|
}
|
|
window.Add(100)
|
|
|
|
if window.Check(100) {
|
|
t.Fatal("duplicate packet 100 should be rejected")
|
|
}
|
|
|
|
if !window.Check(50) {
|
|
t.Fatal("out-of-order packet 50 within window should be accepted")
|
|
}
|
|
window.Add(50)
|
|
if window.Check(50) {
|
|
t.Fatal("duplicate packet 50 should be rejected")
|
|
}
|
|
|
|
// Check packet far behind window (> 8128)
|
|
window.Add(10000)
|
|
if window.Check(1) {
|
|
t.Fatal("packet 1 should be rejected as behind window")
|
|
}
|
|
}
|
|
|
|
func TestTCPStream(t *testing.T) {
|
|
methods := []struct {
|
|
name string
|
|
keySize int
|
|
}{
|
|
{MethodAES128GCM, 16},
|
|
{MethodAES256GCM, 32},
|
|
{MethodChaCha20Poly1305, 32},
|
|
}
|
|
|
|
dest := net.TCPDestination(net.LocalHostIP, net.Port(8080))
|
|
testPayload := []byte("Hello, Shadowsocks 2022 Native Implementation!")
|
|
|
|
for _, m := range methods {
|
|
t.Run(m.name, func(t *testing.T) {
|
|
rawKey := make([]byte, m.keySize)
|
|
_, _ = rand.Read(rawKey)
|
|
method, err := GetCipherMethod(m.name)
|
|
common.Must(err)
|
|
|
|
clientConn, serverConn := gonet.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
var receivedDest net.Destination
|
|
var receivedPayload []byte
|
|
|
|
// Server goroutine
|
|
go func() {
|
|
defer wg.Done()
|
|
salt := make([]byte, method.KeySaltLength)
|
|
_, err := io.ReadFull(serverConn, salt)
|
|
common.Must(err)
|
|
|
|
sessionKey := DeriveSessionSubKey(rawKey, salt, method.KeySaltLength)
|
|
aead, err := method.NewAEAD(sessionKey)
|
|
common.Must(err)
|
|
|
|
reader := NewStreamReader(serverConn, aead)
|
|
|
|
// Read fixed chunk (11 + 16 bytes)
|
|
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
|
_, err = io.ReadFull(serverConn, fixedBuf[:])
|
|
common.Must(err)
|
|
|
|
plainFixed, err := aead.Open(fixedBuf[:0], reader.Nonce(), fixedBuf[:], nil)
|
|
common.Must(err)
|
|
IncreaseNonce(reader.Nonce())
|
|
if plainFixed[0] != HeaderTypeClient {
|
|
t.Errorf("expected client header type, got %d", plainFixed[0])
|
|
}
|
|
|
|
// Read variable chunk
|
|
varLen := int(plainFixed[9])<<8 | int(plainFixed[10])
|
|
varBuf := make([]byte, varLen+AEADTagSize)
|
|
_, err = io.ReadFull(serverConn, varBuf)
|
|
common.Must(err)
|
|
|
|
plainVar, err := aead.Open(varBuf[:0], reader.Nonce(), varBuf, nil)
|
|
common.Must(err)
|
|
IncreaseNonce(reader.Nonce())
|
|
|
|
dest, addrLen, err := ParseAddressPort(plainVar)
|
|
common.Must(err)
|
|
receivedDest = net.TCPDestination(dest.Address, dest.Port)
|
|
plainVar = plainVar[addrLen:]
|
|
padLen := int(binary.BigEndian.Uint16(plainVar[:2]))
|
|
receivedPayload = plainVar[2+padLen:]
|
|
|
|
// Server sends response stream with receivedPayload as first payload
|
|
writer := NewServerStreamWriter(serverConn, method, rawKey, salt)
|
|
pBuf := buf.New()
|
|
pBuf.Write(receivedPayload)
|
|
_ = writer.WriteMultiBuffer(buf.MultiBuffer{pBuf})
|
|
|
|
// Read and echo additional stream data
|
|
mb, err := reader.ReadMultiBuffer()
|
|
common.Must(err)
|
|
_ = writer.WriteMultiBuffer(mb)
|
|
_ = writer.Close()
|
|
}()
|
|
|
|
// Client goroutine
|
|
go func() {
|
|
defer wg.Done()
|
|
clientSalt := make([]byte, method.KeySaltLength)
|
|
common.Must2(io.ReadFull(rand.Reader, clientSalt))
|
|
writer, err := WriteTCPRequest(clientConn, method, [][]byte{rawKey}, dest, clientSalt, testPayload)
|
|
common.Must(err)
|
|
|
|
reader, err := ReadTCPResponse(clientConn, method, rawKey, clientSalt)
|
|
common.Must(err)
|
|
|
|
// The first ReadMultiBuffer drains initialPayload from reader cache
|
|
mbInit, err := reader.ReadMultiBuffer()
|
|
common.Must(err)
|
|
if !bytes.Equal(mbInit[0].Bytes(), testPayload) {
|
|
t.Errorf("drained initial payload mismatch: got %s, want %s", mbInit[0].Bytes(), testPayload)
|
|
}
|
|
buf.ReleaseMulti(mbInit)
|
|
|
|
// Send additional stream data
|
|
streamData := []byte("stream chunk test")
|
|
_ = writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(streamData)})
|
|
|
|
mb, err := reader.ReadMultiBuffer()
|
|
common.Must(err)
|
|
if !bytes.Equal(mb[0].Bytes(), streamData) {
|
|
t.Errorf("echoed stream data mismatch: got %s, want %s", mb[0].Bytes(), streamData)
|
|
}
|
|
buf.ReleaseMulti(mb)
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
if receivedDest.NetAddr() != dest.NetAddr() {
|
|
t.Errorf("destination mismatch: got %s, want %s", receivedDest.NetAddr(), dest.NetAddr())
|
|
}
|
|
if diff := cmp.Diff(receivedPayload, testPayload); diff != "" {
|
|
t.Errorf("payload mismatch: %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUDPCodec(t *testing.T) {
|
|
methods := []string{
|
|
MethodAES128GCM,
|
|
MethodAES256GCM,
|
|
MethodChaCha20Poly1305,
|
|
}
|
|
|
|
dest := net.UDPDestination(net.LocalHostIP, net.Port(53))
|
|
payload := []byte("DNS query payload")
|
|
|
|
for _, methodName := range methods {
|
|
t.Run(methodName, func(t *testing.T) {
|
|
method, err := GetCipherMethod(methodName)
|
|
common.Must(err)
|
|
|
|
psk := make([]byte, method.KeySaltLength)
|
|
_, _ = rand.Read(psk)
|
|
|
|
clientCodec, err := NewUDPPacketCodec(method, [][]byte{psk})
|
|
common.Must(err)
|
|
serverCodec, err := NewUDPServerCodec(method, psk, time.Minute)
|
|
common.Must(err)
|
|
|
|
session, err := clientCodec.NewClientSession()
|
|
common.Must(err)
|
|
pktBuf, err := session.EncodePacket(dest, payload)
|
|
common.Must(err)
|
|
defer pktBuf.Release()
|
|
|
|
rawCopy := make([]byte, pktBuf.Len())
|
|
copy(rawCopy, pktBuf.Bytes())
|
|
|
|
decoded, err := serverCodec.DecodePacket(pktBuf.Bytes())
|
|
common.Must(err)
|
|
|
|
if decoded.HeaderType != HeaderTypeClient {
|
|
t.Errorf("expected header type %d, got %d", HeaderTypeClient, decoded.HeaderType)
|
|
}
|
|
if decoded.Destination.Port != dest.Port {
|
|
t.Errorf("port mismatch: got %d, want %d", decoded.Destination.Port, dest.Port)
|
|
}
|
|
if !bytes.Equal(decoded.Payload, payload) {
|
|
t.Errorf("payload mismatch: got %s, want %s", decoded.Payload, payload)
|
|
}
|
|
|
|
// Replay same packet wire bytes should fail with ErrPacketIdNotUnique
|
|
_, err = serverCodec.DecodePacket(rawCopy)
|
|
if err != ErrPacketIdNotUnique {
|
|
t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiUserManager(t *testing.T) {
|
|
masterKey := generateRandomKey(16)
|
|
userKey1 := generateRandomKey(16)
|
|
userKey2 := generateRandomKey(16)
|
|
|
|
config := &MultiUserServerConfig{
|
|
Method: MethodAES128GCM,
|
|
Key: masterKey,
|
|
Users: []*protocol.User{
|
|
{
|
|
Email: "user1@example.com",
|
|
Account: serial.ToTypedMessage(&Account{Key: userKey1}),
|
|
},
|
|
},
|
|
}
|
|
|
|
inbound, err := NewMultiServer(newTestContext(), config)
|
|
common.Must(err)
|
|
|
|
if inbound.GetUsersCount(context.Background()) != 1 {
|
|
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
|
}
|
|
|
|
u1 := inbound.GetUser(context.Background(), "user1@example.com")
|
|
if u1 == nil || u1.Email != "user1@example.com" {
|
|
t.Fatal("user1 not found")
|
|
}
|
|
|
|
// Add User 2
|
|
rawKey2, _ := base64.StdEncoding.DecodeString(userKey2)
|
|
u2 := &protocol.MemoryUser{
|
|
Email: "user2@example.com",
|
|
Account: &MemoryAccount{
|
|
Key: rawKey2,
|
|
},
|
|
}
|
|
err = inbound.AddUser(context.Background(), u2)
|
|
common.Must(err)
|
|
|
|
if inbound.GetUsersCount(context.Background()) != 2 {
|
|
t.Fatalf("expected 2 users, got %d", inbound.GetUsersCount(context.Background()))
|
|
}
|
|
|
|
// Remove User 1
|
|
err = inbound.RemoveUser(context.Background(), "user1@example.com")
|
|
common.Must(err)
|
|
|
|
if inbound.GetUsersCount(context.Background()) != 1 {
|
|
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
|
}
|
|
if inbound.GetUser(context.Background(), "user1@example.com") != nil {
|
|
t.Fatal("user1 should have been removed")
|
|
}
|
|
}
|
|
|
|
func TestLargeStreamTransfer(t *testing.T) {
|
|
method, err := GetCipherMethod(MethodAES128GCM)
|
|
common.Must(err)
|
|
sessionKey := make([]byte, 16)
|
|
_, _ = rand.Read(sessionKey)
|
|
|
|
clientAead, err := method.NewAEAD(sessionKey)
|
|
common.Must(err)
|
|
serverAead, err := method.NewAEAD(sessionKey)
|
|
common.Must(err)
|
|
|
|
r, w := io.Pipe()
|
|
defer r.Close()
|
|
defer w.Close()
|
|
|
|
writer := NewStreamWriter(w, clientAead)
|
|
reader := NewStreamReader(r, serverAead)
|
|
|
|
const totalSize = 100 * 1024 // 100 KB
|
|
data := make([]byte, totalSize)
|
|
_, _ = rand.Read(data)
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
// Write using Write (which splits by MaxPacketSize = 65535)
|
|
_, werr := writer.Write(data)
|
|
if werr != nil {
|
|
errCh <- werr
|
|
return
|
|
}
|
|
_ = w.Close()
|
|
errCh <- nil
|
|
}()
|
|
|
|
var received []byte
|
|
for {
|
|
mb, rerr := reader.ReadMultiBuffer()
|
|
if !mb.IsEmpty() {
|
|
for _, b := range mb {
|
|
received = append(received, b.Bytes()...)
|
|
}
|
|
buf.ReleaseMulti(mb)
|
|
}
|
|
if rerr != nil {
|
|
if rerr == io.EOF {
|
|
break
|
|
}
|
|
t.Fatalf("ReadMultiBuffer error: %v", rerr)
|
|
}
|
|
}
|
|
|
|
if werr := <-errCh; werr != nil {
|
|
t.Fatalf("writer error: %v", werr)
|
|
}
|
|
|
|
if len(received) != totalSize {
|
|
t.Fatalf("received size mismatch: got %d, want %d", len(received), totalSize)
|
|
}
|
|
if !bytes.Equal(received, data) {
|
|
t.Fatal("received data does not match sent data")
|
|
}
|
|
}
|
|
|
|
func TestClientUDPSessionMultiDestination(t *testing.T) {
|
|
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
|
t.Run(methodName, func(t *testing.T) {
|
|
method, err := GetCipherMethod(methodName)
|
|
common.Must(err)
|
|
rawKey := make([]byte, method.KeySaltLength)
|
|
_, _ = rand.Read(rawKey)
|
|
|
|
clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey})
|
|
common.Must(err)
|
|
serverCodec, err := NewUDPServerCodec(method, rawKey, time.Minute)
|
|
common.Must(err)
|
|
|
|
session, err := clientCodec.NewClientSession()
|
|
common.Must(err)
|
|
|
|
dest1 := net.UDPDestination(net.LocalHostIP, net.Port(53))
|
|
dest2 := net.UDPDestination(net.IPAddress([]byte{127, 0, 0, 2}), net.Port(53))
|
|
|
|
payload1 := []byte("query-google-dns")
|
|
payload2 := []byte("query-cloudflare-dns")
|
|
|
|
// Client sends to dest1 and dest2 using SAME session
|
|
pkt1, err := session.EncodePacket(dest1, payload1)
|
|
common.Must(err)
|
|
defer pkt1.Release()
|
|
pkt2, err := session.EncodePacket(dest2, payload2)
|
|
common.Must(err)
|
|
defer pkt2.Release()
|
|
|
|
// Server decodes both
|
|
dec1, err := serverCodec.DecodePacket(pkt1.Bytes())
|
|
common.Must(err)
|
|
dec2, err := serverCodec.DecodePacket(pkt2.Bytes())
|
|
common.Must(err)
|
|
|
|
if dec1.SessionID != session.ClientSessionID() || dec2.SessionID != session.ClientSessionID() {
|
|
t.Fatalf("both packets must share client session ID %d, got %d and %d", session.ClientSessionID(), dec1.SessionID, dec2.SessionID)
|
|
}
|
|
if dec1.Destination.String() != dest1.String() {
|
|
t.Fatalf("expected dest1 %s, got %s", dest1, dec1.Destination)
|
|
}
|
|
if dec2.Destination.String() != dest2.String() {
|
|
t.Fatalf("expected dest2 %s, got %s", dest2, dec2.Destination)
|
|
}
|
|
if !bytes.Equal(dec1.Payload, payload1) || !bytes.Equal(dec2.Payload, payload2) {
|
|
t.Fatal("payload mismatch")
|
|
}
|
|
|
|
// Server replies to dest1 and dest2
|
|
respPayload1 := []byte("reply-google-dns")
|
|
respPayload2 := []byte("reply-cloudflare-dns")
|
|
|
|
respPkt1, err := serverCodec.EncodeServerPacket(dec1.SessionID, dest1, respPayload1)
|
|
common.Must(err)
|
|
respPkt2, err := serverCodec.EncodeServerPacket(dec2.SessionID, dest2, respPayload2)
|
|
common.Must(err)
|
|
|
|
// Client decodes replies
|
|
clientDec1, err := session.DecodePacket(respPkt1)
|
|
common.Must(err)
|
|
if clientDec1.Destination.String() != dest1.String() {
|
|
t.Fatalf("expected client dec1 dest %s, got %s", dest1, clientDec1.Destination)
|
|
}
|
|
if !bytes.Equal(clientDec1.Payload, respPayload1) {
|
|
t.Fatal("reply payload 1 mismatch")
|
|
}
|
|
|
|
clientDec2, err := session.DecodePacket(respPkt2)
|
|
common.Must(err)
|
|
if clientDec2.Destination.String() != dest2.String() {
|
|
t.Fatalf("expected client dec2 dest %s, got %s", dest2, clientDec2.Destination)
|
|
}
|
|
if !bytes.Equal(clientDec2.Payload, respPayload2) {
|
|
t.Fatal("reply payload 2 mismatch")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Packets of one client session may be decoded concurrently, e.g. by the inbound
|
|
// workers of the client's old and new source address after a NAT rebinding.
|
|
func TestUDPServerConcurrentDecodeSameSession(t *testing.T) {
|
|
for _, methodName := range []string{MethodAES128GCM, MethodAES256GCM, MethodChaCha20Poly1305} {
|
|
t.Run(methodName, func(t *testing.T) {
|
|
method, err := GetCipherMethod(methodName)
|
|
common.Must(err)
|
|
rawKey := make([]byte, method.KeySaltLength)
|
|
_, _ = rand.Read(rawKey)
|
|
|
|
clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey})
|
|
common.Must(err)
|
|
serverCodec, err := NewUDPServerCodec(method, rawKey, time.Minute)
|
|
common.Must(err)
|
|
session, err := clientCodec.NewClientSession()
|
|
common.Must(err)
|
|
|
|
dest := net.UDPDestination(net.LocalHostIP, net.Port(443))
|
|
packets := make([][]byte, 256)
|
|
for i := range packets {
|
|
pkt, err := session.EncodePacket(dest, []byte("payload"))
|
|
common.Must(err)
|
|
packets[i] = append([]byte(nil), pkt.Bytes()...)
|
|
pkt.Release()
|
|
}
|
|
|
|
const workers = 4
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
accepted := make([]int, len(packets))
|
|
for w := 0; w < workers; w++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
// Every worker sees every packet, so each packet must be accepted exactly once.
|
|
for i, pkt := range packets {
|
|
if _, err := serverCodec.DecodePacket(pkt); err == nil {
|
|
mu.Lock()
|
|
accepted[i]++
|
|
mu.Unlock()
|
|
} else if err != ErrPacketIdNotUnique {
|
|
t.Errorf("packet %d: unexpected error: %v", i, err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
for i, n := range accepted {
|
|
if n != 1 {
|
|
t.Fatalf("packet %d accepted %d times, want 1", i, n)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|