mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-30 23:16:58 +00:00
https://github.com/XTLS/Xray-core/pull/7217#issuecomment-6489554617 --------- Co-authored-by: Ahmad-2213 <106820058+Ahmad-2213@users.noreply.github.com>
310 lines
8.2 KiB
Go
310 lines
8.2 KiB
Go
package xdrive
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type memSlots struct {
|
|
mu sync.Mutex
|
|
next int
|
|
descs map[string]string
|
|
bodies map[string][]byte
|
|
reads int
|
|
}
|
|
|
|
func newMemSlots() *memSlots {
|
|
return &memSlots{descs: make(map[string]string), bodies: make(map[string][]byte)}
|
|
}
|
|
|
|
func (m *memSlots) makeSlot(ctx context.Context, name string) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.next++
|
|
id := "slot-" + strconv.Itoa(m.next)
|
|
m.descs[id] = ""
|
|
return id, nil
|
|
}
|
|
|
|
func (m *memSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, ok := m.descs[id]; !ok {
|
|
return errNotFound
|
|
}
|
|
m.descs[id] = description
|
|
if body != nil {
|
|
m.bodies[id] = bytes.Clone(body)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *memSlots) readDescription(ctx context.Context, id string) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.reads++
|
|
d, ok := m.descs[id]
|
|
if !ok {
|
|
return "", errNotFound
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (m *memSlots) readBody(ctx context.Context, id string) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.bodies[id], nil
|
|
}
|
|
|
|
// the slots of all tests are asked for often, set once as a goroutine of a
|
|
// closed Conn may still read them
|
|
func init() {
|
|
slotHuntFloor, slotHuntCeiling, slotTailMin, slotTailMax = 2*time.Millisecond, 10*time.Millisecond, 5*time.Millisecond, 20*time.Millisecond
|
|
slotAckRetry, slotWatermarkTTL = 2*time.Millisecond, 2*time.Millisecond
|
|
}
|
|
|
|
func slotPair(t *testing.T, store slotStore, concurrency, segmentBytes uint32) (*Conn, *Conn) {
|
|
t.Helper()
|
|
p := paramsFromConfig(&Config{
|
|
SegmentBytes: segmentBytes,
|
|
FlushIntervalMs: 1,
|
|
PollIntervalMs: 1,
|
|
MaxPollIntervalMs: 20,
|
|
EagerWindowMs: 50,
|
|
Concurrency: concurrency,
|
|
})
|
|
r, err := makeRing(context.Background(), store, "session", p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := newSlotConn(context.Background(), store, r.Up, r.Down, p, nil)
|
|
server := newSlotConn(context.Background(), store, r.Down, r.Up, p, nil)
|
|
t.Cleanup(func() {
|
|
client.Close()
|
|
server.Close()
|
|
})
|
|
for _, c := range []*Conn{client, server} {
|
|
c.SetReadDeadline(time.Now().Add(30 * time.Second))
|
|
}
|
|
return client, server
|
|
}
|
|
|
|
// writeAll writes payload in pieces of random size, and closes c if end, the
|
|
// error goes to errs
|
|
func writeAll(c *Conn, payload []byte, end bool, errs chan<- error) {
|
|
go func() {
|
|
rng := rand.New(rand.NewSource(1))
|
|
for len(payload) > 0 {
|
|
n := min(len(payload), 1+rng.Intn(40<<10))
|
|
if _, err := c.Write(payload[:n]); err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
payload = payload[n:]
|
|
}
|
|
var err error
|
|
if end {
|
|
err = c.Close()
|
|
}
|
|
errs <- err
|
|
}()
|
|
}
|
|
|
|
func readAll(t *testing.T, c *Conn, want []byte) {
|
|
t.Helper()
|
|
got, err := io.ReadAll(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("got %d bytes, not the %d that were sent", len(got), len(want))
|
|
}
|
|
}
|
|
|
|
func randomPayload(n int) []byte {
|
|
payload := make([]byte, n)
|
|
rand.New(rand.NewSource(int64(n))).Read(payload)
|
|
return payload
|
|
}
|
|
|
|
func TestSlotFrame(t *testing.T) {
|
|
data := []byte("hello")
|
|
f, ok := decodeSlot(newSlotFrame(7, slotInline, data).encode())
|
|
if !ok || f.seq != 7 || f.mode != slotInline || !bytes.Equal(f.data, data) {
|
|
t.Fatalf("inline frame came back as %+v %v", f, ok)
|
|
}
|
|
|
|
body := randomPayload(100)
|
|
f, ok = decodeSlot(newSlotFrame(8, slotMedia, body).encode())
|
|
if !ok || f.seq != 8 || f.mode != slotMedia || f.data != nil || !f.holds(body) || f.holds(body[1:]) {
|
|
t.Fatalf("media frame came back as %+v %v", f, ok)
|
|
}
|
|
|
|
corrupt, _ := base64.StdEncoding.DecodeString(newSlotFrame(1, slotInline, data).encode())
|
|
corrupt[len(corrupt)-1]++
|
|
for _, broken := range []string{
|
|
"", "not base64", newSlotFrame(1, slotInline, data).encode()[:20], "WERSVlNMMDE=",
|
|
base64.StdEncoding.EncodeToString(corrupt),
|
|
} {
|
|
if _, ok := decodeSlot(broken); ok {
|
|
t.Errorf("%q decoded", broken)
|
|
}
|
|
}
|
|
}
|
|
|
|
// both sides send more segments than the ring has slots at once, small ones
|
|
// that go in descriptions and large ones that go in bodies, then one closes
|
|
// and the other reads EOF
|
|
func TestSlotConnRoundTrip(t *testing.T) {
|
|
client, server := slotPair(t, newMemSlots(), 4, 64<<10)
|
|
up, down := randomPayload(1<<20), randomPayload(300<<10)
|
|
errs := make(chan error, 2)
|
|
writeAll(client, up, false, errs)
|
|
writeAll(server, down, false, errs)
|
|
|
|
for _, c := range []struct {
|
|
conn *Conn
|
|
want []byte
|
|
}{{server, up}, {client, down}} {
|
|
got := make([]byte, len(c.want))
|
|
if _, err := io.ReadFull(c.conn, got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(got, c.want) {
|
|
t.Fatal("what came is not what was sent")
|
|
}
|
|
}
|
|
for range 2 {
|
|
if err := <-errs; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := client.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
readAll(t, server, nil)
|
|
}
|
|
|
|
// a reader that does not read holds the writer back instead of losing what a
|
|
// lap of the ring would overwrite
|
|
func TestSlotConnSlowReader(t *testing.T) {
|
|
client, server := slotPair(t, newMemSlots(), 2, 16<<10)
|
|
payload := randomPayload(400 << 10)
|
|
errs := make(chan error, 1)
|
|
writeAll(client, payload, true, errs)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
readAll(t, server, payload)
|
|
if err := <-errs; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// the next segment is asked for at once when it becomes the head, not after
|
|
// the wait it had as one that was fetched ahead
|
|
func TestSlotLatency(t *testing.T) {
|
|
client, server := slotPair(t, newMemSlots(), 8, 64<<10)
|
|
start := time.Now()
|
|
for range 10 {
|
|
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
|
|
c[0].Write([]byte("ping"))
|
|
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
if took := time.Since(start); took > 2*time.Second {
|
|
t.Fatalf("10 round trips took %v", took)
|
|
}
|
|
}
|
|
|
|
// once nothing comes for the eager window only the heads are asked for, not
|
|
// the segments that a reader fetches ahead while things come
|
|
func TestSlotIdle(t *testing.T) {
|
|
store := newMemSlots()
|
|
client, server := slotPair(t, store, 8, 64<<10)
|
|
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
|
|
c[0].Write([]byte("ping"))
|
|
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
store.mu.Lock()
|
|
before := store.reads
|
|
store.mu.Unlock()
|
|
time.Sleep(500 * time.Millisecond)
|
|
store.mu.Lock()
|
|
reads := store.reads - before
|
|
store.mu.Unlock()
|
|
// two heads, each asked for every 20 ms at most
|
|
if reads > 75 {
|
|
t.Fatalf("an idle session asked for slots %d times in 500 ms", reads)
|
|
}
|
|
}
|
|
|
|
type failingSlots struct {
|
|
*memSlots
|
|
}
|
|
|
|
// setSlot fails for segments, not for the error frame
|
|
func (f failingSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
|
if frame, ok := decodeSlot(description); ok && (frame.mode == slotInline || frame.mode == slotMedia) {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
return f.memSlots.setSlot(ctx, id, description, body)
|
|
}
|
|
|
|
func TestSlotWriterFailure(t *testing.T) {
|
|
client, server := slotPair(t, failingSlots{newMemSlots()}, 2, 16<<10)
|
|
client.Write([]byte("lost"))
|
|
server.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
if _, err := server.Read(make([]byte, 10)); err == nil || err == io.EOF || errors.Is(err, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("the reader got %v, not the failure of the writer", err)
|
|
}
|
|
for start := time.Now(); time.Since(start) < 5*time.Second; time.Sleep(time.Millisecond) {
|
|
if _, err := client.Write([]byte("more")); err != nil {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("Write did not fail")
|
|
}
|
|
|
|
type staleSlots struct {
|
|
*memSlots
|
|
mu sync.Mutex
|
|
served map[string]bool
|
|
}
|
|
|
|
// readBody gives what a slot held before once, as a body read right after its
|
|
// description may
|
|
func (s *staleSlots) readBody(ctx context.Context, id string) ([]byte, error) {
|
|
s.mu.Lock()
|
|
first := !s.served[id]
|
|
s.served[id] = true
|
|
s.mu.Unlock()
|
|
if first {
|
|
return []byte("stale"), nil
|
|
}
|
|
return s.memSlots.readBody(ctx, id)
|
|
}
|
|
|
|
func TestSlotStaleBody(t *testing.T) {
|
|
client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10)
|
|
payload := randomPayload(200 << 10)
|
|
errs := make(chan error, 1)
|
|
writeAll(client, payload, true, errs)
|
|
readAll(t, server, payload)
|
|
}
|