Mult Port + TCP Calibration (SSH DEAD)

This commit is contained in:
2026-08-17 17:08:57 -03:00
parent 7ea221a99c
commit b997294607
58 changed files with 6033 additions and 497 deletions
+450 -35
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
@@ -22,6 +23,8 @@ type chunkClientOptions struct {
maxSize int
adaptive bool
adaptSuccesses int
shrinkAfter int
shrinkStep int
adaptLog bool
pollers int
reconnectEvery int
@@ -33,6 +36,7 @@ type chunkClientOptions struct {
headerMask byte
coverProfile cover.Profile
skipPathProbe bool
forceMaxStart bool
}
type adaptiveSizer struct {
@@ -43,6 +47,9 @@ type adaptiveSizer struct {
max int
adaptive bool
adaptSuccesses int
shrinkAfter int
shrinkStep int
failures int
successes int
good int
bad int
@@ -68,6 +75,13 @@ func newAdaptiveSizer(name string, start int, opts chunkClientOptions) *adaptive
}
return 64
}(),
shrinkAfter: func() int {
if opts.shrinkAfter > 0 {
return opts.shrinkAfter
}
return 1
}(),
shrinkStep: opts.shrinkStep,
logChanges: opts.adaptLog,
}
}
@@ -81,7 +95,14 @@ func (s *adaptiveSizer) Current() int {
func (s *adaptiveSizer) Success(attempted int) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.adaptive || attempted != s.current || s.current >= s.max {
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 {
@@ -135,11 +156,25 @@ func (s *adaptiveSizer) FailureReason(attempted int, cause error) (int, int) {
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.good > 0 && s.good < attempted {
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
@@ -470,6 +505,123 @@ func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, cand
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 {
@@ -512,28 +664,268 @@ func probeCandidates(minSize, maxSize int) []int {
return out
}
func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte) int {
candidates := probeCandidates(opts.minSize, opts.maxSize)
lo, hi := 0, len(candidates)-1
best := opts.minSize
for lo <= hi {
mid := lo + (hi-lo)/2
candidate := candidates[mid]
if probeOne(serverAddr, token, opts, kind, candidate) {
best = candidate
lo = mid + 1
} else {
hi = mid - 1
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
}
if best < opts.minSize {
best = opts.minSize
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)
}
return best
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", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear)
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
@@ -545,23 +937,27 @@ func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfi
fallbackUp := minInt(opts.maxSize, maxInt(opts.minSize, 32768))
fallbackDown := minInt(opts.maxSize, maxInt(opts.minSize, 1350))
upCh := make(chan int, 1)
downCh := make(chan int, 1)
go func() { upCh <- probeMaximum(serverAddr, token, opts, wire.ProbeUpload) }()
go func() { downCh <- probeMaximum(serverAddr, token, opts, wire.ProbeDownload) }()
p := pathProfile{upload: fallbackUp, download: fallbackDown, persistent: false, at: time.Now()}
select {
case p.upload = <-upCh:
case <-time.After(20 * time.Second):
}
select {
case p.download = <-downCh:
case <-time.After(20 * time.Second):
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: upload=%d download=%d persistent=%t\n", p.upload, p.download, p.persistent)
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
@@ -649,6 +1045,12 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
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
}
@@ -665,9 +1067,13 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
opts.minPipeline = opts.maxPipeline
}
defaultStart := opts.minSize
if opts.forceMaxStart {
defaultStart = opts.maxSize
}
profile := pathProfile{
upload: opts.minSize,
download: opts.minSize,
upload: defaultStart,
download: defaultStart,
persistent: false,
at: time.Now(),
}
@@ -738,8 +1144,17 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
minPipeline: opts.minPipeline,
maxPipeline: opts.maxPipeline,
}
c.upSizer = newAdaptiveSizer("upload", upStart, opts)
c.downSizer = newAdaptiveSizer("download", downStart, opts)
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
}