New
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/protocol"
|
||||
"dragontcp/internal/wire"
|
||||
)
|
||||
|
||||
const (
|
||||
bpModeProbe byte = 0
|
||||
bpModeUpload byte = 1
|
||||
bpModeDownload byte = 2
|
||||
bpModeBatchDownload byte = 3
|
||||
bpModeACK byte = 4
|
||||
bpHeaderSize = 29
|
||||
)
|
||||
|
||||
var bpOpenMagic = [4]byte{'D', 'O', 'P', '1'}
|
||||
var bpCloseMagic = [4]byte{'D', 'C', 'L', '1'}
|
||||
|
||||
type bpPhysicalConn struct {
|
||||
conn net.Conn
|
||||
requests int
|
||||
}
|
||||
|
||||
type bpLane struct {
|
||||
mu sync.Mutex
|
||||
serverAddr string
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
coverProfile cover.Profile
|
||||
autoReconnect bool
|
||||
pc *bpPhysicalConn
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newBPLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, coverProfile cover.Profile) *bpLane {
|
||||
autoReconnect := reconnectEvery == 1
|
||||
if autoReconnect {
|
||||
reconnectEvery = 0
|
||||
}
|
||||
return &bpLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
coverProfile: coverProfile,
|
||||
autoReconnect: autoReconnect,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *bpLane) transportFailureLocked(reused bool) {
|
||||
if l.autoReconnect && reused {
|
||||
l.reconnectEvery = 1
|
||||
}
|
||||
l.discardLocked()
|
||||
}
|
||||
|
||||
func (l *bpLane) discardLocked() {
|
||||
if l.pc != nil {
|
||||
_ = l.pc.conn.Close()
|
||||
l.pc = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *bpLane) closeAfterLocked() {
|
||||
if l.pc != nil && l.reconnectEvery > 0 && l.pc.requests >= l.reconnectEvery {
|
||||
l.discardLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *bpLane) ensureLocked() error {
|
||||
if l.closed {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if l.pc != nil {
|
||||
if l.reconnectEvery <= 0 || l.pc.requests < l.reconnectEvery {
|
||||
return nil
|
||||
}
|
||||
l.discardLocked()
|
||||
}
|
||||
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
conn, err := d.Dial("tcp", l.serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
||||
l.pc = &bpPhysicalConn{conn: conn}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *bpLane) Close() {
|
||||
l.mu.Lock()
|
||||
l.closed = true
|
||||
l.discardLocked()
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func writeBPRequest(w io.Writer, mode byte, sid wire.SessionID, seq uint64, payload []byte, downloadHint uint32, headerMask byte, clear bool) error {
|
||||
n := uint32(len(payload))
|
||||
if mode == bpModeDownload {
|
||||
n = downloadHint
|
||||
payload = nil
|
||||
}
|
||||
if len(payload) > wire.MaxPayload {
|
||||
return fmt.Errorf("BP payload too large: %d", len(payload))
|
||||
}
|
||||
var header [bpHeaderSize]byte
|
||||
header[0] = mode ^ headerMask
|
||||
copy(header[1:17], sid[:])
|
||||
binary.BigEndian.PutUint64(header[17:25], seq)
|
||||
binary.BigEndian.PutUint32(header[25:29], n)
|
||||
if clear {
|
||||
buffers := net.Buffers{header[:], payload}
|
||||
_, err := buffers.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
packet := make([]byte, bpHeaderSize+len(payload))
|
||||
copy(packet[:bpHeaderSize], header[:])
|
||||
copy(packet[bpHeaderSize:], payload)
|
||||
wire.MaskInPlace(packet[bpHeaderSize:], sid, mode, seq, false)
|
||||
for len(packet) > 0 {
|
||||
written, err := w.Write(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written <= 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
packet = packet[written:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readBPResponse(r io.Reader, sid wire.SessionID, mode byte, seq uint64, headerMask byte, clear bool) (byte, []byte, error) {
|
||||
status, body, err := wire.ReadResponseProfile(r, headerMask)
|
||||
if err == nil && status != wire.StatusError && len(body) > 0 && !clear {
|
||||
wire.MaskInPlace(body, sid, mode, seq, true)
|
||||
}
|
||||
return status, body, err
|
||||
}
|
||||
|
||||
func (l *bpLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte, downloadHint uint32) (byte, []byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := writeBPRequest(l.pc.conn, mode, sid, seq, payload, downloadHint, l.coverProfile.HeaderMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
status, body, err := readBPResponse(l.pc.conn, sid, mode, seq, l.coverProfile.HeaderMask, l.coverProfile.Clear)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return status, body, nil
|
||||
}
|
||||
return 0, nil, fmt.Errorf("BP request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
func decodeBPData(body []byte) ([]byte, error) {
|
||||
if len(body) < 4 {
|
||||
return nil, fmt.Errorf("short BP DATA body")
|
||||
}
|
||||
n := int(binary.BigEndian.Uint32(body[:4]))
|
||||
if n < 0 || n > len(body)-4 {
|
||||
return nil, fmt.Errorf("bad BP DATA length")
|
||||
}
|
||||
return append([]byte(nil), body[4:4+n]...), nil
|
||||
}
|
||||
|
||||
func (l *bpLane) download(sid wire.SessionID, offset uint64, maxChunk, count int) ([][]byte, byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
mode := bpModeDownload
|
||||
payload := []byte(nil)
|
||||
hint := uint32(maxChunk)
|
||||
if count > 1 {
|
||||
mode = bpModeBatchDownload
|
||||
payload = make([]byte, 6)
|
||||
binary.BigEndian.PutUint32(payload[:4], uint32(maxChunk))
|
||||
binary.BigEndian.PutUint16(payload[4:6], uint16(count))
|
||||
hint = 0
|
||||
}
|
||||
responses := 1
|
||||
if mode == bpModeBatchDownload {
|
||||
responses = count
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := writeBPRequest(l.pc.conn, mode, sid, offset, payload, hint, l.coverProfile.HeaderMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
|
||||
out := make([][]byte, 0, responses)
|
||||
lastStatus := wire.StatusOK
|
||||
for i := 0; i < responses; i++ {
|
||||
status, body, err := readBPResponse(l.pc.conn, sid, mode, offset, l.coverProfile.HeaderMask, l.coverProfile.Clear)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
goto retry
|
||||
}
|
||||
lastStatus = status
|
||||
switch status {
|
||||
case wire.StatusData:
|
||||
data, err := decodeBPData(body)
|
||||
if err != nil {
|
||||
l.discardLocked()
|
||||
return out, status, err
|
||||
}
|
||||
if len(data) > 0 {
|
||||
out = append(out, data)
|
||||
}
|
||||
case wire.StatusOK, wire.StatusWait:
|
||||
case wire.StatusEOF:
|
||||
case wire.StatusError:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("%s", string(body))
|
||||
default:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("unexpected BP download status %d", status)
|
||||
}
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return out, lastStatus, nil
|
||||
retry:
|
||||
}
|
||||
return nil, 0, fmt.Errorf("BP download request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
type bpConn struct {
|
||||
sid wire.SessionID
|
||||
opts chunkClientOptions
|
||||
uploadLane *bpLane
|
||||
downloadLane *bpLane
|
||||
upSizer *adaptiveSizer
|
||||
downSizer *adaptiveSizer
|
||||
|
||||
writeMu sync.Mutex
|
||||
upOffset uint64
|
||||
|
||||
readMu sync.Mutex
|
||||
readBuf []byte
|
||||
downloadOffset uint64
|
||||
consumedOffset uint64
|
||||
lastAck uint64
|
||||
eof bool
|
||||
pipeline int
|
||||
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func openBPTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
|
||||
if opts.minSize < 32 {
|
||||
opts.minSize = 32
|
||||
}
|
||||
if opts.maxSize < opts.minSize {
|
||||
opts.maxSize = opts.minSize
|
||||
}
|
||||
if opts.maxSize > 1024*1024 {
|
||||
opts.maxSize = 1024 * 1024
|
||||
}
|
||||
if opts.startSize < opts.minSize || opts.startSize > opts.maxSize {
|
||||
opts.startSize = opts.maxSize
|
||||
}
|
||||
if opts.txnTimeout <= 0 {
|
||||
opts.txnTimeout = 5 * time.Second
|
||||
}
|
||||
if opts.maxPipeline < 1 {
|
||||
opts.maxPipeline = 1
|
||||
}
|
||||
if opts.maxPipeline > 256 {
|
||||
opts.maxPipeline = 256
|
||||
}
|
||||
if opts.minPipeline < 1 {
|
||||
opts.minPipeline = 1
|
||||
}
|
||||
if opts.minPipeline > opts.maxPipeline {
|
||||
opts.minPipeline = opts.maxPipeline
|
||||
}
|
||||
reconnect := opts.reconnectEvery
|
||||
if reconnect < 0 {
|
||||
reconnect = 0
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadLane := newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile)
|
||||
status, body, err := uploadLane.single(bpModeUpload, sid, 0, nil, 0)
|
||||
if err != nil {
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("%s", string(body))
|
||||
}
|
||||
if status != wire.StatusOK {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("bad BP registration response %d", status)
|
||||
}
|
||||
openPayload, err := encodeOpen(token, targetHost, targetPort)
|
||||
if err != nil {
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
openPayload = append(append([]byte(nil), bpOpenMagic[:]...), openPayload...)
|
||||
status, body, err = uploadLane.single(bpModeUpload, sid, 1, openPayload, 0)
|
||||
if err != nil {
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("%s", string(body))
|
||||
}
|
||||
if status != wire.StatusOK {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("bad BP OPEN response %d", status)
|
||||
}
|
||||
|
||||
c := &bpConn{
|
||||
sid: sid,
|
||||
opts: opts,
|
||||
uploadLane: uploadLane,
|
||||
downloadLane: newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile),
|
||||
pipeline: opts.maxPipeline,
|
||||
}
|
||||
c.upSizer = newAdaptiveSizer("BP upload", opts.startSize, opts)
|
||||
c.downSizer = newAdaptiveSizer("BP download", opts.startSize, opts)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *bpConn) fillReadBuffer() error {
|
||||
if c.eof {
|
||||
return io.EOF
|
||||
}
|
||||
for len(c.readBuf) == 0 && !c.eof {
|
||||
if c.consumedOffset > c.lastAck {
|
||||
status, body, err := c.downloadLane.single(bpModeACK, c.sid, c.consumedOffset, nil, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
return fmt.Errorf("%s", string(body))
|
||||
}
|
||||
c.lastAck = c.consumedOffset
|
||||
}
|
||||
chunk := c.downSizer.Current()
|
||||
count := c.pipeline
|
||||
if count < c.opts.minPipeline {
|
||||
count = c.opts.minPipeline
|
||||
}
|
||||
if count > c.opts.maxPipeline {
|
||||
count = c.opts.maxPipeline
|
||||
}
|
||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, chunk, count)
|
||||
if err != nil {
|
||||
old, next := c.downSizer.FailureReason(chunk, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
return err
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
c.readBuf = appendChunkParts(c.readBuf, data)
|
||||
for _, part := range data {
|
||||
c.downloadOffset += uint64(len(part))
|
||||
}
|
||||
if len(data) > 0 {
|
||||
c.downSizer.Success(chunk)
|
||||
if c.pipeline < c.opts.maxPipeline {
|
||||
c.pipeline++
|
||||
}
|
||||
}
|
||||
if status == wire.StatusEOF {
|
||||
c.eof = true
|
||||
}
|
||||
if len(c.readBuf) == 0 && !c.eof {
|
||||
delay := c.opts.pollDelay
|
||||
if delay <= 0 {
|
||||
delay = 5 * time.Millisecond
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
if c.eof && len(c.readBuf) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *bpConn) Read(p []byte) (int, error) {
|
||||
c.readMu.Lock()
|
||||
defer c.readMu.Unlock()
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if len(c.readBuf) == 0 {
|
||||
if err := c.fillReadBuffer(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
n := copy(p, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
c.consumedOffset += uint64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *bpConn) Write(p []byte) (int, error) {
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
total := 0
|
||||
for len(p) > 0 {
|
||||
size := c.upSizer.Current()
|
||||
n := minInt(size, len(p))
|
||||
status, body, err := c.uploadLane.single(bpModeUpload, c.sid, c.upOffset+2, p[:n], 0)
|
||||
if err != nil {
|
||||
old, next := c.upSizer.FailureReason(size, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
return total, err
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
return total, fmt.Errorf("%s", string(body))
|
||||
}
|
||||
if status != wire.StatusOK {
|
||||
return total, fmt.Errorf("unexpected BP upload status %d", status)
|
||||
}
|
||||
c.upOffset += uint64(n)
|
||||
total += n
|
||||
p = p[n:]
|
||||
c.upSizer.Success(size)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (c *bpConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
_, _, _ = c.downloadLane.single(bpModeACK, c.sid, c.consumedOffset, bpCloseMagic[:], 0)
|
||||
c.uploadLane.Close()
|
||||
c.downloadLane.Close()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *bpConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-bp-local") }
|
||||
func (c *bpConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-bp-remote") }
|
||||
func (c *bpConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *bpConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *bpConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/protocol"
|
||||
"dragontcp/internal/wire"
|
||||
)
|
||||
@@ -29,6 +30,9 @@ type chunkClientOptions struct {
|
||||
tcpBuffer int
|
||||
minPipeline int
|
||||
maxPipeline int
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
skipPathProbe bool
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
@@ -120,6 +124,10 @@ func (s *adaptiveSizer) Success(attempted int) {
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Failure(attempted int) (int, int) {
|
||||
return s.FailureReason(attempted, nil)
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) FailureReason(attempted int, cause error) (int, int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.current
|
||||
@@ -147,7 +155,11 @@ func (s *adaptiveSizer) Failure(attempted int) (int, int) {
|
||||
}
|
||||
s.current = next
|
||||
if s.logChanges && old != next {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
if cause != nil {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure: %v\n", s.name, old, next, cause)
|
||||
} else {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
}
|
||||
}
|
||||
return old, next
|
||||
}
|
||||
@@ -163,19 +175,39 @@ type requestLane struct {
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
autoReconnect bool
|
||||
pc *physicalConn
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *requestLane {
|
||||
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *requestLane {
|
||||
autoReconnect := reconnectEvery == 1
|
||||
if autoReconnect {
|
||||
// Auto starts persistent. If a request fails only after this lane has
|
||||
// already completed traffic on the connection, it learns that reuse is
|
||||
// unsafe and switches itself to one request per connection.
|
||||
reconnectEvery = 0
|
||||
}
|
||||
return &requestLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
headerMask: headerMask,
|
||||
coverProfile: coverProfile,
|
||||
autoReconnect: autoReconnect,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *requestLane) transportFailureLocked(reused bool) {
|
||||
if l.autoReconnect && reused {
|
||||
l.reconnectEvery = 1
|
||||
}
|
||||
l.discardLocked()
|
||||
}
|
||||
|
||||
func (l *requestLane) discardLocked() {
|
||||
if l.pc != nil {
|
||||
_ = l.pc.conn.Close()
|
||||
@@ -204,6 +236,10 @@ func (l *requestLane) ensureLocked() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
||||
l.pc = &physicalConn{conn: conn}
|
||||
@@ -220,30 +256,38 @@ func (l *requestLane) Close() {
|
||||
func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte) (byte, []byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequest(l.pc.conn, mode, sid, seq, payload); err != nil {
|
||||
l.discardLocked()
|
||||
return 0, nil, err
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequestProfileEncoding(l.pc.conn, mode, sid, seq, payload, l.headerMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
if status != wire.StatusError && len(body) > 0 && !l.coverProfile.Clear {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
|
||||
}
|
||||
return status, body, nil
|
||||
}
|
||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
||||
if err != nil {
|
||||
l.discardLocked()
|
||||
return 0, nil, err
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
if status != wire.StatusError && len(body) > 0 {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
|
||||
}
|
||||
return status, body, nil
|
||||
return 0, nil, fmt.Errorf("request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
// download sends one compact request and consumes up to count response records.
|
||||
@@ -252,61 +296,71 @@ func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload
|
||||
func (l *requestLane) download(sid wire.SessionID, startOffset, ackOffset uint64, maxChunk, count int) ([][]byte, byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
payload := make([]byte, 14)
|
||||
binary.BigEndian.PutUint64(payload[0:8], ackOffset)
|
||||
binary.BigEndian.PutUint32(payload[8:12], uint32(maxChunk))
|
||||
binary.BigEndian.PutUint16(payload[12:14], uint16(count))
|
||||
if err := wire.WriteRequest(l.pc.conn, wire.ModeDownload, sid, startOffset, payload); err != nil {
|
||||
l.discardLocked()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
out := make([][]byte, 0, count)
|
||||
offset := startOffset
|
||||
lastStatus := wire.StatusOK
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
||||
if err != nil {
|
||||
l.discardLocked()
|
||||
return out, lastStatus, err
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
lastStatus = status
|
||||
switch status {
|
||||
case wire.StatusData:
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
|
||||
if len(body) == 0 {
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequestProfileEncoding(l.pc.conn, wire.ModeDownload, sid, startOffset, payload, l.headerMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
|
||||
out := make([][]byte, 0, count)
|
||||
offset := startOffset
|
||||
lastStatus := wire.StatusOK
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
goto retry
|
||||
}
|
||||
lastStatus = status
|
||||
switch status {
|
||||
case wire.StatusData:
|
||||
if !l.coverProfile.Clear {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("empty DATA response")
|
||||
}
|
||||
out = append(out, body)
|
||||
offset += uint64(len(body))
|
||||
case wire.StatusWait, wire.StatusEOF:
|
||||
i = count // stop after this response
|
||||
case wire.StatusError:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("empty DATA response")
|
||||
return out, status, fmt.Errorf("%s", string(body))
|
||||
default:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("unknown response status %d", status)
|
||||
}
|
||||
if status == wire.StatusWait || status == wire.StatusEOF {
|
||||
break
|
||||
}
|
||||
out = append(out, body)
|
||||
offset += uint64(len(body))
|
||||
case wire.StatusWait, wire.StatusEOF:
|
||||
i = count // stop after this response
|
||||
case wire.StatusError:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("%s", string(body))
|
||||
default:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("unknown response status %d", status)
|
||||
}
|
||||
if status == wire.StatusWait || status == wire.StatusEOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return out, lastStatus, nil
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return out, lastStatus, nil
|
||||
retry:
|
||||
}
|
||||
return nil, 0, fmt.Errorf("download request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
type pathProfile struct {
|
||||
@@ -364,7 +418,7 @@ func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, cand
|
||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
||||
timeout = 2500 * time.Millisecond
|
||||
}
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout)
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
|
||||
defer lane.Close()
|
||||
seq := probeSeq.Add(1)
|
||||
|
||||
@@ -405,7 +459,7 @@ func probePersistent(serverAddr, token string, opts chunkClientOptions) bool {
|
||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
||||
timeout = 2500 * time.Millisecond
|
||||
}
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout)
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout, opts.headerMask, opts.coverProfile)
|
||||
defer lane.Close()
|
||||
for i := 0; i < 8; i++ {
|
||||
seq := probeSeq.Add(1)
|
||||
@@ -459,7 +513,7 @@ func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte)
|
||||
}
|
||||
|
||||
func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile {
|
||||
key := fmt.Sprintf("%s|%s|%d|%d", serverAddr, token, opts.minSize, opts.maxSize)
|
||||
key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear)
|
||||
profileState.Lock()
|
||||
if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute {
|
||||
p := profileState.p
|
||||
@@ -533,6 +587,31 @@ type chunkConn struct {
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// appendChunkParts keeps the single-response fast path zero-copy. For a batch,
|
||||
// it reserves the complete size once rather than repeatedly growing and copying
|
||||
// the aggregate read buffer.
|
||||
func appendChunkParts(dst []byte, parts [][]byte) []byte {
|
||||
if len(parts) == 0 {
|
||||
return dst
|
||||
}
|
||||
if len(dst) == 0 && len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
total := len(dst)
|
||||
for _, part := range parts {
|
||||
total += len(part)
|
||||
}
|
||||
if cap(dst) < total {
|
||||
grown := make([]byte, len(dst), total)
|
||||
copy(grown, dst)
|
||||
dst = grown
|
||||
}
|
||||
for _, part := range parts {
|
||||
dst = append(dst, part...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
|
||||
if opts.minSize < 32 {
|
||||
opts.minSize = 32
|
||||
@@ -565,16 +644,21 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
opts.minPipeline = opts.maxPipeline
|
||||
}
|
||||
|
||||
profile := getPathProfile(serverAddr, token, opts)
|
||||
profile := pathProfile{
|
||||
upload: opts.minSize,
|
||||
download: opts.minSize,
|
||||
persistent: false,
|
||||
at: time.Now(),
|
||||
}
|
||||
if !opts.skipPathProbe {
|
||||
profile = getPathProfile(serverAddr, token, opts)
|
||||
}
|
||||
reconnect := opts.reconnectEvery
|
||||
// Compatibility-friendly reconnect modes:
|
||||
// 0 = persistent (CLI explicit)
|
||||
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
|
||||
// 1 = auto: start persistent, then learn one request/connection only if
|
||||
// reuse fails during real traffic
|
||||
// N>=2 = force connection rotation after N logical requests
|
||||
// Resolved silently: this runs once per proxied flow, so it must never log.
|
||||
if reconnect == 1 && profile.persistent {
|
||||
reconnect = 0
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
@@ -583,7 +667,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
// OPEN rides the upload lane instead of a throwaway connection. A dedicated
|
||||
// control connection cost one extra dial per proxied flow, which shows up on
|
||||
// the server as connection churn on top of the steady-state count.
|
||||
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
||||
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile)
|
||||
payload, err := encodeOpen(token, targetHost, targetPort)
|
||||
if err != nil {
|
||||
uploadLane.Close()
|
||||
@@ -624,7 +708,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
opts: opts,
|
||||
serverMax: serverMax,
|
||||
uploadLane: uploadLane,
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile),
|
||||
// Start at the configured ceiling. On transport failure the batch is
|
||||
// halved but never below minPipeline; successful data grows it back by
|
||||
// one. When min == max the depth is pinned and never adapts, which is
|
||||
@@ -659,8 +743,8 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
}
|
||||
|
||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
||||
c.readBuf = appendChunkParts(c.readBuf, data)
|
||||
for _, part := range data {
|
||||
c.readBuf = append(c.readBuf, part...)
|
||||
c.downloadOffset += uint64(len(part))
|
||||
}
|
||||
if len(data) > 0 {
|
||||
@@ -678,10 +762,10 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
c.pipeline = c.minPipeline
|
||||
}
|
||||
if c.opts.adaptLog && old != c.pipeline {
|
||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure\n", old, c.pipeline)
|
||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure: %v\n", old, c.pipeline, err)
|
||||
}
|
||||
} else {
|
||||
old, next := c.downSizer.Failure(chunk)
|
||||
old, next := c.downSizer.FailureReason(chunk, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
minFailures++
|
||||
if minFailures >= 8 {
|
||||
@@ -745,7 +829,7 @@ func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
n := minInt(size, len(p))
|
||||
status, body, err := c.uploadLane.single(wire.ModeUpload, c.sid, c.upOffset, p[:n])
|
||||
if err != nil {
|
||||
old, next := c.upSizer.Failure(size)
|
||||
old, next := c.upSizer.FailureReason(size, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
minFailures++
|
||||
if minFailures >= 8 {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/wire"
|
||||
)
|
||||
|
||||
func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
||||
opts := chunkClientOptions{
|
||||
@@ -23,9 +30,72 @@ func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnectAutoLearnsFromRealReuseFailure(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
first, err := ln.Accept()
|
||||
if err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
if _, err := wire.ReadRequest(first); err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
if err := wire.WriteResponse(first, wire.StatusOK, nil); err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
_ = first.Close() // Force the next logical request to reconnect.
|
||||
|
||||
second, err := ln.Accept()
|
||||
if err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
defer second.Close()
|
||||
if _, err := wire.ReadRequest(second); err != nil {
|
||||
serverErr <- err
|
||||
return
|
||||
}
|
||||
serverErr <- wire.WriteResponse(second, wire.StatusOK, nil)
|
||||
}()
|
||||
|
||||
lane := newRequestLane(ln.Addr().String(), 0, 1, time.Second, 0, cover.Profile{})
|
||||
defer lane.Close()
|
||||
if !lane.autoReconnect || lane.reconnectEvery != 0 {
|
||||
t.Fatalf("auto lane started auto=%t reconnectEvery=%d", lane.autoReconnect, lane.reconnectEvery)
|
||||
}
|
||||
var sid wire.SessionID
|
||||
if status, _, err := lane.single(wire.ModeProbe, sid, 1, nil); err != nil || status != wire.StatusOK {
|
||||
t.Fatalf("first request status=%d err=%v", status, err)
|
||||
}
|
||||
if status, _, err := lane.single(wire.ModeProbe, sid, 2, nil); err != nil || status != wire.StatusOK {
|
||||
t.Fatalf("retried request status=%d err=%v", status, err)
|
||||
}
|
||||
if lane.reconnectEvery != 1 || lane.pc != nil {
|
||||
t.Fatalf("auto lane did not learn single-request mode: reconnectEvery=%d pc=%v", lane.reconnectEvery, lane.pc)
|
||||
}
|
||||
if err := <-serverErr; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnectZeroMeansPersistent(t *testing.T) {
|
||||
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
|
||||
lane := newRequestLane("127.0.0.1:1", 0, 0, 0, 0, cover.Profile{})
|
||||
if lane.reconnectEvery != 0 {
|
||||
t.Fatalf("reconnectEvery=%d, want 0", lane.reconnectEvery)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBPAutoStartsPersistent(t *testing.T) {
|
||||
lane := newBPLane("127.0.0.1:1", 0, 1, time.Second, cover.Profile{})
|
||||
if !lane.autoReconnect || lane.reconnectEvery != 0 {
|
||||
t.Fatalf("BP auto lane started auto=%t reconnectEvery=%d", lane.autoReconnect, lane.reconnectEvery)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,10 +377,12 @@ func main() {
|
||||
chunkPollers = flag.Int("chunk-pollers", 1, "reserved compatibility setting; binary transport uses one download worker")
|
||||
chunkConcurrency = flag.Int("chunk-concurrency", 1, "maximum download records per request (1-256)")
|
||||
chunkConcurrencyMin = flag.Int("chunk-concurrency-min", 1, "minimum download records per request (1-256); equal to --chunk-concurrency pins the depth")
|
||||
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "force reconnect after N logical requests; 0 = persistent/automatic")
|
||||
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "connection reuse: 0 persistent, 1 auto-learn, N rotate after N requests")
|
||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||
wireMode = flag.String("wire", "auto", "wire mode: b, x, or auto (probe and pick)")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 5*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||
wireMode = flag.String("wire", "auto", "wire mode: b, bp, x, or auto (probe and pick)")
|
||||
wireProbeDelay = flag.Duration("wire-probe-delay", time.Second, "minimum delay between wire profile probe starts (200ms-30s)")
|
||||
wireProbeThreads = flag.Int("wire-probe-threads", 1, "maximum concurrent wire profile probes (1-16)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -430,19 +432,29 @@ func main() {
|
||||
}
|
||||
*wireMode = strings.ToLower(strings.TrimSpace(*wireMode))
|
||||
switch *wireMode {
|
||||
case WireBinary, WireXOR, WireAuto:
|
||||
case WireBinary, WireBP, WireXOR, WireAuto:
|
||||
case "binary":
|
||||
*wireMode = WireBinary
|
||||
case "bh", "h":
|
||||
*wireMode = WireBP
|
||||
case "xor":
|
||||
*wireMode = WireXOR
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "--wire must be b, x or auto")
|
||||
fmt.Fprintln(os.Stderr, "--wire must be b, bp, x or auto")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkReconnect < 0 {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-reconnect-every must be 0 or greater")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *wireProbeDelay < 200*time.Millisecond || *wireProbeDelay > 30*time.Second {
|
||||
fmt.Fprintln(os.Stderr, "--wire-probe-delay must be between 200ms and 30s")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *wireProbeThreads < 1 || *wireProbeThreads > 16 {
|
||||
fmt.Fprintln(os.Stderr, "--wire-probe-threads must be between 1 and 16")
|
||||
os.Exit(2)
|
||||
}
|
||||
chunkOpts := chunkClientOptions{
|
||||
startSize: *chunkStart,
|
||||
minSize: *chunkMin,
|
||||
@@ -498,15 +510,11 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
wires := newWireSelector(*wireMode, serverAddr, *token, chunkOpts, xorOpts)
|
||||
if *wireMode == WireAuto {
|
||||
fmt.Printf("wire=auto probing %s\n", probeHost)
|
||||
// Resolve in the background so startup is not blocked; a connection that
|
||||
// arrives first simply waits for the same result.
|
||||
go wires.mode()
|
||||
} else {
|
||||
fmt.Printf("wire=%s (manual)\n", *wireMode)
|
||||
}
|
||||
wires := newWireSelector(*wireMode, serverAddr, *token, chunkOpts, xorOpts, *wireProbeDelay, *wireProbeThreads)
|
||||
fmt.Printf("wire=%s discovering fixed header profile via http://%s/ probe_delay=%s probe_threads=%d\n", *wireMode, probeHost, wireProbeDelay.String(), *wireProbeThreads)
|
||||
// Discover in the background so the local listener starts immediately. A
|
||||
// connection arriving first waits on the same selector lock and result.
|
||||
go wires.mode()
|
||||
|
||||
slots := make(chan struct{}, *maxConnections)
|
||||
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/xorchunk"
|
||||
)
|
||||
|
||||
// DragonTCP speaks two wires that are not interchangeable:
|
||||
// DragonTCP speaks three wires that are not interchangeable:
|
||||
//
|
||||
// b — compact binary records (29/5-byte headers, SHA-256 keystream mask)
|
||||
// b — compact binary records (29/5-byte headers, clear or SHA-256-compatible payloads)
|
||||
// bp — compatible registration/upload/download/ACK records, clear or SHA-256-compatible
|
||||
// x — legacy UP/OK framing with XOR 0xAD over ASCII chunk commands
|
||||
//
|
||||
// Networks differ in which they pass, so the client can be pinned to either or
|
||||
@@ -20,6 +23,7 @@ import (
|
||||
// keeping the first that answers.
|
||||
const (
|
||||
WireBinary = "b"
|
||||
WireBP = "bp"
|
||||
WireXOR = "x"
|
||||
WireAuto = "auto"
|
||||
)
|
||||
@@ -34,109 +38,316 @@ const (
|
||||
)
|
||||
|
||||
type wireSelector struct {
|
||||
mu sync.Mutex
|
||||
configured string // b, x or auto
|
||||
resolved string // b or x once decided
|
||||
serverAddr string
|
||||
token string
|
||||
binOpts chunkClientOptions
|
||||
xorOpts xorchunk.Options
|
||||
mu sync.Mutex
|
||||
configured string // b, x or auto
|
||||
resolved wireChoice
|
||||
hasChoice bool
|
||||
serverAddr string
|
||||
token string
|
||||
binOpts chunkClientOptions
|
||||
xorOpts xorchunk.Options
|
||||
probeDelay time.Duration
|
||||
probeThreads int
|
||||
|
||||
// Test hooks are nil in production.
|
||||
candidateOverride []wireChoice
|
||||
probeOverride func(wireChoice) bool
|
||||
}
|
||||
|
||||
func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options) *wireSelector {
|
||||
s := &wireSelector{
|
||||
configured: configured,
|
||||
serverAddr: serverAddr,
|
||||
token: token,
|
||||
binOpts: binOpts,
|
||||
xorOpts: xorOpts,
|
||||
type wireChoice struct {
|
||||
mode string
|
||||
mask byte
|
||||
cover cover.Profile
|
||||
}
|
||||
|
||||
func (c wireChoice) String() string {
|
||||
if c.mode == WireBP {
|
||||
if c.cover.Enabled {
|
||||
return fmt.Sprintf("bp/%s", c.cover)
|
||||
}
|
||||
return "bp/direct"
|
||||
}
|
||||
if configured != WireAuto {
|
||||
s.resolved = configured
|
||||
if c.cover.Enabled {
|
||||
return fmt.Sprintf("%s/mask-%02x/%s", c.mode, c.mask, c.cover)
|
||||
}
|
||||
return fmt.Sprintf("%s/mask-%02x/direct", c.mode, c.mask)
|
||||
}
|
||||
|
||||
func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options, probeDelay time.Duration, probeThreads int) *wireSelector {
|
||||
s := &wireSelector{
|
||||
configured: configured,
|
||||
serverAddr: serverAddr,
|
||||
token: token,
|
||||
binOpts: binOpts,
|
||||
xorOpts: xorOpts,
|
||||
probeDelay: probeDelay,
|
||||
probeThreads: probeThreads,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// dial opens a tunnel over the active wire, resolving the wire first if needed.
|
||||
func (s *wireSelector) dial(host string, port int) (net.Conn, error) {
|
||||
mode := s.mode()
|
||||
if mode == WireXOR {
|
||||
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts)
|
||||
choice := s.mode()
|
||||
if choice.mode == WireBP {
|
||||
opts := s.binOpts
|
||||
opts.headerMask = choice.mask
|
||||
opts.coverProfile = choice.cover
|
||||
return openBPTunnel(s.serverAddr, s.token, host, port, opts)
|
||||
}
|
||||
return openChunkTunnel(s.serverAddr, s.token, host, port, s.binOpts)
|
||||
if choice.mode == WireXOR {
|
||||
if choice.cover.Enabled {
|
||||
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts.WithCoverProfile(choice.cover))
|
||||
}
|
||||
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts.WithHeaderMask(choice.mask))
|
||||
}
|
||||
opts := s.binOpts
|
||||
opts.headerMask = choice.mask
|
||||
opts.coverProfile = choice.cover
|
||||
return openChunkTunnel(s.serverAddr, s.token, host, port, opts)
|
||||
}
|
||||
|
||||
// mode returns the wire to use, running detection once if configured as auto.
|
||||
// Detection failure is not cached, so a client that starts before the network
|
||||
// is usable retries on the next connection instead of latching a bad guess.
|
||||
func (s *wireSelector) mode() string {
|
||||
func (s *wireSelector) mode() wireChoice {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.resolved != "" {
|
||||
if s.hasChoice {
|
||||
return s.resolved
|
||||
}
|
||||
if picked, ok := s.detectLocked(); ok {
|
||||
s.resolved = picked
|
||||
s.hasChoice = true
|
||||
return picked
|
||||
}
|
||||
// Undecided: use the binary wire for this attempt without caching it.
|
||||
return WireBinary
|
||||
// Undecided: honor an explicitly pinned family for this attempt without
|
||||
// caching it. Auto retains the original B fallback and retries discovery on
|
||||
// the next connection.
|
||||
switch s.configured {
|
||||
case WireBP:
|
||||
return wireChoice{mode: WireBP}
|
||||
case WireXOR:
|
||||
return wireChoice{mode: WireXOR}
|
||||
default:
|
||||
return wireChoice{mode: WireBinary}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *wireSelector) detectLocked() (string, bool) {
|
||||
for _, candidate := range []string{WireBinary, WireXOR} {
|
||||
if s.probe(candidate) {
|
||||
fmt.Printf("wire probe: %s selected via %s\n", candidate, probeHost)
|
||||
return candidate, true
|
||||
// profileCandidates covers all compatible B first-byte bases and all X magic
|
||||
// masks that cannot be confused with B. Profile zero for each wire is first so
|
||||
// existing permissive networks complete discovery quickly.
|
||||
func (s *wireSelector) profileCandidates() []wireChoice {
|
||||
var binaryProfiles []wireChoice
|
||||
var xorProfiles []wireChoice
|
||||
if s.configured == WireAuto || s.configured == WireBinary {
|
||||
for n := 0; n < 256; n += 8 {
|
||||
binaryProfiles = append(binaryProfiles, wireChoice{mode: WireBinary, mask: byte(n)})
|
||||
}
|
||||
fmt.Printf("wire probe: %s failed\n", candidate)
|
||||
}
|
||||
fmt.Printf("wire probe: neither wire reached %s; retrying later\n", probeHost)
|
||||
return "", false
|
||||
if s.configured == WireAuto || s.configured == WireXOR {
|
||||
for n := 0; n < 256; n++ {
|
||||
mask := byte(n)
|
||||
if ('U'^mask)&7 >= 5 {
|
||||
xorProfiles = append(xorProfiles, wireChoice{mode: WireXOR, mask: mask})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paddingRange := []uint16{0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 768, 1024, 1400, 2048, 4096}
|
||||
makeCovered := func(n int, xor, clear bool) cover.Profile {
|
||||
first := byte(n)
|
||||
second := byte(n*197 + 101)
|
||||
mask := byte(n*149 + 37)
|
||||
return cover.Profile{
|
||||
Enabled: true,
|
||||
ID: uint16(first)<<8 | uint16(second),
|
||||
Padding: paddingRange[n%len(paddingRange)],
|
||||
HeaderMask: mask,
|
||||
XOR: xor,
|
||||
Clear: clear,
|
||||
}
|
||||
}
|
||||
|
||||
// New peers try the clear-payload profile first. The next candidates are
|
||||
// legacy direct profiles, so an older server falls back immediately instead
|
||||
// of screening the complete expanded profile range.
|
||||
out := make([]wireChoice, 0, len(binaryProfiles)+len(xorProfiles)+1025)
|
||||
if s.configured == WireAuto || s.configured == WireBinary {
|
||||
profile := makeCovered(0, false, true)
|
||||
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
|
||||
}
|
||||
if s.configured == WireAuto || s.configured == WireBP {
|
||||
profile := makeCovered(0, false, true)
|
||||
out = append(out, wireChoice{mode: WireBP, mask: profile.HeaderMask, cover: profile})
|
||||
}
|
||||
|
||||
// Interleave formats so neither family can consume the entire discovery
|
||||
// window before the other one gets a chance.
|
||||
for i := 0; i < len(binaryProfiles) || i < len(xorProfiles); i++ {
|
||||
if i < len(binaryProfiles) {
|
||||
out = append(out, binaryProfiles[i])
|
||||
}
|
||||
if i < len(xorProfiles) {
|
||||
out = append(out, xorProfiles[i])
|
||||
}
|
||||
if i == 0 && s.configured == WireAuto {
|
||||
out = append(out, wireChoice{mode: WireBP})
|
||||
}
|
||||
}
|
||||
if s.configured == WireBP {
|
||||
out = append(out, wireChoice{mode: WireBP})
|
||||
}
|
||||
|
||||
// Covered profiles expand discovery beyond the one-byte direct formats
|
||||
// without taking the Cartesian product (which would create thousands of
|
||||
// connections). Across this distributed range each wire still exercises all
|
||||
// 256 first bytes, all 256 frame masks, and every padding length repeatedly.
|
||||
for n := 0; n < 256; n++ {
|
||||
if s.configured == WireAuto || s.configured == WireBinary {
|
||||
profile := makeCovered(n, false, false)
|
||||
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
|
||||
if n != 0 {
|
||||
profile = makeCovered(n, false, true)
|
||||
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
|
||||
}
|
||||
}
|
||||
if s.configured == WireAuto || s.configured == WireBP {
|
||||
if n != 0 {
|
||||
profile := makeCovered(n, false, true)
|
||||
out = append(out, wireChoice{mode: WireBP, mask: profile.HeaderMask, cover: profile})
|
||||
}
|
||||
}
|
||||
if s.configured == WireAuto || s.configured == WireXOR {
|
||||
profile := makeCovered(n, true, false)
|
||||
out = append(out, wireChoice{mode: WireXOR, mask: profile.HeaderMask, cover: profile})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// detectLocked validates candidates with real HTTP traffic through ip.dr2.site.
|
||||
// The default is one worker. Users may explicitly allow more workers, while the
|
||||
// launch delay still spaces new attempts globally to avoid a connection burst.
|
||||
func (s *wireSelector) detectLocked() (wireChoice, bool) {
|
||||
candidates := s.profileCandidates()
|
||||
if s.candidateOverride != nil {
|
||||
candidates = s.candidateOverride
|
||||
}
|
||||
threads := s.probeThreads
|
||||
if threads < 1 {
|
||||
threads = 1
|
||||
}
|
||||
if threads > 16 {
|
||||
threads = 16
|
||||
}
|
||||
delay := s.probeDelay
|
||||
if delay <= 0 {
|
||||
delay = time.Second
|
||||
}
|
||||
type result struct {
|
||||
choice wireChoice
|
||||
ok bool
|
||||
}
|
||||
results := make(chan result, threads)
|
||||
next := 0
|
||||
inflight := 0
|
||||
completed := 0
|
||||
started := time.Now()
|
||||
var lastLaunch time.Time
|
||||
for next < len(candidates) || inflight > 0 {
|
||||
canLaunch := next < len(candidates) && inflight < threads
|
||||
if canLaunch && (lastLaunch.IsZero() || time.Since(lastLaunch) >= delay) {
|
||||
candidate := candidates[next]
|
||||
next++
|
||||
inflight++
|
||||
lastLaunch = time.Now()
|
||||
go func(choice wireChoice) {
|
||||
validated := false
|
||||
if s.probeOverride != nil {
|
||||
validated = s.probeOverride(choice)
|
||||
} else {
|
||||
validated = s.probe(choice)
|
||||
}
|
||||
results <- result{choice: choice, ok: validated}
|
||||
}(candidate)
|
||||
continue
|
||||
}
|
||||
|
||||
var got result
|
||||
if canLaunch {
|
||||
wait := delay - time.Since(lastLaunch)
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case got = <-results:
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case <-timer.C:
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
got = <-results
|
||||
}
|
||||
inflight--
|
||||
completed++
|
||||
if got.ok {
|
||||
fmt.Printf("wire probe: selected=%s completed=%d launched=%d elapsed=%s target=http://%s/ validated=true threads=%d fixed_until_restart=true\n", got.choice, completed, next, time.Since(started).Round(time.Millisecond), probeHost, threads)
|
||||
return got.choice, true
|
||||
}
|
||||
if completed%32 == 0 {
|
||||
fmt.Printf("wire probe: completed=%d/%d launched=%d elapsed=%s target=http://%s/ no validated profile yet\n", completed, len(candidates), next, time.Since(started).Round(time.Millisecond), probeHost)
|
||||
}
|
||||
}
|
||||
fmt.Printf("wire probe: no profile validated through http://%s/ after %d candidates in %s; retrying later\n", probeHost, completed, time.Since(started).Round(time.Millisecond))
|
||||
return wireChoice{}, false
|
||||
}
|
||||
|
||||
// probe fetches probeHost through one wire and reports whether a well-formed
|
||||
// HTTP status line came back.
|
||||
func (s *wireSelector) probe(mode string) bool {
|
||||
type result struct{ ok bool }
|
||||
done := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
var (
|
||||
conn net.Conn
|
||||
err error
|
||||
)
|
||||
if mode == WireXOR {
|
||||
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts)
|
||||
func (s *wireSelector) probe(choice wireChoice) bool {
|
||||
var (
|
||||
conn net.Conn
|
||||
err error
|
||||
)
|
||||
if choice.mode == WireXOR {
|
||||
if choice.cover.Enabled {
|
||||
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithCoverProfile(choice.cover))
|
||||
} else {
|
||||
conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, s.binOpts)
|
||||
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithHeaderMask(choice.mask))
|
||||
}
|
||||
if err != nil {
|
||||
done <- result{false}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
request := "GET / HTTP/1.1\r\nHost: " + probeHost + "\r\nUser-Agent: dragontcp\r\nConnection: close\r\n\r\n"
|
||||
if _, err := conn.Write([]byte(request)); err != nil {
|
||||
done <- result{false}
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 64)
|
||||
n, err := conn.Read(buf)
|
||||
if n <= 0 || (err != nil && n == 0) {
|
||||
done <- result{false}
|
||||
return
|
||||
}
|
||||
done <- result{strings.HasPrefix(string(buf[:n]), "HTTP/")}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
return r.ok
|
||||
case <-time.After(probeTimeout):
|
||||
// The tunnel goroutine is left to unwind on its own; the wire simply
|
||||
// did not answer in time, which is all the caller needs to know.
|
||||
} else if choice.mode == WireBP {
|
||||
opts := s.binOpts
|
||||
opts.headerMask = choice.mask
|
||||
opts.coverProfile = choice.cover
|
||||
opts.skipPathProbe = true
|
||||
opts.minSize = 32
|
||||
opts.startSize = 32
|
||||
opts.maxSize = 32
|
||||
conn, err = openBPTunnel(s.serverAddr, s.token, probeHost, probePort, opts)
|
||||
} else {
|
||||
opts := s.binOpts
|
||||
opts.headerMask = choice.mask
|
||||
opts.coverProfile = choice.cover
|
||||
opts.skipPathProbe = true
|
||||
opts.minSize = 32
|
||||
opts.startSize = 32
|
||||
opts.maxSize = 32
|
||||
conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(probeTimeout))
|
||||
|
||||
request := "GET / HTTP/1.1\r\nHost: " + probeHost + "\r\nUser-Agent: dragontcp\r\nConnection: close\r\n\r\n"
|
||||
if _, err := conn.Write([]byte(request)); err != nil {
|
||||
return false
|
||||
}
|
||||
statusLine, err := bufio.NewReader(conn).ReadString('\n')
|
||||
return err == nil && strings.HasPrefix(statusLine, "HTTP/")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProfileCandidatesCoverBothWireFamilies(t *testing.T) {
|
||||
selector := &wireSelector{configured: WireAuto}
|
||||
candidates := selector.profileCandidates()
|
||||
binaryCount, bpCount, xorCount := 0, 0, 0
|
||||
seen := make(map[wireChoice]bool, len(candidates))
|
||||
firstBytes := make(map[byte]bool, 256)
|
||||
coveredMasks := map[string]map[byte]bool{WireBinary: {}, WireBP: {}, WireXOR: {}}
|
||||
coveredPadding := map[string]map[uint16]bool{WireBinary: {}, WireBP: {}, WireXOR: {}}
|
||||
for _, candidate := range candidates {
|
||||
if seen[candidate] {
|
||||
t.Fatalf("duplicate candidate: %s", candidate)
|
||||
}
|
||||
seen[candidate] = true
|
||||
if candidate.cover.Enabled {
|
||||
coveredMasks[candidate.mode][candidate.mask] = true
|
||||
coveredPadding[candidate.mode][candidate.cover.Padding] = true
|
||||
}
|
||||
switch candidate.mode {
|
||||
case WireBinary:
|
||||
binaryCount++
|
||||
if !candidate.cover.Enabled && candidate.mask&7 != 0 {
|
||||
t.Fatalf("ambiguous binary mask: %02x", candidate.mask)
|
||||
}
|
||||
if candidate.cover.Enabled {
|
||||
firstBytes[byte(candidate.cover.ID>>8)] = true
|
||||
} else {
|
||||
for mode := byte(0); mode <= 4; mode++ {
|
||||
firstBytes[mode^candidate.mask] = true
|
||||
}
|
||||
}
|
||||
case WireXOR:
|
||||
xorCount++
|
||||
if !candidate.cover.Enabled && ('U'^candidate.mask)&7 < 5 {
|
||||
t.Fatalf("ambiguous XOR mask: %02x", candidate.mask)
|
||||
}
|
||||
if candidate.cover.Enabled {
|
||||
firstBytes[byte(candidate.cover.ID>>8)] = true
|
||||
} else {
|
||||
firstBytes['U'^candidate.mask] = true
|
||||
}
|
||||
case WireBP:
|
||||
bpCount++
|
||||
if candidate.cover.Enabled && !candidate.cover.Clear {
|
||||
t.Fatalf("covered BP profile must use clear payloads: %s", candidate)
|
||||
}
|
||||
if !candidate.cover.Enabled && candidate.mask != 0 {
|
||||
t.Fatalf("direct BP profile must keep a clear header: %s", candidate)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unknown candidate: %s", candidate)
|
||||
}
|
||||
}
|
||||
if binaryCount != 544 || bpCount != 257 || xorCount != 352 {
|
||||
t.Fatalf("profiles B=%d BP=%d X=%d, want B=544 BP=257 X=352", binaryCount, bpCount, xorCount)
|
||||
}
|
||||
if len(firstBytes) != 256 {
|
||||
t.Fatalf("profiles cover %d first-byte values, want 256", len(firstBytes))
|
||||
}
|
||||
for _, mode := range []string{WireBinary, WireBP, WireXOR} {
|
||||
if len(coveredMasks[mode]) != 256 {
|
||||
t.Fatalf("mode %s covers %d masks, want 256", mode, len(coveredMasks[mode]))
|
||||
}
|
||||
if len(coveredPadding[mode]) != 16 {
|
||||
t.Fatalf("mode %s covers %d padding lengths, want 16", mode, len(coveredPadding[mode]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualWireStillDiscoversAllProfilesForThatFamily(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
mode string
|
||||
want int
|
||||
}{{WireBinary, 544}, {WireBP, 257}, {WireXOR, 352}} {
|
||||
selector := &wireSelector{configured: tc.mode}
|
||||
candidates := selector.profileCandidates()
|
||||
if len(candidates) != tc.want {
|
||||
t.Fatalf("mode %s profiles=%d, want %d", tc.mode, len(candidates), tc.want)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if candidate.mode != tc.mode {
|
||||
t.Fatalf("mode %s included %s", tc.mode, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearProfilesAreTriedBeforeLegacyFallbacks(t *testing.T) {
|
||||
for _, mode := range []string{WireBinary, WireBP} {
|
||||
candidates := (&wireSelector{configured: mode}).profileCandidates()
|
||||
if len(candidates) < 2 || !candidates[0].cover.Clear {
|
||||
t.Fatalf("mode %s does not prefer a clear profile", mode)
|
||||
}
|
||||
if candidates[1].cover.Enabled {
|
||||
t.Fatalf("mode %s does not fall back immediately to a legacy direct profile", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func measureDiscoveryConcurrency(t *testing.T, threads int) int32 {
|
||||
t.Helper()
|
||||
candidates := make([]wireChoice, 24)
|
||||
for i := range candidates {
|
||||
candidates[i] = wireChoice{mode: WireBinary, mask: byte(i * 8)}
|
||||
}
|
||||
var active atomic.Int32
|
||||
var maximum atomic.Int32
|
||||
var calls atomic.Int32
|
||||
selector := &wireSelector{
|
||||
configured: WireAuto,
|
||||
candidateOverride: candidates,
|
||||
probeThreads: threads,
|
||||
probeDelay: time.Nanosecond,
|
||||
probeOverride: func(wireChoice) bool {
|
||||
current := active.Add(1)
|
||||
for {
|
||||
old := maximum.Load()
|
||||
if current <= old || maximum.CompareAndSwap(old, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
calls.Add(1)
|
||||
// Keep attempts alive long enough for the globally spaced scheduler
|
||||
// to fill every configured worker reliably on slower CI runners.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
active.Add(-1)
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
if _, ok := selector.detectLocked(); ok {
|
||||
t.Fatal("unexpected working profile")
|
||||
}
|
||||
if calls.Load() != int32(len(candidates)) {
|
||||
t.Fatalf("screened=%d, want %d", calls.Load(), len(candidates))
|
||||
}
|
||||
return maximum.Load()
|
||||
}
|
||||
|
||||
func TestDiscoveryDefaultsToOneWorker(t *testing.T) {
|
||||
if maximum := measureDiscoveryConcurrency(t, 0); maximum != 1 {
|
||||
t.Fatalf("maximum concurrent probes=%d, want 1", maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoveryHonorsConfiguredWorkers(t *testing.T) {
|
||||
if maximum := measureDiscoveryConcurrency(t, 4); maximum != 4 {
|
||||
t.Fatalf("maximum concurrent probes=%d, want 4", maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedManualDiscoveryKeepsPinnedWire(t *testing.T) {
|
||||
for _, mode := range []string{WireBinary, WireBP, WireXOR} {
|
||||
selector := &wireSelector{
|
||||
configured: mode,
|
||||
candidateOverride: []wireChoice{{mode: mode}},
|
||||
probeDelay: time.Nanosecond,
|
||||
probeOverride: func(wireChoice) bool { return false },
|
||||
}
|
||||
if choice := selector.mode(); choice.mode != mode {
|
||||
t.Fatalf("configured=%s fallback=%s", mode, choice.mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user