719 lines
17 KiB
Go
719 lines
17 KiB
Go
package obfs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// --- in-memory packet pipe ---
|
|
|
|
// memEnd is one side of a bidirectional, lossy in-memory packet pipe.
|
|
type memEnd struct {
|
|
addr net.Addr
|
|
other *memEnd
|
|
|
|
mu sync.Mutex
|
|
closed bool
|
|
done chan struct{}
|
|
inbox chan memPacket
|
|
|
|
writeCount atomic.Int64
|
|
dropFn func(idx int) bool // optional: return true to drop the n-th outgoing packet
|
|
}
|
|
|
|
type memPacket struct {
|
|
src net.Addr
|
|
data []byte
|
|
}
|
|
|
|
func newMemPipe(aAddr, bAddr net.Addr) (*memEnd, *memEnd) {
|
|
a := &memEnd{addr: aAddr, inbox: make(chan memPacket, 1024), done: make(chan struct{})}
|
|
b := &memEnd{addr: bAddr, inbox: make(chan memPacket, 1024), done: make(chan struct{})}
|
|
a.other = b
|
|
b.other = a
|
|
return a, b
|
|
}
|
|
|
|
func (e *memEnd) WriteTo(p []byte, _ net.Addr) (int, error) {
|
|
idx := int(e.writeCount.Add(1) - 1)
|
|
if e.dropFn != nil && e.dropFn(idx) {
|
|
return len(p), nil
|
|
}
|
|
cp := make([]byte, len(p))
|
|
copy(cp, p)
|
|
select {
|
|
case e.other.inbox <- memPacket{src: e.addr, data: cp}:
|
|
case <-e.other.done:
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (e *memEnd) ReadFrom(p []byte) (int, net.Addr, error) {
|
|
select {
|
|
case pkt := <-e.inbox:
|
|
return copy(p, pkt.data), pkt.src, nil
|
|
case <-e.done:
|
|
return 0, nil, net.ErrClosed
|
|
}
|
|
}
|
|
|
|
func (e *memEnd) Close() error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.closed {
|
|
return nil
|
|
}
|
|
e.closed = true
|
|
close(e.done)
|
|
return nil
|
|
}
|
|
|
|
func (e *memEnd) LocalAddr() net.Addr { return e.addr }
|
|
func (e *memEnd) SetDeadline(time.Time) error { return nil }
|
|
func (e *memEnd) SetReadDeadline(time.Time) error { return nil }
|
|
func (e *memEnd) SetWriteDeadline(time.Time) error { return nil }
|
|
|
|
// pendingInbox returns the count of buffered, undelivered packets on this end.
|
|
func (e *memEnd) pendingInbox() int { return len(e.inbox) }
|
|
|
|
// --- helpers ---
|
|
|
|
func mustWrapGecko(t *testing.T, conn net.PacketConn, password string) net.PacketConn {
|
|
t.Helper()
|
|
g, err := WrapPacketConnGecko(conn, GeckoOptions{Password: []byte(password)})
|
|
if err != nil {
|
|
t.Fatalf("WrapPacketConnGecko: %v", err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
func makeAddrs() (a, b net.Addr) {
|
|
return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1111},
|
|
&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 2222}
|
|
}
|
|
|
|
// quicLong / quicShort produce realistic-looking QUIC payloads of n bytes
|
|
// with the appropriate top-bit on byte 0 to trigger / avoid fragmentation.
|
|
func quicLong(n int) []byte {
|
|
p := make([]byte, n)
|
|
rand.New(rand.NewSource(1)).Read(p)
|
|
p[0] = 0xc0
|
|
return p
|
|
}
|
|
|
|
func quicShort(n int) []byte {
|
|
p := make([]byte, n)
|
|
rand.New(rand.NewSource(2)).Read(p)
|
|
p[0] = 0x40
|
|
return p
|
|
}
|
|
|
|
// --- tests ---
|
|
|
|
func TestGeckoRoundTripShortHeader(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
ga := mustWrapGecko(t, a, "test")
|
|
gb := mustWrapGecko(t, b, "test")
|
|
defer ga.Close()
|
|
defer gb.Close()
|
|
|
|
payload := quicShort(400)
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
// Short-header packets should produce exactly one wire datagram.
|
|
if got := a.writeCount.Load(); got != 1 {
|
|
t.Fatalf("inner writes = %d, want 1", got)
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, src, err := gb.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadFrom: %v", err)
|
|
}
|
|
if !bytes.Equal(buf[:n], payload) {
|
|
t.Fatalf("payload mismatch")
|
|
}
|
|
if src.String() != aAddr.String() {
|
|
t.Fatalf("src = %v, want %v", src, aAddr)
|
|
}
|
|
}
|
|
|
|
func TestGeckoRoundTripLongHeader(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
ga := mustWrapGecko(t, a, "test")
|
|
gb := mustWrapGecko(t, b, "test")
|
|
defer ga.Close()
|
|
defer gb.Close()
|
|
|
|
payload := quicLong(1200)
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
chunks := a.writeCount.Load()
|
|
if chunks < geckoMinFragmentChunks || chunks > geckoMaxFragmentChunks {
|
|
t.Fatalf("inner writes = %d, want in [%d,%d]", chunks, geckoMinFragmentChunks, geckoMaxFragmentChunks)
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, _, err := gb.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadFrom: %v", err)
|
|
}
|
|
if !bytes.Equal(buf[:n], payload) {
|
|
t.Fatalf("payload mismatch")
|
|
}
|
|
}
|
|
|
|
func TestGeckoRoundTripSmallLongHeader(t *testing.T) {
|
|
for _, size := range []int{1, 2, 5, 10, 15, 20, 25, 27, 30, 40, 64, 128} {
|
|
t.Run(fmt.Sprintf("size=%d", size), func(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
ga := mustWrapGecko(t, a, "test")
|
|
gb := mustWrapGecko(t, b, "test")
|
|
defer ga.Close()
|
|
defer gb.Close()
|
|
|
|
payload := quicLong(size)
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, _, err := gb.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadFrom: %v", err)
|
|
}
|
|
if !bytes.Equal(buf[:n], payload) {
|
|
t.Fatalf("payload mismatch: got %d bytes, want %d", n, size)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGeckoWriteFragmentedNeverPanics(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
stop := make(chan struct{})
|
|
defer close(stop)
|
|
drainInbox(b, stop)
|
|
|
|
g := mustWrapGecko(t, a, "test").(*geckoPacketConn)
|
|
defer g.Close()
|
|
|
|
for size := 1; size <= 64; size++ {
|
|
payload := quicLong(size)
|
|
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("size=%d: WriteTo: %v", size, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGeckoReassemblesOutOfOrder(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
// We need exclusive access to b's inbox to reorder. Wrap a side directly,
|
|
// receive raw datagrams from b, shuffle them into a private pipe that
|
|
// feeds the receiver gecko.
|
|
ga := mustWrapGecko(t, a, "test")
|
|
defer ga.Close()
|
|
|
|
payload := quicLong(900)
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
|
|
// Drain b's inbox.
|
|
var pkts []memPacket
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) && a.writeCount.Load() > int64(len(pkts)) {
|
|
select {
|
|
case pkt := <-b.inbox:
|
|
pkts = append(pkts, pkt)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
if int64(len(pkts)) != a.writeCount.Load() {
|
|
t.Fatalf("captured %d/%d", len(pkts), a.writeCount.Load())
|
|
}
|
|
|
|
// Build a private pipe and feed packets in reverse order.
|
|
c, d := newMemPipe(aAddr, bAddr)
|
|
gd := mustWrapGecko(t, d, "test")
|
|
defer gd.Close()
|
|
defer c.Close()
|
|
go func() {
|
|
for i := len(pkts) - 1; i >= 0; i-- {
|
|
d.inbox <- pkts[i]
|
|
}
|
|
}()
|
|
|
|
buf := make([]byte, 4096)
|
|
n, _, err := gd.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadFrom: %v", err)
|
|
}
|
|
if !bytes.Equal(buf[:n], payload) {
|
|
t.Fatalf("payload mismatch after reorder")
|
|
}
|
|
}
|
|
|
|
func TestGeckoExpiresIncompleteFragment(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
// Drop the first chunk so reassembly can never complete.
|
|
a.dropFn = func(idx int) bool { return idx == 0 }
|
|
|
|
ga := mustWrapGecko(t, a, "test")
|
|
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
|
defer ga.Close()
|
|
defer gb.Close()
|
|
|
|
payload := quicLong(900)
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
|
|
// Read all incoming packets in a goroutine; ReadFrom should never return
|
|
// because no message is complete.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
gb.ReadFrom(buf)
|
|
close(done)
|
|
}()
|
|
|
|
// Wait until the reassembly entry exists, then fast-forward time.
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
gb.mu.Lock()
|
|
n := len(gb.reassembly)
|
|
gb.mu.Unlock()
|
|
if n > 0 {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
gb.mu.Lock()
|
|
if len(gb.reassembly) == 0 {
|
|
gb.mu.Unlock()
|
|
t.Fatal("expected at least one reassembly entry")
|
|
}
|
|
gb.mu.Unlock()
|
|
|
|
gb.gcExpired(time.Now().Add(geckoReassemblyTTL + time.Second))
|
|
|
|
gb.mu.Lock()
|
|
if len(gb.reassembly) != 0 {
|
|
gb.mu.Unlock()
|
|
t.Fatalf("reassembly map not empty after gc")
|
|
}
|
|
if len(gb.perSource) != 0 {
|
|
gb.mu.Unlock()
|
|
t.Fatalf("perSource map not empty after gc")
|
|
}
|
|
gb.mu.Unlock()
|
|
|
|
// Cleanup: closing gb unblocks the reader goroutine.
|
|
gb.Close()
|
|
<-done
|
|
}
|
|
|
|
func TestGeckoEnforcesPerSourceCap(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
// Drop every second chunk onward so reassembly never completes; the
|
|
// receiver accumulates partial entries.
|
|
a.dropFn = func(idx int) bool { return idx > 0 && idx%2 == 0 }
|
|
|
|
ga := mustWrapGecko(t, a, "test")
|
|
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
|
defer ga.Close()
|
|
defer gb.Close()
|
|
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
if _, _, err := gb.ReadFrom(buf); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for i := 0; i < geckoMaxPerSource+5; i++ {
|
|
if _, err := ga.WriteTo(quicLong(1200), bAddr); err != nil {
|
|
t.Fatalf("WriteTo %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Wait for the receiver to settle.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
gb.mu.Lock()
|
|
defer gb.mu.Unlock()
|
|
count := gb.perSource[aAddr.String()]
|
|
if count > geckoMaxPerSource {
|
|
t.Fatalf("perSource = %d, want <= %d", count, geckoMaxPerSource)
|
|
}
|
|
}
|
|
|
|
func TestGeckoEvictsOldestOnGlobalCap(t *testing.T) {
|
|
g := newGeckoPacketConn(nil, geckoDefaultMinPacket, geckoDefaultMaxPacket) // not actually used for I/O
|
|
defer close(g.closeCh)
|
|
|
|
// Manually fill the reassembly map past the cap with entries from
|
|
// distinct sources (so the per-source cap doesn't trigger first).
|
|
now := time.Now()
|
|
for i := 0; i < geckoMaxReassembly; i++ {
|
|
key := reassemblyKey{addr: fmt.Sprintf("src-%d", i), msgID: 1}
|
|
g.reassembly[key] = &reassemblyEntry{
|
|
chunks: make([][]byte, 4),
|
|
total: 4,
|
|
deadline: now.Add(time.Duration(i) * time.Millisecond),
|
|
}
|
|
g.perSource[key.addr]++
|
|
}
|
|
|
|
// Trigger eviction.
|
|
g.mu.Lock()
|
|
g.evictOldestLocked()
|
|
g.mu.Unlock()
|
|
|
|
if len(g.reassembly) != geckoMaxReassembly-1 {
|
|
t.Fatalf("after evict len = %d, want %d", len(g.reassembly), geckoMaxReassembly-1)
|
|
}
|
|
// The "oldest" was the one with the smallest deadline → src-0.
|
|
if _, ok := g.reassembly[reassemblyKey{addr: "src-0", msgID: 1}]; ok {
|
|
t.Fatal("oldest entry not evicted")
|
|
}
|
|
if g.perSource["src-0"] != 0 {
|
|
t.Fatalf("perSource[src-0] = %d after eviction, want 0", g.perSource["src-0"])
|
|
}
|
|
}
|
|
|
|
func TestGeckoBoundedUnderGarbageFlood(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
|
defer gb.Close()
|
|
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
if _, _, err := gb.ReadFrom(buf); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Inject 50k random datagrams from a moderate number of distinct sources.
|
|
rng := rand.New(rand.NewSource(42))
|
|
for i := 0; i < 50_000; i++ {
|
|
size := 16 + rng.Intn(200)
|
|
junk := make([]byte, size)
|
|
rng.Read(junk)
|
|
src := &net.UDPAddr{IP: net.IPv4(10, 0, 0, byte(i%256)), Port: 1024 + i%4096}
|
|
select {
|
|
case b.inbox <- memPacket{src: src, data: junk}:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("inbox blocked")
|
|
}
|
|
}
|
|
|
|
// Let the receiver drain.
|
|
for i := 0; i < 50; i++ {
|
|
if a.pendingInbox() == 0 && b.pendingInbox() == 0 {
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
gb.mu.Lock()
|
|
defer gb.mu.Unlock()
|
|
if len(gb.reassembly) > geckoMaxReassembly {
|
|
t.Fatalf("reassembly map size = %d, exceeds cap %d", len(gb.reassembly), geckoMaxReassembly)
|
|
}
|
|
}
|
|
|
|
func TestGeckoUDPPassthrough(t *testing.T) {
|
|
udp, err := net.ListenUDP("udp", &net.UDPAddr{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer udp.Close()
|
|
|
|
g, err := WrapPacketConnGecko(udp, GeckoOptions{Password: []byte("test")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer g.Close()
|
|
|
|
type udpLike interface {
|
|
SyscallConn() (syscall.RawConn, error)
|
|
SetReadBuffer(int) error
|
|
SetWriteBuffer(int) error
|
|
}
|
|
u, ok := g.(udpLike)
|
|
if !ok {
|
|
t.Fatal("gecko conn does not expose UDP-flavor methods")
|
|
}
|
|
if rc, err := u.SyscallConn(); err != nil || rc == nil {
|
|
t.Fatalf("SyscallConn: %v %v", rc, err)
|
|
}
|
|
if err := u.SetReadBuffer(1 << 20); err != nil {
|
|
t.Fatalf("SetReadBuffer: %v", err)
|
|
}
|
|
if err := u.SetWriteBuffer(1 << 20); err != nil {
|
|
t.Fatalf("SetWriteBuffer: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGeckoRequiresPassword(t *testing.T) {
|
|
if _, err := WrapPacketConnGecko(nil, GeckoOptions{}); err == nil {
|
|
t.Fatal("expected error for missing password")
|
|
}
|
|
}
|
|
|
|
func TestGeckoRejectsInvalidPacketSize(t *testing.T) {
|
|
cases := []GeckoOptions{
|
|
{Password: []byte("x"), MinPacketSize: 1000, MaxPacketSize: 500}, // min > max
|
|
{Password: []byte("x"), MinPacketSize: -1}, // min <= 0
|
|
{Password: []byte("x"), MaxPacketSize: geckoBufferSize + 1}, // max too large
|
|
}
|
|
for i, opt := range cases {
|
|
if _, err := WrapPacketConnGecko(nil, opt); err == nil {
|
|
t.Fatalf("case %d: expected error", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGeckoPaddingWithinBounds verifies every fragmented wire datagram is
|
|
// padded into the configured [min, max] size band.
|
|
func TestGeckoPaddingWithinBounds(t *testing.T) {
|
|
const minSize, maxSize = 400, 900
|
|
aAddr, bAddr := makeAddrs()
|
|
a, b := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
ga, err := WrapPacketConnGecko(a, GeckoOptions{
|
|
Password: []byte("test"),
|
|
MinPacketSize: minSize,
|
|
MaxPacketSize: maxSize,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("WrapPacketConnGecko: %v", err)
|
|
}
|
|
defer ga.Close()
|
|
|
|
for _, size := range []int{1, 50, 200, 600, 1200} {
|
|
if _, err := ga.WriteTo(quicLong(size), bAddr); err != nil {
|
|
t.Fatalf("WriteTo size=%d: %v", size, err)
|
|
}
|
|
}
|
|
for {
|
|
select {
|
|
case pkt := <-b.inbox:
|
|
if len(pkt.data) < minSize || len(pkt.data) > maxSize {
|
|
t.Fatalf("datagram size %d outside [%d, %d]", len(pkt.data), minSize, maxSize)
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compile-time interface assertions.
|
|
var (
|
|
_ net.PacketConn = (*geckoPacketConn)(nil)
|
|
_ udpLikePacketConn = (*geckoPacketConn)(nil)
|
|
)
|
|
|
|
// Sanity: errors.ErrUnsupported is what we return when inner isn't UDP-like.
|
|
func TestGeckoNonUDPInnerReturnsUnsupported(t *testing.T) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, _ := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
|
|
g := mustWrapGecko(t, a, "test").(*geckoPacketConn)
|
|
defer g.Close()
|
|
|
|
if _, err := g.SyscallConn(); !errors.Is(err, errors.ErrUnsupported) {
|
|
t.Fatalf("SyscallConn err = %v, want ErrUnsupported", err)
|
|
}
|
|
if err := g.SetReadBuffer(1 << 20); !errors.Is(err, errors.ErrUnsupported) {
|
|
t.Fatalf("SetReadBuffer err = %v, want ErrUnsupported", err)
|
|
}
|
|
if err := g.SetWriteBuffer(1 << 20); !errors.Is(err, errors.ErrUnsupported) {
|
|
t.Fatalf("SetWriteBuffer err = %v, want ErrUnsupported", err)
|
|
}
|
|
}
|
|
|
|
// --- benchmarks ---
|
|
|
|
// drainInbox empties an inbox in a goroutine until done is closed; used in
|
|
// benchmarks so a full memEnd.inbox never blocks WriteTo.
|
|
func drainInbox(end *memEnd, done <-chan struct{}) {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-end.inbox:
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func BenchmarkGeckoWriteShortHeader(b *testing.B) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, bEnd := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer bEnd.Close()
|
|
stop := make(chan struct{})
|
|
defer close(stop)
|
|
drainInbox(bEnd, stop)
|
|
|
|
g, _ := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
|
defer g.Close()
|
|
|
|
payload := quicShort(400)
|
|
b.SetBytes(int64(len(payload)))
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGeckoWriteLongHeader(b *testing.B) {
|
|
aAddr, bAddr := makeAddrs()
|
|
a, bEnd := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer bEnd.Close()
|
|
stop := make(chan struct{})
|
|
defer close(stop)
|
|
drainInbox(bEnd, stop)
|
|
|
|
g, _ := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
|
defer g.Close()
|
|
|
|
payload := quicLong(1200)
|
|
b.SetBytes(int64(len(payload)))
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// captureGeckoWire fragments payload through a sender Gecko conn and returns
|
|
// the resulting wire datagrams, for use as fixed input to read benchmarks.
|
|
func captureGeckoWire(b *testing.B, payload []byte) []memPacket {
|
|
b.Helper()
|
|
aAddr, bAddr := makeAddrs()
|
|
a, bEnd := newMemPipe(aAddr, bAddr)
|
|
defer a.Close()
|
|
defer bEnd.Close()
|
|
|
|
ga, err := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer ga.Close()
|
|
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
var wire []memPacket
|
|
for {
|
|
select {
|
|
case pkt := <-bEnd.inbox:
|
|
wire = append(wire, pkt)
|
|
default:
|
|
return wire
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGeckoReadShortHeader(b *testing.B) {
|
|
wire := captureGeckoWire(b, quicShort(400))
|
|
|
|
_, recv := newMemPipe(makeAddrs())
|
|
gb, err := WrapPacketConnGecko(recv, GeckoOptions{Password: []byte("bench")})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer gb.Close()
|
|
|
|
buf := make([]byte, 4096)
|
|
b.SetBytes(400)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for _, pkt := range wire {
|
|
recv.inbox <- pkt
|
|
}
|
|
if _, _, err := gb.ReadFrom(buf); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGeckoReadLongHeader(b *testing.B) {
|
|
wire := captureGeckoWire(b, quicLong(1200))
|
|
|
|
_, recv := newMemPipe(makeAddrs())
|
|
gb, err := WrapPacketConnGecko(recv, GeckoOptions{Password: []byte("bench")})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer gb.Close()
|
|
|
|
buf := make([]byte, 4096)
|
|
b.SetBytes(1200)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for _, pkt := range wire {
|
|
recv.inbox <- pkt
|
|
}
|
|
if _, _, err := gb.ReadFrom(buf); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|