package main import ( "bytes" "crypto/rand" "encoding/binary" "fmt" "io" "net" "sort" "sync" "sync/atomic" "time" "dragontcp/internal/cover" "dragontcp/internal/protocol" "dragontcp/internal/wire" ) type chunkClientOptions struct { startSize int minSize int maxSize int adaptive bool adaptSuccesses int shrinkAfter int shrinkStep int adaptLog bool pollers int reconnectEvery int pollDelay time.Duration txnTimeout time.Duration tcpBuffer int minPipeline int maxPipeline int headerMask byte coverProfile cover.Profile skipPathProbe bool forceMaxStart bool } type adaptiveSizer struct { mu sync.Mutex name string current int min int max int adaptive bool adaptSuccesses int shrinkAfter int shrinkStep int failures int successes int good int bad int logChanges bool } func newAdaptiveSizer(name string, start int, opts chunkClientOptions) *adaptiveSizer { if start < opts.minSize { start = opts.minSize } if start > opts.maxSize { start = opts.maxSize } return &adaptiveSizer{ name: name, current: start, min: opts.minSize, max: opts.maxSize, adaptive: opts.adaptive, adaptSuccesses: func() int { if opts.adaptSuccesses > 0 { return opts.adaptSuccesses } return 64 }(), shrinkAfter: func() int { if opts.shrinkAfter > 0 { return opts.shrinkAfter } return 1 }(), shrinkStep: opts.shrinkStep, logChanges: opts.adaptLog, } } func (s *adaptiveSizer) Current() int { s.mu.Lock() defer s.mu.Unlock() return s.current } func (s *adaptiveSizer) Success(attempted int) { s.mu.Lock() defer s.mu.Unlock() if !s.adaptive || attempted != s.current { return } // A real success proves the current size still works. In max-first mode this // resets the consecutive-failure budget so isolated carrier errors never // cause a downshift. s.failures = 0 if s.current >= s.max { return } if attempted > s.good { s.good = attempted } s.successes++ growAfter := s.adaptSuccesses if s.bad > 0 && s.bad-s.good <= 64 { growAfter *= 8 } if s.successes < growAfter { return } s.successes = 0 old := s.current next := 0 if s.bad > old+1 { next = old + (s.bad-old)/2 } else { if s.bad > 0 { s.bad = 0 } step := old / 4 if step < 32 { step = 32 } next = old + step } if next > s.max { next = s.max } if next <= old { return } s.current = next if s.logChanges { fmt.Printf("adaptive %s chunk: %d -> %d after stable success\n", s.name, old, next) } } 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 if !s.adaptive || attempted != s.current { return old, old } s.successes = 0 s.failures++ if s.failures < s.shrinkAfter { if s.logChanges && s.shrinkAfter > 1 { fmt.Printf("adaptive %s chunk: holding %d after failure %d/%d: %v\n", s.name, old, s.failures, s.shrinkAfter, cause) } return old, old } s.failures = 0 if s.bad == 0 || attempted < s.bad { s.bad = attempted } next := attempted / 2 if s.shrinkStep > 0 { next = attempted - s.shrinkStep // MAX-FIRST linear downgrade intentionally ignores an older known-good // point: the goal is to walk down in small deterministic steps instead // of making a large jump after a transient carrier rejection. s.good = 0 } else if s.good > 0 && s.good < attempted { next = s.good } else { s.good = 0 } if next < s.min { next = s.min } if next >= attempted && attempted > s.min { next = attempted - 1 } if next < s.min { next = s.min } s.current = next if s.logChanges && 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 } type physicalConn struct { conn net.Conn requests int } type requestLane struct { mu sync.Mutex serverAddr string 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, 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() l.pc = nil } } func (l *requestLane) closeAfterLocked() { if l.pc != nil && l.reconnectEvery > 0 && l.pc.requests >= l.reconnectEvery { l.discardLocked() } } func (l *requestLane) 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 = &physicalConn{conn: conn} return nil } func (l *requestLane) Close() { l.mu.Lock() l.closed = true l.discardLocked() l.mu.Unlock() } func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte) (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 { // A dial timeout is a dead path for this logical tunnel. Do not // spend another timeout replaying the same request. if isTransportTimeout(err) { return 0, nil, err } 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 { l.transportFailureLocked(reused) if isTransportTimeout(err) { return 0, nil, err } lastErr = err continue } status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask) if err != nil { l.transportFailureLocked(reused) if isTransportTimeout(err) { return 0, nil, err } lastErr = err 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 } return 0, nil, fmt.Errorf("request failed after reconnect: %w", lastErr) } // download sends one compact request and consumes up to count response records. // startOffset is also the response keystream sequence. Each DATA response advances // it by exactly the returned byte count. func (l *requestLane) download(sid wire.SessionID, startOffset, ackOffset uint64, maxChunk, count int) ([][]byte, byte, error) { l.mu.Lock() defer l.mu.Unlock() timeout := l.timeout if timeout <= 0 { timeout = 5 * time.Second } 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)) var lastErr error for attempt := 0; attempt < 2; attempt++ { if err := l.ensureLocked(); err != nil { if isTransportTimeout(err) { return nil, 0, err } lastErr = err continue } 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 { l.transportFailureLocked(reused) if isTransportTimeout(err) { return nil, 0, err } lastErr = err 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 { l.transportFailureLocked(reused) if isTransportTimeout(err) { return nil, 0, err } lastErr = err 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("%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 retry: } return nil, 0, fmt.Errorf("download request failed after reconnect: %w", lastErr) } type pathProfile struct { upload int download int persistent bool at time.Time } var profileState struct { sync.Mutex key string p pathProfile } var probeSeq atomic.Uint64 func randomSessionID() (wire.SessionID, error) { var sid wire.SessionID _, err := rand.Read(sid[:]) return sid, err } func probePattern(n int) []byte { out := make([]byte, n) for i := range out { out[i] = byte((i*31 + 17) & 0xff) } return out } func makeProbePayload(kind byte, value, total int, token string) []byte { base := 11 + len(token) if total < base { total = base } out := make([]byte, total) copy(out[:4], wire.ProbeMagic[:]) out[4] = kind binary.BigEndian.PutUint16(out[5:7], uint16(len(token))) binary.BigEndian.PutUint32(out[7:11], uint32(value)) copy(out[11:11+len(token)], token) for i := base; i < len(out); i++ { out[i] = byte((i*31 + 17) & 0xff) } return out } func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int) bool { sid, err := randomSessionID() if err != nil { return false } timeout := opts.txnTimeout if timeout <= 0 || timeout > 2500*time.Millisecond { timeout = 2500 * time.Millisecond } lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile) defer lane.Close() seq := probeSeq.Add(1) total := 0 value := candidate if kind == wire.ProbeUpload { total = candidate } payload := makeProbePayload(kind, value, total, token) status, body, err := lane.single(wire.ModeProbe, sid, seq, payload) if err != nil { return false } if kind == wire.ProbeUpload { return status == wire.StatusOK } if kind == wire.ProbeDownload { if status != wire.StatusData || len(body) != candidate { return false } want := probePattern(candidate) for i := range body { if body[i] != want[i] { return false } } return true } return status == wire.StatusOK } type iperfProbeResult struct { ok bool bytes int elapsed time.Duration err error } func (r iperfProbeResult) mbps() float64 { if r.elapsed <= 0 || r.bytes <= 0 { return 0 } return (float64(r.bytes) * 8) / r.elapsed.Seconds() / 1_000_000 } func calibrationDeadline(opts chunkClientOptions) time.Duration { t := opts.txnTimeout if t < 8*time.Second { t = 8 * time.Second } if t > 20*time.Second { t = 20 * time.Second } return t } func dialProbeConn(serverAddr string, opts chunkClientOptions) (net.Conn, error) { d := net.Dialer{Timeout: minDuration(calibrationDeadline(opts), 10*time.Second), KeepAlive: 30 * time.Second} conn, err := d.Dial("tcp", serverAddr) if err != nil { return nil, err } if err := cover.WritePreface(conn, opts.coverProfile); err != nil { _ = conn.Close() return nil, err } protocol.TuneTCP(conn) protocol.TuneTCPBuffer(conn, opts.tcpBuffer) _ = conn.SetDeadline(time.Now().Add(calibrationDeadline(opts))) return conn, nil } // probeIperfOne validates one UP or DW record at a time against the DragonTCP // server. Calibration is intentionally single-poller and never pipelines // multiple outstanding records: it measures a single lane's safe record size, // not aggregate throughput. Boundary confirmation retries happen sequentially // on fresh connections. func probeIperfOne(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int) iperfProbeResult { count := wire.ProbeBurstCount(candidate) totalBytes := candidate * count conn, err := dialProbeConn(serverAddr, opts) if err != nil { return iperfProbeResult{err: err} } defer conn.Close() sid, err := randomSessionID() if err != nil { return iperfProbeResult{err: err} } startSeq := probeSeq.Add(uint64(count)) - uint64(count) + 1 started := time.Now() switch kind { case wire.ProbeUpload: payload := makeProbePayload(wire.ProbeIperfUpload, candidate, candidate, token) totalBytes = len(payload) * count for i := 0; i < count; i++ { seq := startSeq + uint64(i) if err := wire.WriteRequestProfileEncoding(conn, wire.ModeProbe, sid, seq, payload, opts.headerMask, opts.coverProfile.Clear); err != nil { return iperfProbeResult{bytes: i * len(payload), elapsed: time.Since(started), err: err} } } for i := 0; i < count; i++ { status, body, err := wire.ReadResponseProfile(conn, opts.headerMask) if err != nil { return iperfProbeResult{bytes: totalBytes, elapsed: time.Since(started), err: err} } if status != wire.StatusOK { return iperfProbeResult{bytes: totalBytes, elapsed: time.Since(started), err: fmt.Errorf("upload status=%d body=%s", status, string(body))} } } return iperfProbeResult{ok: true, bytes: totalBytes, elapsed: time.Since(started)} case wire.ProbeDownload: payload := makeProbePayload(wire.ProbeIperfDownload, candidate, 0, token) if err := wire.WriteRequestProfileEncoding(conn, wire.ModeProbe, sid, startSeq, payload, opts.headerMask, opts.coverProfile.Clear); err != nil { return iperfProbeResult{elapsed: time.Since(started), err: err} } want := probePattern(candidate) gotBytes := 0 for i := 0; i < count; i++ { status, body, err := wire.ReadResponseProfile(conn, opts.headerMask) if err != nil { return iperfProbeResult{bytes: gotBytes, elapsed: time.Since(started), err: err} } seq := startSeq + uint64(i) if status != wire.StatusError && len(body) > 0 && !opts.coverProfile.Clear { body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeProbe, seq) } if status != wire.StatusData || !bytes.Equal(body, want) { return iperfProbeResult{bytes: gotBytes, elapsed: time.Since(started), err: fmt.Errorf("download validation failed status=%d len=%d want=%d", status, len(body), candidate)} } gotBytes += len(body) } return iperfProbeResult{ok: true, bytes: gotBytes, elapsed: time.Since(started)} default: return iperfProbeResult{err: fmt.Errorf("unknown iperf probe kind %d", kind)} } } func minDuration(a, b time.Duration) time.Duration { if a < b { return a } return b } func probePersistent(serverAddr, token string, opts chunkClientOptions) bool { sid, err := randomSessionID() if err != nil { return false } timeout := opts.txnTimeout if timeout <= 0 || timeout > 2500*time.Millisecond { timeout = 2500 * time.Millisecond } lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout, opts.headerMask, opts.coverProfile) defer lane.Close() for i := 0; i < 8; i++ { seq := probeSeq.Add(1) payload := makeProbePayload(wire.ProbeKeepalive, i, 32+len(token), token) status, _, err := lane.single(wire.ModeProbe, sid, seq, payload) if err != nil || status != wire.StatusOK { return false } } return true } func probeCandidates(minSize, maxSize int) []int { base := []int{32, 64, 128, 256, 512, 1024, 1200, 1280, 1320, 1350, 1360, 1380, 1400, 1450, 1600, 2048, 3205, 4096, 8192, 16384, 32768, 65536, 98304, 131072, 262144, 524288, 786432, 1048576} seen := map[int]bool{} out := make([]int, 0, len(base)+2) for _, n := range base { if n >= minSize && n <= maxSize && !seen[n] { out = append(out, n) seen[n] = true } } if !seen[minSize] { out = append(out, minSize) } if !seen[maxSize] { out = append(out, maxSize) } sort.Ints(out) return out } const calibrationFineResolution = 32 const calibrationDecisionAttempts = 3 // confirmIperfResult makes boundary decisions resistant to one transient // carrier hiccup. The caller provides the first observation; this function // opens fresh probe connections until either success or non-timeout failure has // a 2-of-3 majority. Connection-level timeouts are inconclusive and never count // as proof that a chunk size is too large. func confirmIperfResult(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int, first iperfProbeResult, stage string) iperfProbeResult { name := probeKindName(kind) successes, failures := 0, 0 var lastSuccess, lastFailure, lastTimeout iperfProbeResult observe := func(r iperfProbeResult) { if r.ok { successes++ lastSuccess = r return } if isTransportTimeout(r.err) { lastTimeout = r return } failures++ lastFailure = r } observe(first) for attempt := 2; attempt <= calibrationDecisionAttempts && successes < 2 && failures < 2; attempt++ { r := probeIperfOne(serverAddr, token, opts, kind, candidate) observe(r) result := "failure" if r.ok { result = "success" } else if isTransportTimeout(r.err) { result = "connection_timeout" } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=%s chunk=%d confirmation=%d/%d result=%s\n", name, stage, candidate, attempt, calibrationDecisionAttempts, result) } if successes >= 2 { return lastSuccess } if failures >= 2 { return lastFailure } // No majority means connection health was too unstable to classify the // candidate. Preserve the old timeout semantics by reporting the timeout and // keeping the last known-good size. if lastTimeout.err != nil { return lastTimeout } if successes > failures && lastSuccess.ok { return lastSuccess } return lastFailure } func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte) int { name := probeKindName(kind) // Startup calibration precision is independent from the runtime shrink step. // Grow quickly, then resolve only the final good/bad boundary to 32 bytes. fine := calibrationFineResolution // Ascending calibration deliberately starts small and grows geometrically in large steps. // This avoids hammering a constrained carrier with 1 MiB records before we // know they are viable, while still reaching the ceiling in O(log N) probes. // Once the first failure is found, binary refinement resolves the highest // known-good size to roughly `fine` bytes (32 B in the Android build). good := 0 bad := 0 candidate := opts.minSize if candidate < 32 { candidate = 32 } if candidate > opts.maxSize { candidate = opts.maxSize } for { result := probeIperfOne(serverAddr, token, opts, kind, candidate) if result.ok { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps()) good = candidate if candidate >= opts.maxSize { return opts.maxSize } next := candidate * 4 // Do not waste many tiny probes when the configured floor is very // small. After proving the floor, jump to at least 512 B and then // continue growing by 4x. if candidate == opts.minSize && next < 512 && opts.maxSize >= 512 { next = 512 } if next > opts.maxSize { next = opts.maxSize } if next <= candidate { return good } fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=ascend chunk_upgrade=%d->%d\n", name, candidate, next) candidate = next continue } // A first failure may be transient. Re-test this exact size on fresh // connections before declaring the ascending ceiling. result = confirmIperfResult(serverAddr, token, opts, kind, candidate, result, "ascend-confirm") if result.ok { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d result=recovered_after_retry\n", name, candidate) good = candidate if candidate >= opts.maxSize { return opts.maxSize } next := candidate * 4 if candidate == opts.minSize && next < 512 && opts.maxSize >= 512 { next = 512 } if next > opts.maxSize { next = opts.maxSize } if next <= candidate { return good } fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=ascend chunk_upgrade=%d->%d\n", name, candidate, next) candidate = next continue } if isTransportTimeout(result.err) { // A real I/O timeout means the physical connection is dead. It is not // evidence that this record size is invalid, so stop calibration at // the last proven size instead of walking the size ladder. selected := good if selected == 0 { selected = opts.minSize } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, candidate, selected, result.err) return selected } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps(), result.err) bad = candidate if good == 0 { return opts.minSize } break } for bad-good > fine { next := good + (bad-good)/2 if next <= good || next >= bad { break } result := probeIperfOne(serverAddr, token, opts, kind, next) result = confirmIperfResult(serverAddr, token, opts, kind, next, result, "refine-confirm") if result.ok { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps()) good = next continue } if isTransportTimeout(result.err) { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, next, good, result.err) return good } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps(), result.err) bad = next } fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=refine selected=%d failed_above=%d resolution=%d\n", name, good, bad, fine) return good } func probeKindName(kind byte) string { if kind == wire.ProbeDownload { return "download" } return "upload" } const ( // A literal 200-byte descent from 1 MiB can require more than five thousand // carrier probes per direction. MAX-FIRST therefore finds a working region // with a bounded coarse descent, then resolves the final good/bad boundary to // approximately 32 bytes. This preserves fine sizing without probe floods. maxFirstCoarseStep = 128 * 1024 maxFirstFineResolution = calibrationFineResolution ) // probeMaximumMaxFirst always validates the configured ceiling first. On a // recoverable size rejection it descends by 128 KiB until it finds a known-good // size, then binary-refines the interval between that good size and the nearest // failed size to <= 32 bytes. A connection-level timeout is never interpreted // as evidence that the chunk is too large, so it does not trigger a size sweep. func probeMaximumMaxFirst(serverAddr, token string, opts chunkClientOptions, kind byte) int { fine := maxFirstFineResolution candidate := opts.maxSize if candidate < opts.minSize { candidate = opts.minSize } name := probeKindName(kind) failedHigh := 0 // Coarse descent: at most about eight probes from 1 MiB to the bottom of // the normal range, instead of thousands of 200-byte requests. for { result := probeIperfOne(serverAddr, token, opts, kind, candidate) if result.ok { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps()) if failedHigh == 0 { return candidate } break } if isTransportTimeout(result.err) { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d result=connection_timeout action=keep_size_and_replace_connection err=%v\n", name, candidate, result.err) return candidate } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps(), result.err) failedHigh = candidate if candidate <= opts.minSize { return opts.minSize } next := candidate - maxFirstCoarseStep if next < opts.minSize { next = opts.minSize } if next >= candidate { next = opts.minSize } fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=coarse chunk_downgrade=%d->%d step=%d\n", name, candidate, next, maxFirstCoarseStep) candidate = next } // Fine boundary search. candidate is known-good and failedHigh is known-bad. good := candidate bad := failedHigh for bad-good > fine { next := good + (bad-good)/2 if next <= good { break } result := probeIperfOne(serverAddr, token, opts, kind, next) if result.ok { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps()) good = next continue } if isTransportTimeout(result.err) { fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, next, good, result.err) return good } fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps(), result.err) bad = next } fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=fine selected=%d failed_above=%d resolution=%d\n", name, good, bad, fine) return good } func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile { key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t|maxfirst=%t|shrink=%d|step=%d", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear, opts.forceMaxStart, opts.shrinkAfter, opts.shrinkStep) profileState.Lock() if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute { p := profileState.p profileState.Unlock() return p } profileState.Unlock() fallbackUp := minInt(opts.maxSize, maxInt(opts.minSize, 32768)) fallbackDown := minInt(opts.maxSize, maxInt(opts.minSize, 1350)) p := pathProfile{upload: fallbackUp, download: fallbackDown, persistent: false, at: time.Now()} if opts.forceMaxStart { // Legacy CLI-only mode retained for compatibility. The Android app no // longer exposes MAX FIRST and uses the ascending path below. p.upload = probeMaximumMaxFirst(serverAddr, token, opts, wire.ProbeUpload) p.download = probeMaximumMaxFirst(serverAddr, token, opts, wire.ProbeDownload) } else { // Keep UP and DW calibration sequential. Running both fake-iperf probes // together can look like a traffic burst and distort the carrier limit we // are trying to measure. p.upload = probeMaximum(serverAddr, token, opts, wire.ProbeUpload) p.download = probeMaximum(serverAddr, token, opts, wire.ProbeDownload) } p.persistent = probePersistent(serverAddr, token, opts) fmt.Printf("path probe: strategy=%s upload=%d download=%d persistent=%t\n", func() string { if opts.forceMaxStart { return "max-first" } return "ascending" }(), p.upload, p.download, p.persistent) profileState.Lock() profileState.key = key profileState.p = p profileState.Unlock() return p } func encodeOpen(token, host string, port int) ([]byte, error) { if len(token) > 65535 || len(host) > 65535 { return nil, fmt.Errorf("token or hostname too long") } out := make([]byte, 6+len(token)+len(host)) binary.BigEndian.PutUint16(out[0:2], uint16(len(token))) binary.BigEndian.PutUint16(out[2:4], uint16(len(host))) binary.BigEndian.PutUint16(out[4:6], uint16(port)) copy(out[6:6+len(token)], token) copy(out[6+len(token):], host) return out, nil } type chunkConn struct { sid wire.SessionID opts chunkClientOptions uploadLane *requestLane downloadLane *requestLane serverMax int upSizer *adaptiveSizer downSizer *adaptiveSizer writeMu sync.Mutex upOffset uint64 readMu sync.Mutex readBuf []byte downloadOffset uint64 consumedOffset uint64 eof bool pipeline int minPipeline int maxPipeline int closeOnce sync.Once dead atomic.Bool } // 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 } if opts.maxSize < opts.minSize { opts.maxSize = opts.minSize } if opts.maxSize > 1024*1024 { opts.maxSize = 1024 * 1024 } if opts.txnTimeout <= 0 { opts.txnTimeout = 5 * time.Second } if opts.adaptSuccesses < 1 { opts.adaptSuccesses = 64 } if opts.shrinkAfter < 1 { opts.shrinkAfter = 1 } if opts.forceMaxStart { opts.startSize = opts.maxSize } if opts.reconnectEvery < 0 { opts.reconnectEvery = 0 } 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 } defaultStart := opts.minSize if opts.forceMaxStart { defaultStart = opts.maxSize } profile := pathProfile{ upload: defaultStart, download: defaultStart, 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: start persistent, then learn one request/connection only if // reuse fails during real traffic // N>=2 = force connection rotation after N logical requests sid, err := randomSessionID() if err != nil { return nil, err } // 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, opts.headerMask, opts.coverProfile) payload, err := encodeOpen(token, targetHost, targetPort) if err != nil { uploadLane.Close() return nil, err } status, body, err := uploadLane.single(wire.ModeOpen, sid, 0, payload) 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 || len(body) != 4 { uploadLane.Close() return nil, fmt.Errorf("bad OPEN response") } serverMax := int(binary.BigEndian.Uint32(body)) if serverMax < opts.minSize { uploadLane.Close() return nil, fmt.Errorf("server maximum chunk %d is below client minimum %d", serverMax, opts.minSize) } if opts.maxSize > serverMax { opts.maxSize = serverMax } upStart := minInt(profile.upload, opts.maxSize) downStart := minInt(profile.download, opts.maxSize) if upStart < opts.minSize { upStart = opts.minSize } if downStart < opts.minSize { downStart = opts.minSize } c := &chunkConn{ sid: sid, opts: opts, serverMax: serverMax, uploadLane: uploadLane, 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 // what paths that only work at one specific batch size need. pipeline: opts.maxPipeline, minPipeline: opts.minPipeline, maxPipeline: opts.maxPipeline, } upSizerOpts := opts downSizerOpts := opts if !opts.skipPathProbe { // Calibration is the path ceiling for this VPN session. Runtime // adaptation may shrink after repeated failures and recover to this // value, but it must not grow above a size the UP/DW test did not prove. upSizerOpts.maxSize = upStart downSizerOpts.maxSize = downStart } c.upSizer = newAdaptiveSizer("upload", upStart, upSizerOpts) c.downSizer = newAdaptiveSizer("download", downStart, downSizerOpts) return c, nil } func (c *chunkConn) fillReadBuffer() error { if c.eof { return io.EOF } minFailures := 0 for len(c.readBuf) == 0 && !c.eof { chunk := c.downSizer.Current() count := c.pipeline if count < c.minPipeline { count = c.minPipeline } if count > c.maxPipeline { count = c.maxPipeline } // Bound each batch to roughly 1 MiB of useful data, but never below the // configured floor: a pinned depth is a path requirement, not a hint. if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count { count = maxInt(maxCount, c.minPipeline) } 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.downloadOffset += uint64(len(part)) } if len(data) > 0 { c.downSizer.Success(chunk) if c.pipeline < c.maxPipeline { c.pipeline++ } minFailures = 0 } if err != nil { if isTransportTimeout(err) { c.dead.Store(true) return err } if c.pipeline > c.minPipeline { old := c.pipeline c.pipeline /= 2 if c.pipeline < c.minPipeline { c.pipeline = c.minPipeline } if c.opts.adaptLog && old != c.pipeline { fmt.Printf("adaptive download pipeline: %d -> %d after transport failure: %v\n", old, c.pipeline, err) } } else { old, next := c.downSizer.FailureReason(chunk, err) if old == next && next == c.opts.minSize { minFailures++ if minFailures >= 8 { return fmt.Errorf("download failed at minimum chunk %d: %w", next, err) } } } time.Sleep(30 * time.Millisecond) if len(c.readBuf) > 0 { return nil } continue } switch status { case wire.StatusEOF: c.eof = true case wire.StatusWait: if c.opts.pollDelay > 0 { time.Sleep(c.opts.pollDelay) } else { time.Sleep(5 * time.Millisecond) } } if len(c.readBuf) > 0 { return nil } } if c.eof && len(c.readBuf) == 0 { return io.EOF } return nil } func (c *chunkConn) 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 *chunkConn) Write(p []byte) (int, error) { c.writeMu.Lock() defer c.writeMu.Unlock() if len(p) == 0 { return 0, nil } total := 0 minFailures := 0 for len(p) > 0 { size := c.upSizer.Current() n := minInt(size, len(p)) status, body, err := c.uploadLane.single(wire.ModeUpload, c.sid, c.upOffset, p[:n]) if err != nil { if isTransportTimeout(err) { c.dead.Store(true) return total, err } old, next := c.upSizer.FailureReason(size, err) if old == next && next == c.opts.minSize { minFailures++ if minFailures >= 8 { return total, fmt.Errorf("upload failed at minimum chunk %d: %w", next, err) } } else { minFailures = 0 } 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 upload status %d", status) } c.upOffset += uint64(n) total += n p = p[n:] c.upSizer.Success(size) minFailures = 0 } return total, nil } func (c *chunkConn) Close() error { c.closeOnce.Do(func() { // A timed-out tunnel is already dead. Do not redial merely to send a // graceful CLOSE for a session that the caller is abandoning. if !c.dead.Load() { // Reuse the upload lane rather than dialling a connection just to say // goodbye; that was a second wasted dial per flow. _, _, _ = c.uploadLane.single(wire.ModeClose, c.sid, 0, nil) } c.uploadLane.Close() c.downloadLane.Close() }) return nil } func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-binary-local") } func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-binary-remote") } func (c *chunkConn) SetDeadline(time.Time) error { return nil } func (c *chunkConn) SetReadDeadline(time.Time) error { return nil } func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil } type dummyAddr string func (d dummyAddr) Network() string { return "dragontcp-binary" } func (d dummyAddr) String() string { return string(d) } func minInt(a, b int) int { if a < b { return a } return b } func maxInt(a, b int) int { if a > b { return a } return b }