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
+73 -8
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
@@ -37,6 +38,7 @@ type bpLane struct {
tcpBuffer int
reconnectEvery int
timeout time.Duration
headerMask byte
coverProfile cover.Profile
autoReconnect bool
pc *bpPhysicalConn
@@ -44,6 +46,10 @@ type bpLane struct {
}
func newBPLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, coverProfile cover.Profile) *bpLane {
return newBPLaneWithMask(serverAddr, tcpBuffer, reconnectEvery, timeout, coverProfile.HeaderMask, coverProfile)
}
func newBPLaneWithMask(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *bpLane {
autoReconnect := reconnectEvery == 1
if autoReconnect {
reconnectEvery = 0
@@ -53,6 +59,7 @@ func newBPLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Du
tcpBuffer: tcpBuffer,
reconnectEvery: reconnectEvery,
timeout: timeout,
headerMask: headerMask,
coverProfile: coverProfile,
autoReconnect: autoReconnect,
}
@@ -172,7 +179,7 @@ func (l *bpLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byt
}
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 {
if err := writeBPRequest(l.pc.conn, mode, sid, seq, payload, downloadHint, l.headerMask, l.coverProfile.Clear); err != nil {
l.transportFailureLocked(reused)
if isTransportTimeout(err) {
return 0, nil, err
@@ -180,7 +187,7 @@ func (l *bpLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byt
lastErr = err
continue
}
status, body, err := readBPResponse(l.pc.conn, sid, mode, seq, l.coverProfile.HeaderMask, l.coverProfile.Clear)
status, body, err := readBPResponse(l.pc.conn, sid, mode, seq, l.headerMask, l.coverProfile.Clear)
if err != nil {
l.transportFailureLocked(reused)
if isTransportTimeout(err) {
@@ -197,6 +204,31 @@ func (l *bpLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byt
return 0, nil, fmt.Errorf("BP request failed after reconnect: %w", lastErr)
}
// probeBPProfile validates one BP header/cover profile with a single small
// BHP1 echo transaction. It deliberately does not register/open a target
// session; authenticated DTP2 calibration runs immediately after profile
// selection and remains authoritative for the configured server token.
func probeBPProfile(serverAddr string, opts chunkClientOptions) bool {
sid, err := randomSessionID()
if err != nil {
return false
}
timeout := opts.txnTimeout
if timeout <= 0 || timeout > profileProbeTimeout {
timeout = profileProbeTimeout
}
lane := newBPLaneWithMask(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
defer lane.Close()
payload := make([]byte, 10)
copy(payload[:4], []byte("BHP1"))
payload[4] = 1
payload[5] = bpModeUpload
binary.BigEndian.PutUint32(payload[6:10], 0)
status, body, err := lane.single(bpModeProbe, sid, 0, payload, 0)
return err == nil && status == wire.StatusOK && bytes.Equal(body, payload)
}
func decodeBPData(body []byte) ([]byte, error) {
if len(body) < 4 {
return nil, fmt.Errorf("short BP DATA body")
@@ -240,7 +272,7 @@ func (l *bpLane) download(sid wire.SessionID, offset uint64, maxChunk, count int
}
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 {
if err := writeBPRequest(l.pc.conn, mode, sid, offset, payload, hint, l.headerMask, l.coverProfile.Clear); err != nil {
l.transportFailureLocked(reused)
if isTransportTimeout(err) {
return nil, 0, err
@@ -252,7 +284,7 @@ func (l *bpLane) download(sid wire.SessionID, offset uint64, maxChunk, count int
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)
status, body, err := readBPResponse(l.pc.conn, sid, mode, offset, l.headerMask, l.coverProfile.Clear)
if err != nil {
l.transportFailureLocked(reused)
if isTransportTimeout(err) {
@@ -327,9 +359,15 @@ func openBPTunnel(serverAddr, token, targetHost string, targetPort int, opts chu
if opts.startSize < opts.minSize || opts.startSize > opts.maxSize {
opts.startSize = opts.maxSize
}
if opts.forceMaxStart {
opts.startSize = opts.maxSize
}
if opts.txnTimeout <= 0 {
opts.txnTimeout = 5 * time.Second
}
if opts.shrinkAfter < 1 {
opts.shrinkAfter = 1
}
if opts.maxPipeline < 1 {
opts.maxPipeline = 1
}
@@ -347,11 +385,24 @@ func openBPTunnel(serverAddr, token, targetHost string, targetPort int, opts chu
reconnect = 0
}
profile := pathProfile{
upload: opts.startSize,
download: opts.startSize,
persistent: false,
at: time.Now(),
}
if !opts.skipPathProbe {
// BP uses the same binary framing sizes and cover preface as the native B
// transport. Reuse the authenticated DTP2 probe machinery to calibrate
// the carrier before BP starts moving SSH/application data.
profile = getPathProfile(serverAddr, token, opts)
}
sid, err := randomSessionID()
if err != nil {
return nil, err
}
uploadLane := newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile)
uploadLane := newBPLaneWithMask(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile)
status, body, err := uploadLane.single(bpModeUpload, sid, 0, nil, 0)
if err != nil {
uploadLane.Close()
@@ -389,11 +440,25 @@ func openBPTunnel(serverAddr, token, targetHost string, targetPort int, opts chu
sid: sid,
opts: opts,
uploadLane: uploadLane,
downloadLane: newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile),
downloadLane: newBPLaneWithMask(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile),
pipeline: opts.maxPipeline,
}
c.upSizer = newAdaptiveSizer("BP upload", opts.startSize, opts)
c.downSizer = newAdaptiveSizer("BP download", opts.startSize, opts)
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
}
upSizerOpts := opts
downSizerOpts := opts
if !opts.skipPathProbe {
upSizerOpts.maxSize = upStart
downSizerOpts.maxSize = downStart
}
c.upSizer = newAdaptiveSizer("BP upload", upStart, upSizerOpts)
c.downSizer = newAdaptiveSizer("BP download", downStart, downSizerOpts)
return c, nil
}
+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
}
+417
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/binary"
"net"
"testing"
"time"
@@ -30,6 +31,165 @@ func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
}
}
func TestAdaptiveSizerWaitsForFailureBudgetBeforeShrinking(t *testing.T) {
opts := chunkClientOptions{
startSize: 1024,
minSize: 32,
maxSize: 1024,
adaptive: true,
shrinkAfter: 3,
}
s := newAdaptiveSizer("test", 1024, opts)
for i := 1; i <= 2; i++ {
old, next := s.Failure(1024)
if old != 1024 || next != 1024 || s.Current() != 1024 {
t.Fatalf("failure %d reduced early: old=%d next=%d current=%d", i, old, next, s.Current())
}
}
// Any successful record resets the consecutive-failure budget.
s.Success(1024)
for i := 1; i <= 2; i++ {
_, next := s.Failure(1024)
if next != 1024 {
t.Fatalf("post-success failure %d reduced early to %d", i, next)
}
}
_, next := s.Failure(1024)
if next != 512 || s.Current() != 512 {
t.Fatalf("third consecutive failure should reduce 1024 -> 512, next=%d current=%d", next, s.Current())
}
}
func TestForceMaxStartCalibratesAtCeiling(t *testing.T) {
profileState.Lock()
profileState.key = ""
profileState.p = pathProfile{}
profileState.Unlock()
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() {
defer close(serverErr)
for connection := 0; connection < 4; connection++ {
conn, err := ln.Accept()
if err != nil {
serverErr <- err
return
}
req, err := wire.ReadRequest(conn)
if err != nil {
_ = conn.Close()
serverErr <- err
return
}
if req.Mode == wire.ModeOpen {
body := make([]byte, 4)
binary.BigEndian.PutUint32(body, 1024)
err = wire.WriteMaskedResponse(conn, wire.StatusOK, body, req.Session, req.Mode, req.Seq)
_ = conn.Close()
if err != nil {
serverErr <- err
}
continue
}
if req.Mode != wire.ModeProbe || len(req.Payload) < 11 {
_ = conn.Close()
serverErr <- &testError{"expected calibration probe"}
return
}
kind := req.Payload[4]
candidate := int(binary.BigEndian.Uint32(req.Payload[7:11]))
switch kind {
case wire.ProbeIperfUpload:
count := wire.ProbeBurstCount(candidate)
for i := 0; i < count; i++ {
if i > 0 {
req, err = wire.ReadRequest(conn)
if err != nil {
break
}
}
if len(req.Payload) != candidate {
err = &testError{"wrong upload calibration size"}
break
}
if err = wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
break
}
}
case wire.ProbeIperfDownload:
for i := 0; i < wire.ProbeBurstCount(candidate); i++ {
if err = wire.WriteMaskedResponse(conn, wire.StatusData, probePattern(candidate), req.Session, wire.ModeProbe, req.Seq+uint64(i)); err != nil {
break
}
}
case wire.ProbeKeepalive:
for i := 0; i < 8; i++ {
if i > 0 {
req, err = wire.ReadRequest(conn)
if err != nil {
break
}
}
if err = wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
break
}
}
default:
err = &testError{"unexpected calibration kind"}
}
_ = conn.Close()
if err != nil {
serverErr <- err
return
}
}
}()
opts := chunkClientOptions{
startSize: 32,
minSize: 32,
maxSize: 1024,
adaptive: true,
shrinkAfter: 3,
minPipeline: 1,
maxPipeline: 1,
txnTimeout: time.Second,
forceMaxStart: true,
skipPathProbe: false,
}
conn, err := openChunkTunnel(ln.Addr().String(), "", "example.com", 443, opts)
if err != nil {
t.Fatal(err)
}
c := conn.(*chunkConn)
if got := c.upSizer.Current(); got != 1024 {
t.Fatalf("upload start=%d, want calibrated max 1024", got)
}
if got := c.downSizer.Current(); got != 1024 {
t.Fatalf("download start=%d, want calibrated max 1024", got)
}
c.uploadLane.Close()
c.downloadLane.Close()
for err := range serverErr {
if err != nil {
t.Fatal(err)
}
}
}
type testError struct{ message string }
func (e *testError) Error() string { return e.message }
func TestReconnectAutoLearnsFromRealReuseFailure(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
@@ -99,3 +259,260 @@ func TestBPAutoStartsPersistent(t *testing.T) {
t.Fatalf("BP auto lane started auto=%t reconnectEvery=%d", lane.autoReconnect, lane.reconnectEvery)
}
}
func TestAscendingIperfFindsBoundaryWithoutFlood(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
const threshold = 900000
attempts := make(chan int, 64)
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
defer close(attempts)
for {
conn, err := ln.Accept()
if err != nil {
return
}
req, err := wire.ReadRequest(conn)
if err != nil {
_ = conn.Close()
continue
}
candidate := int(binary.BigEndian.Uint32(req.Payload[7:11]))
attempts <- candidate
if candidate > threshold {
_ = wire.WriteResponse(conn, wire.StatusError, []byte("synthetic carrier rejection"))
_ = conn.Close()
continue
}
count := wire.ProbeBurstCount(candidate)
for i := 0; i < count; i++ {
if i > 0 {
req, err = wire.ReadRequest(conn)
if err != nil {
break
}
}
if err = wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
break
}
}
_ = conn.Close()
}
}()
opts := chunkClientOptions{
minSize: 32,
maxSize: 1024 * 1024,
shrinkAfter: 1,
shrinkStep: 200,
txnTimeout: time.Second,
coverProfile: cover.Profile{},
}
got := probeMaximum(ln.Addr().String(), "", opts, wire.ProbeUpload)
_ = ln.Close()
<-serverDone
if got > threshold {
t.Fatalf("calibrated size=%d exceeds threshold=%d", got, threshold)
}
if threshold-got > calibrationFineResolution {
t.Fatalf("calibrated size=%d is more than %d bytes below threshold=%d", got, calibrationFineResolution, threshold)
}
var seen []int
for candidate := range attempts {
seen = append(seen, candidate)
}
if len(seen) > 40 {
t.Fatalf("ascending calibration used %d probes, want <=40 with boundary confirmation; attempts=%v", len(seen), seen)
}
if len(seen) == 0 || seen[0] != 32 {
t.Fatalf("first ascending probe=%v, want minimum 32", seen)
}
if len(seen) < 2 || seen[1] != 512 {
t.Fatalf("second ascending probe=%v, want fast jump to 512", seen)
}
for i := 1; i < len(seen); i++ {
if seen[i] > threshold {
break
}
if seen[i] < seen[i-1] {
t.Fatalf("coarse ascending phase moved backward: %v", seen)
}
}
}
func TestAscendingIperfRetriesTransientFailureBeforeLoweringCeiling(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
const (
threshold = 4096
transientChunk = 2048
)
var transientAttempts int
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
for {
conn, err := ln.Accept()
if err != nil {
return
}
req, err := wire.ReadRequest(conn)
if err != nil {
_ = conn.Close()
continue
}
candidate := int(binary.BigEndian.Uint32(req.Payload[7:11]))
if candidate == transientChunk && transientAttempts == 0 {
transientAttempts++
_ = wire.WriteResponse(conn, wire.StatusError, []byte("synthetic transient rejection"))
_ = conn.Close()
continue
}
if candidate > threshold {
_ = wire.WriteResponse(conn, wire.StatusError, []byte("synthetic carrier rejection"))
_ = conn.Close()
continue
}
count := wire.ProbeBurstCount(candidate)
for i := 0; i < count; i++ {
if i > 0 {
req, err = wire.ReadRequest(conn)
if err != nil {
break
}
}
if err = wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
break
}
}
_ = conn.Close()
}
}()
opts := chunkClientOptions{
minSize: 32,
maxSize: 16 * 1024,
shrinkAfter: 1,
shrinkStep: 200,
txnTimeout: time.Second,
coverProfile: cover.Profile{},
}
got := probeMaximum(ln.Addr().String(), "", opts, wire.ProbeUpload)
_ = ln.Close()
<-serverDone
if transientAttempts != 1 {
t.Fatalf("transient failure count=%d, want 1", transientAttempts)
}
if got < transientChunk {
t.Fatalf("calibration collapsed below transiently failed %d-byte probe: got %d", transientChunk, got)
}
if got > threshold || threshold-got > calibrationFineResolution {
t.Fatalf("calibrated size=%d, want within %d bytes below threshold=%d", got, calibrationFineResolution, threshold)
}
}
func TestMaxFirstIperfUsesBoundedCoarseFineSearch(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
const threshold = 900000
attempts := make(chan int, 64)
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
defer close(attempts)
for {
conn, err := ln.Accept()
if err != nil {
return
}
req, err := wire.ReadRequest(conn)
if err != nil {
_ = conn.Close()
continue
}
candidate := int(binary.BigEndian.Uint32(req.Payload[7:11]))
attempts <- candidate
if candidate > threshold {
_ = wire.WriteResponse(conn, wire.StatusError, []byte("synthetic carrier rejection"))
_ = conn.Close()
continue
}
count := wire.ProbeBurstCount(candidate)
for i := 0; i < count; i++ {
if i > 0 {
req, err = wire.ReadRequest(conn)
if err != nil {
break
}
}
if err = wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
break
}
}
_ = conn.Close()
}
}()
opts := chunkClientOptions{
minSize: 32,
maxSize: 1024 * 1024,
shrinkAfter: 1,
shrinkStep: 200,
txnTimeout: time.Second,
coverProfile: cover.Profile{},
}
got := probeMaximumMaxFirst(ln.Addr().String(), "", opts, wire.ProbeUpload)
_ = ln.Close()
<-serverDone
if got > threshold {
t.Fatalf("calibrated size=%d exceeds threshold=%d", got, threshold)
}
if threshold-got > calibrationFineResolution {
t.Fatalf("calibrated size=%d is more than %d bytes below threshold=%d", got, calibrationFineResolution, threshold)
}
var seen []int
for candidate := range attempts {
seen = append(seen, candidate)
}
if len(seen) > 24 {
t.Fatalf("calibration used %d probes, want <=24; attempts=%v", len(seen), seen)
}
if len(seen) == 0 || seen[0] != 1024*1024 {
t.Fatalf("first probe=%v, want maximum %d", seen, 1024*1024)
}
}
func TestAdaptiveSizerLinearShrinkStep(t *testing.T) {
opts := chunkClientOptions{
startSize: 1024,
minSize: 256,
maxSize: 1024,
adaptive: true,
shrinkAfter: 1,
shrinkStep: 200,
}
s := newAdaptiveSizer("test", 1024, opts)
_, next := s.Failure(1024)
if next != 824 {
t.Fatalf("linear failure should reduce 1024 -> 824, got %d", next)
}
_, next = s.Failure(824)
if next != 624 {
t.Fatalf("linear failure should reduce 824 -> 624, got %d", next)
}
}
+210 -22
View File
@@ -357,12 +357,77 @@ func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer i
}
}
const serverPortReachabilityTimeout = 900 * time.Millisecond
func serverPortReachable(host string, port int, tcpBuffer int) bool {
d := net.Dialer{Timeout: serverPortReachabilityTimeout, KeepAlive: 30 * time.Second}
conn, err := d.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return false
}
protocol.TuneTCP(conn)
protocol.TuneTCPBuffer(conn, tcpBuffer)
_ = conn.Close()
return true
}
// selectServerEndpoint walks the user-configured port range in ascending order.
// A port is not considered working merely because the TCP handshake succeeds:
// the normal DragonTCP wire/header probe must also validate on that endpoint.
// Only one port candidate is protocol-tested at a time; UP/DW calibration starts
// only after a single endpoint has been locked.
func selectServerEndpoint(
host string,
portStart int,
portEnd int,
token string,
configuredWire string,
binOpts chunkClientOptions,
xorOpts xorchunk.Options,
probeDelay time.Duration,
probeThreads int,
forceClear bool,
tcpBuffer int,
) (*wireSelector, string, int, error) {
total := portEnd - portStart + 1
fmt.Printf("[D-TCP] phase=PORT_SCAN state=starting host=%s start=%d end=%d total=%d protocol_validation=true\n", host, portStart, portEnd, total)
for port := portStart; port <= portEnd; port++ {
index := port - portStart + 1
// Keep logs readable for wide ranges: always show the first candidate,
// every 16th candidate, and every TCP-reachable candidate.
if index == 1 || index%16 == 0 || port == portEnd {
fmt.Printf("[D-TCP] phase=PORT_SCAN state=testing candidate=%d progress=%d/%d\n", port, index, total)
}
if !serverPortReachable(host, port, tcpBuffer) {
continue
}
addr := net.JoinHostPort(host, strconv.Itoa(port))
fmt.Printf("[D-TCP] phase=PORT_SCAN state=tcp_reachable candidate=%d progress=%d/%d\n", port, index, total)
selector := newWireSelector(configuredWire, addr, token, binOpts, xorOpts, probeDelay, probeThreads, forceClear)
choice, err := selector.resolveOnly()
if err != nil {
fmt.Printf("[D-TCP] phase=PORT_SCAN state=rejected candidate=%d reason=no_validated_wire\n", port)
continue
}
fmt.Printf("[D-TCP] phase=PORT_SCAN state=success port=%d wire=%s header_mask=%02x clear_payload=%t\n", port, choice.mode, choice.mask, choice.cover.Clear)
return selector, addr, port, nil
}
fmt.Printf("[D-TCP] phase=PORT_SCAN state=failed start=%d end=%d reason=no_working_dragontcp_port\n", portStart, portEnd)
return nil, "", 0, fmt.Errorf("no working DragonTCP port found in %d-%d", portStart, portEnd)
}
func main() {
var (
listenHost = flag.String("listen-host", "127.0.0.1", "local proxy listen host")
listenPort = flag.Int("listen-port", 8080, "local proxy listen port")
serverHost = flag.String("server-host", "", "remote DragonTCP server host")
serverPort = flag.Int("server-port", 53, "remote DragonTCP server port")
serverPort = flag.Int("server-port", 53, "remote DragonTCP server port; used when no range is supplied")
serverPortStart = flag.Int("server-port-start", 0, "first remote DragonTCP port to scan; 0 uses --server-port")
serverPortEnd = flag.Int("server-port-end", 0, "last remote DragonTCP port to scan; 0 uses the resolved start port")
token = flag.String("token", "", "optional shared token")
maxConnections = flag.Int("max-connections", 20000, "max simultaneous proxy connections")
transport = flag.String("transport", "chunk", "transport: chunk (DragonTCP binary adaptive transport)")
@@ -372,6 +437,9 @@ func main() {
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (up to 1 MiB)")
chunkAdaptive = flag.Bool("chunk-adaptive", true, "automatically shrink on failures and grow after stable success")
chunkSuccesses = flag.Int("chunk-grow-after", 16, "successful data records required before increasing chunk size")
chunkShrinkAfter = flag.Int("chunk-shrink-after", 1, "consecutive recoverable transfer failures required before reducing chunk size")
chunkShrinkStep = flag.Int("chunk-shrink-step", 200, "bytes to subtract on each recoverable runtime chunk failure; Android uses 200")
chunkMaxFirst = flag.Bool("chunk-max-first", false, "legacy CLI-only max-first calibration; Android uses ascending calibration")
chunkAdaptLog = flag.Bool("chunk-adapt-log", true, "print adaptive chunk size changes")
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
chunkPollers = flag.Int("chunk-pollers", 1, "reserved compatibility setting; binary transport uses one download worker")
@@ -379,17 +447,48 @@ func main() {
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, "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", 5*time.Second, "per-record transaction timeout before adaptive shrink")
chunkTimeout = flag.Duration("chunk-timeout", 5*time.Second, "per-record transaction timeout; a real timeout terminates the tunnel")
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)")
forceClearPayload = flag.Bool("force-clear-payload", false, "force B/BP clear payloads and disable the SHA-256 payload mask; no masked fallback")
wireProbeDelay = flag.Duration("wire-probe-delay", 100*time.Millisecond, "minimum delay between header/profile probe starts (50ms-30s)")
wireProbeThreads = flag.Int("wire-probe-threads", 1, "maximum concurrent wire profile probes (1-16)")
sshUser = flag.String("ssh-user", "", "SSH tunnel username; enables tunnel-only SSH/SOCKS mode")
sshPassword = flag.String("ssh-password", "", "SSH tunnel password")
sshPasswordEnv = flag.String("ssh-password-env", "", "environment variable containing the SSH tunnel password")
sshInternalHost = flag.String("ssh-internal-host", defaultSSHInternalHostClient, "reserved DragonTCP target for internal SSH")
sshInternalPort = flag.Int("ssh-internal-port", 2222, "internal fake SSH port on the DragonTCP server")
sshHostKeyPin = flag.String("ssh-hostkey-pin-file", "", "TOFU SSH host-key fingerprint file")
sshSocksHost = flag.String("ssh-socks-host", "127.0.0.1", "local SOCKS5 listen host when SSH mode is enabled")
sshSocksPort = flag.Int("ssh-socks-port", 1080, "local SOCKS5 listen port when SSH mode is enabled")
sshUDPGWHost = flag.String("ssh-udpgw-host", "dragontcp-udpgw.internal", "reserved UDPGW target as seen by the SSH server")
sshUDPGWPort = flag.Int("ssh-udpgw-port", 7400, "UDPGW port as seen by the SSH server")
)
flag.Parse()
if strings.TrimSpace(*sshPasswordEnv) != "" {
*sshPassword = os.Getenv(strings.TrimSpace(*sshPasswordEnv))
}
if *serverHost == "" {
fmt.Fprintln(os.Stderr, "--server-host is required")
os.Exit(2)
}
resolvedPortStart := *serverPortStart
resolvedPortEnd := *serverPortEnd
if resolvedPortStart == 0 {
resolvedPortStart = *serverPort
}
if resolvedPortEnd == 0 {
resolvedPortEnd = resolvedPortStart
}
if resolvedPortStart < 1 || resolvedPortStart > 65535 || resolvedPortEnd < 1 || resolvedPortEnd > 65535 {
fmt.Fprintln(os.Stderr, "server ports must be between 1 and 65535")
os.Exit(2)
}
if resolvedPortStart > resolvedPortEnd {
fmt.Fprintln(os.Stderr, "--server-port-start must not exceed --server-port-end")
os.Exit(2)
}
*transport = strings.ToLower(*transport)
if *transport != "chunk" {
@@ -406,6 +505,11 @@ func main() {
*chunkMax = *chunkSizeLegacy
*chunkAdaptive = false
}
if *chunkMaxFirst {
// Retained for CLI compatibility only. The Android build never enables
// this flag; its startup calibration is always ascending.
*chunkStart = *chunkMax
}
if *chunkMin < 32 || *chunkMax > protocol.MaxChunkPayload || *chunkMin > *chunkStart || *chunkStart > *chunkMax {
fmt.Fprintf(os.Stderr, "require 32 <= --chunk-min <= --chunk-start <= --chunk-max <= %d\n", protocol.MaxChunkPayload)
os.Exit(2)
@@ -414,6 +518,23 @@ func main() {
fmt.Fprintln(os.Stderr, "--chunk-grow-after must be at least 1")
os.Exit(2)
}
if *chunkShrinkAfter < 1 || *chunkShrinkAfter > 32 {
fmt.Fprintln(os.Stderr, "--chunk-shrink-after must be between 1 and 32")
os.Exit(2)
}
if *chunkShrinkStep < 0 || *chunkShrinkStep > protocol.MaxChunkPayload {
fmt.Fprintf(os.Stderr, "--chunk-shrink-step must be between 0 and %d bytes\n", protocol.MaxChunkPayload)
os.Exit(2)
}
if *chunkMaxFirst {
// MAX-FIRST always uses one recoverable failure as a signal to move
// to the next calibration candidate. shrinkStep is the final boundary
// resolution and the runtime linear recovery step.
if *chunkShrinkStep == 0 {
*chunkShrinkStep = maxFirstFineResolution
}
*chunkShrinkAfter = 1
}
if *chunkPollers < 1 || *chunkPollers > 128 {
fmt.Fprintln(os.Stderr, "--chunk-pollers must be between 1 and 128")
os.Exit(2)
@@ -443,24 +564,42 @@ func main() {
fmt.Fprintln(os.Stderr, "--wire must be b, bp, x or auto")
os.Exit(2)
}
if *forceClearPayload && *wireMode == WireXOR {
fmt.Fprintln(os.Stderr, "--force-clear-payload cannot be used with --wire x; use auto, b, or bp")
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")
if *wireProbeDelay < 50*time.Millisecond || *wireProbeDelay > 30*time.Second {
fmt.Fprintln(os.Stderr, "--wire-probe-delay must be between 50ms 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)
}
sshEnabled := strings.TrimSpace(*sshUser) != ""
var err error
if sshEnabled {
if *sshPassword == "" {
fmt.Fprintln(os.Stderr, "--ssh-password is required when --ssh-user is set")
os.Exit(2)
}
if *sshInternalPort < 1 || *sshInternalPort > 65535 || *sshSocksPort < 1 || *sshSocksPort > 65535 || *sshUDPGWPort < 1 || *sshUDPGWPort > 65535 {
fmt.Fprintln(os.Stderr, "SSH/SOCKS/UDPGW ports must be between 1 and 65535")
os.Exit(2)
}
}
chunkOpts := chunkClientOptions{
startSize: *chunkStart,
minSize: *chunkMin,
maxSize: *chunkMax,
adaptive: *chunkAdaptive,
adaptSuccesses: *chunkSuccesses,
shrinkAfter: *chunkShrinkAfter,
shrinkStep: *chunkShrinkStep,
adaptLog: *chunkAdaptLog,
pollers: *chunkPollers,
minPipeline: *chunkConcurrencyMin,
@@ -469,25 +608,17 @@ func main() {
pollDelay: *chunkPollDelay,
txnTimeout: *chunkTimeout,
tcpBuffer: *tcpBuffer,
forceMaxStart: *chunkMaxFirst,
}
xorOpts := xorchunk.NewOptions(
*chunkStart, *chunkMin, *chunkMax, *chunkAdaptive, *chunkSuccesses, *chunkAdaptLog,
*chunkStart, *chunkMin, *chunkMax, *chunkAdaptive, *chunkSuccesses, *chunkShrinkAfter, *chunkAdaptLog,
*chunkPollers, *chunkReconnect, *chunkPollDelay, *chunkTimeout, *tcpBuffer,
)
listenAddr := net.JoinHostPort(*listenHost, strconv.Itoa(*listenPort))
serverAddr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer ln.Close()
fmt.Printf("local Go HTTP proxy listening on %s\n", listenAddr)
fmt.Printf("remote DragonTCP endpoint=%s\n", serverAddr)
fmt.Printf("remote DragonTCP host=%s port_range=%d-%d\n", *serverHost, resolvedPortStart, resolvedPortEnd)
fmt.Printf("max_connections=%d transport=%s tcp_buffer=%d\n", *maxConnections, *transport, *tcpBuffer)
if *transport == "chunk" {
batchMode := "adaptive"
@@ -495,12 +626,15 @@ func main() {
batchMode = "pinned"
}
fmt.Printf(
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d pollers=%d batch=%d-%d(%s) reconnect_every=%d timeout=%s\n",
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d shrink_after=%d shrink_step=%d max_first=%v pollers=%d batch=%d-%d(%s) reconnect_every=%d timeout=%s\n",
*chunkAdaptive,
*chunkStart,
*chunkMin,
*chunkMax,
*chunkSuccesses,
*chunkShrinkAfter,
*chunkShrinkStep,
*chunkMaxFirst,
*chunkPollers,
*chunkConcurrencyMin,
*chunkConcurrency,
@@ -510,11 +644,65 @@ func main() {
)
}
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()
if *forceClearPayload {
fmt.Printf("wire=%s force_clear_payload=true payload_sha256=false clear_header_order=00,25 protocol_probe=server-local\n", *wireMode)
} else {
fmt.Printf("wire=%s discovering fixed header profile full_range=00-ff protocol_probe=server-local probe_delay=%s probe_threads=%d force_clear_payload=false\n", *wireMode, wireProbeDelay.String(), *wireProbeThreads)
}
// Port discovery happens before chunk calibration. A TCP-open port must also
// pass the DragonTCP wire/header probe before it becomes the WORKING PORT.
wires, serverAddr, selectedPort, err := selectServerEndpoint(
*serverHost, resolvedPortStart, resolvedPortEnd, *token, *wireMode,
chunkOpts, xorOpts, *wireProbeDelay, *wireProbeThreads, *forceClearPayload, *tcpBuffer,
)
if err != nil {
fmt.Fprintf(os.Stderr, "DragonTCP port discovery failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("remote DragonTCP endpoint=%s working_port=%d\n", serverAddr, selectedPort)
// Resolve/authenticate the already-selected wire and calibrate UP/DW before
// exposing the local proxy. resolveOnly() cached the profile, so prepare()
// performs no second header scan.
if _, err := wires.prepare(); err != nil {
fmt.Fprintf(os.Stderr, "DragonTCP startup preflight failed: %v\n", err)
os.Exit(1)
}
var sshManager *sshTunnelManager
var socksListener net.Listener
if strings.TrimSpace(*sshUser) != "" {
sshManager, err = newSSHTunnelManager(wires, *sshUser, *sshPassword, *sshInternalHost, *sshInternalPort, *sshHostKeyPin, *sshUDPGWHost, *sshUDPGWPort)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
defer sshManager.Close()
// Validate the complete DragonTCP -> SSH path before advertising the
// local SOCKS endpoint. Previously the Android UI could say "SSH tunnel
// ready" even though no SSH handshake had happened yet.
if err = sshManager.Warmup(); err != nil {
fmt.Fprintf(os.Stderr, "SSH startup failed: %v\n", err)
os.Exit(1)
}
socksAddr := net.JoinHostPort(*sshSocksHost, strconv.Itoa(*sshSocksPort))
socksListener, err = startSOCKS5Proxy(socksAddr, sshManager, *maxConnections, *tcpBuffer)
if err != nil {
fmt.Fprintf(os.Stderr, "SOCKS5 listen failed: %v\n", err)
os.Exit(1)
}
defer socksListener.Close()
fmt.Printf("ssh_mode=true local_socks5=%s internal_ssh=%s:%d udpgw=%s:%d user=%s\n", socksAddr, *sshInternalHost, *sshInternalPort, *sshUDPGWHost, *sshUDPGWPort, *sshUser)
}
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer ln.Close()
fmt.Printf("local Go HTTP proxy listening on %s\n", listenAddr)
slots := make(chan struct{}, *maxConnections)
+349
View File
@@ -0,0 +1,349 @@
package main
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"dragontcp/internal/protocol"
)
const (
socksVersion5 = 5
socksCmdConnect = 1
socksCmdUDPAssociate = 3
socksAtypIPv4 = 1
socksAtypDomain = 3
socksAtypIPv6 = 4
)
func startSOCKS5Proxy(listenAddr string, manager *sshTunnelManager, maxConnections int, tcpBuffer int) (net.Listener, error) {
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
return nil, err
}
if maxConnections < 1 {
maxConnections = 1
}
slots := make(chan struct{}, maxConnections)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
select {
case slots <- struct{}{}:
go func(c net.Conn) {
defer func() { <-slots; _ = c.Close() }()
protocol.TuneTCP(c)
protocol.TuneTCPBuffer(c, tcpBuffer)
_ = handleSOCKS5(c, manager)
}(conn)
default:
_ = conn.Close()
}
}
}()
return ln, nil
}
func handleSOCKS5(conn net.Conn, manager *sshTunnelManager) error {
br := bufio.NewReaderSize(conn, 4096)
if err := socksNegotiate(br, conn); err != nil {
return err
}
cmd, host, port, err := socksReadRequest(br)
if err != nil {
_ = socksReply(conn, 1, nil)
return err
}
switch cmd {
case socksCmdConnect:
remote, err := manager.DialTCP(host, port)
if err != nil {
_ = socksReply(conn, 5, nil)
return err
}
defer remote.Close()
if err := socksReply(conn, 0, remote.LocalAddr()); err != nil {
return err
}
protocol.RelayRaw(conn, remote)
return nil
case socksCmdUDPAssociate:
return handleSOCKSUDPAssociate(conn, br, manager)
default:
_ = socksReply(conn, 7, nil)
return fmt.Errorf("SOCKS command %d unsupported", cmd)
}
}
func socksNegotiate(br *bufio.Reader, w io.Writer) error {
header := make([]byte, 2)
if _, err := io.ReadFull(br, header); err != nil {
return err
}
if header[0] != socksVersion5 || header[1] == 0 {
return errors.New("invalid SOCKS5 greeting")
}
methods := make([]byte, int(header[1]))
if _, err := io.ReadFull(br, methods); err != nil {
return err
}
noAuth := false
for _, method := range methods {
if method == 0 {
noAuth = true
break
}
}
if !noAuth {
_, _ = w.Write([]byte{5, 0xff})
return errors.New("SOCKS5 client does not support no-auth")
}
_, err := w.Write([]byte{5, 0})
return err
}
func socksReadRequest(br *bufio.Reader) (cmd byte, host string, port int, err error) {
header := make([]byte, 4)
if _, err = io.ReadFull(br, header); err != nil {
return
}
if header[0] != 5 || header[2] != 0 {
err = errors.New("invalid SOCKS5 request")
return
}
cmd = header[1]
host, err = socksReadHost(br, header[3])
if err != nil {
return
}
var portBuf [2]byte
if _, err = io.ReadFull(br, portBuf[:]); err != nil {
return
}
port = int(binary.BigEndian.Uint16(portBuf[:]))
// CONNECT requires a real destination port. UDP ASSOCIATE commonly uses
// 0.0.0.0:0 to ask the proxy to choose the relay endpoint, which is exactly
// what the Android VPN adapter sends.
if cmd == socksCmdConnect && port < 1 {
err = errors.New("invalid SOCKS5 port")
}
return
}
func socksReadHost(r io.Reader, atyp byte) (string, error) {
switch atyp {
case socksAtypIPv4:
b := make([]byte, 4)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return net.IP(b).String(), nil
case socksAtypIPv6:
b := make([]byte, 16)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return net.IP(b).String(), nil
case socksAtypDomain:
var n [1]byte
if _, err := io.ReadFull(r, n[:]); err != nil {
return "", err
}
if n[0] == 0 {
return "", errors.New("empty SOCKS domain")
}
b := make([]byte, int(n[0]))
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return string(b), nil
default:
return "", fmt.Errorf("unsupported SOCKS address type %d", atyp)
}
}
func socksReply(w io.Writer, rep byte, addr net.Addr) error {
ip := net.IPv4zero
port := 0
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
if v4 := tcpAddr.IP.To4(); v4 != nil {
ip = v4
}
port = tcpAddr.Port
} else if udpAddr, ok := addr.(*net.UDPAddr); ok {
if v4 := udpAddr.IP.To4(); v4 != nil {
ip = v4
}
port = udpAddr.Port
}
out := []byte{5, rep, 0, socksAtypIPv4, 0, 0, 0, 0, 0, 0}
copy(out[4:8], ip.To4())
binary.BigEndian.PutUint16(out[8:10], uint16(port))
_, err := w.Write(out)
return err
}
func handleSOCKSUDPAssociate(control net.Conn, br *bufio.Reader, manager *sshTunnelManager) error {
udp, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
if err != nil {
return err
}
defer udp.Close()
if err := socksReply(control, 0, udp.LocalAddr()); err != nil {
return err
}
gw, err := manager.DialUDPGW()
if err != nil {
return err
}
defer gw.Close()
_ = gw.SetDeadline(time.Time{})
done := make(chan struct{})
var closeOnce sync.Once
closeAll := func() { closeOnce.Do(func() { close(done); _ = udp.Close(); _ = gw.Close() }) }
defer closeAll()
var clientMu sync.RWMutex
var clientAddr *net.UDPAddr
writeMu := sync.Mutex{}
go func() {
defer closeAll()
reader := bufio.NewReaderSize(gw, 32*1024)
for {
payload, err := readUDPGWFrame(reader)
if err != nil {
return
}
if len(payload) < 9 {
continue
}
srcIP := payload[3:7]
srcPort := binary.BigEndian.Uint16(payload[7:9])
data := payload[9:]
packet := make([]byte, 10+len(data))
packet[0], packet[1], packet[2], packet[3] = 0, 0, 0, socksAtypIPv4
copy(packet[4:8], srcIP)
binary.BigEndian.PutUint16(packet[8:10], srcPort)
copy(packet[10:], data)
clientMu.RLock()
to := clientAddr
clientMu.RUnlock()
if to != nil {
_, _ = udp.WriteToUDP(packet, to)
}
}
}()
go func() {
defer closeAll()
// The UDP association lifetime is the TCP control connection lifetime.
buf := make([]byte, 1)
for {
if _, err := br.Read(buf); err != nil {
return
}
}
}()
buf := make([]byte, 65535)
for {
n, from, err := udp.ReadFromUDP(buf)
if err != nil {
return nil
}
clientMu.Lock()
clientAddr = from
clientMu.Unlock()
ip, port, payload, err := parseSOCKSUDPDatagram(buf[:n])
if err != nil {
continue
}
frame := buildUDPGWRequest(1, 0, ip, uint16(port), payload)
writeMu.Lock()
_, err = gw.Write(frame)
writeMu.Unlock()
if err != nil {
return err
}
select {
case <-done:
return nil
default:
}
}
}
func parseSOCKSUDPDatagram(packet []byte) ([4]byte, int, []byte, error) {
var out [4]byte
if len(packet) < 10 || packet[0] != 0 || packet[1] != 0 || packet[2] != 0 {
return out, 0, nil, errors.New("invalid SOCKS5 UDP packet")
}
pos := 3
atyp := packet[pos]
pos++
switch atyp {
case socksAtypIPv4:
if len(packet) < pos+4+2 {
return out, 0, nil, io.ErrUnexpectedEOF
}
copy(out[:], packet[pos:pos+4])
pos += 4
case socksAtypDomain:
if len(packet) <= pos {
return out, 0, nil, io.ErrUnexpectedEOF
}
n := int(packet[pos])
pos++
if len(packet) < pos+n+2 {
return out, 0, nil, io.ErrUnexpectedEOF
}
return out, 0, nil, errors.New("SOCKS UDP domain destinations are disabled to avoid local DNS leakage; use an IPv4 destination")
case socksAtypIPv6:
return out, 0, nil, errors.New("UDPGW supports IPv4 only")
default:
return out, 0, nil, errors.New("unsupported SOCKS UDP address type")
}
port := int(binary.BigEndian.Uint16(packet[pos : pos+2]))
pos += 2
return out, port, packet[pos:], nil
}
func buildUDPGWRequest(connID uint16, x byte, ip [4]byte, port uint16, data []byte) []byte {
payloadLen := 9 + len(data)
frame := make([]byte, 2+payloadLen)
binary.LittleEndian.PutUint16(frame[0:2], uint16(payloadLen))
binary.BigEndian.PutUint16(frame[2:4], connID)
frame[4] = x
copy(frame[5:9], ip[:])
binary.BigEndian.PutUint16(frame[9:11], port)
copy(frame[11:], data)
return frame
}
func readUDPGWFrame(r *bufio.Reader) ([]byte, error) {
var lenBuf [2]byte
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
return nil, err
}
n := int(binary.LittleEndian.Uint16(lenBuf[:]))
if n < 9 || n > 65535 {
return nil, fmt.Errorf("invalid UDPGW frame %d", n)
}
payload := make([]byte, n)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
return payload, nil
}
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"bufio"
"bytes"
"encoding/binary"
"testing"
)
func TestUDPGWFrameRoundTripShape(t *testing.T) {
ip := [4]byte{1, 2, 3, 4}
data := []byte("dragon")
frame := buildUDPGWRequest(7, 0, ip, 5353, data)
payload, err := readUDPGWFrame(bufio.NewReader(bytes.NewReader(frame)))
if err != nil {
t.Fatal(err)
}
if got := binary.BigEndian.Uint16(payload[0:2]); got != 7 {
t.Fatalf("conn id=%d", got)
}
if !bytes.Equal(payload[3:7], ip[:]) {
t.Fatalf("ip=%v", payload[3:7])
}
if got := binary.BigEndian.Uint16(payload[7:9]); got != 5353 {
t.Fatalf("port=%d", got)
}
if !bytes.Equal(payload[9:], data) {
t.Fatalf("data=%q", payload[9:])
}
}
func TestParseSOCKSUDPDatagramIPv4(t *testing.T) {
packet := []byte{0, 0, 0, socksAtypIPv4, 8, 8, 8, 8, 0, 53, 1, 2, 3}
ip, port, data, err := parseSOCKSUDPDatagram(packet)
if err != nil {
t.Fatal(err)
}
if ip != [4]byte{8, 8, 8, 8} {
t.Fatalf("ip=%v", ip)
}
if port != 53 {
t.Fatalf("port=%d", port)
}
if !bytes.Equal(data, []byte{1, 2, 3}) {
t.Fatalf("data=%v", data)
}
}
func TestParseSOCKSUDPDatagramRejectsDomainToAvoidDNSLeak(t *testing.T) {
packet := []byte{0, 0, 0, socksAtypDomain, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0, 53, 1}
_, _, _, err := parseSOCKSUDPDatagram(packet)
if err == nil {
t.Fatal("expected domain-form UDP destination to be rejected")
}
}
func TestSOCKSReadRequestAllowsZeroPortForUDPAssociate(t *testing.T) {
request := []byte{5, socksCmdUDPAssociate, 0, socksAtypIPv4, 0, 0, 0, 0, 0, 0}
cmd, host, port, err := socksReadRequest(bufio.NewReader(bytes.NewReader(request)))
if err != nil {
t.Fatal(err)
}
if cmd != socksCmdUDPAssociate || host != "0.0.0.0" || port != 0 {
t.Fatalf("got cmd=%d host=%q port=%d", cmd, host, port)
}
}
+211
View File
@@ -0,0 +1,211 @@
package main
import (
"fmt"
"io"
"net"
"sync"
"time"
)
// sshCarrierConn absorbs the relatively small encrypted writes produced by the
// SSH packet layer and combines them before handing them to DragonTCP.
//
// This matters because a DragonTCP upload is a request/ack transaction. Without
// write combining, one SSH packet can become one full network round trip even
// when the discovered DragonTCP path supports much larger chunks. The wrapper
// deliberately behaves like a kernel socket send buffer: Write returns after
// the bytes have been copied into a bounded queue, while a single ordered
// writer drains that queue to the underlying DragonTCP stream.
type sshCarrierConn struct {
raw net.Conn
flushBytes int
maxBuffered int
flushDelay time.Duration
mu sync.Mutex
cond *sync.Cond
buf []byte
closing bool
writeErr error
coalesceLogged bool
done chan struct{}
closeOnce sync.Once
}
func newSSHCarrierConn(raw net.Conn, flushBytes, maxBuffered int, flushDelay time.Duration) net.Conn {
if raw == nil {
return nil
}
if flushBytes < 32*1024 {
flushBytes = 32 * 1024
}
if maxBuffered < flushBytes*2 {
maxBuffered = flushBytes * 2
}
if flushDelay <= 0 {
flushDelay = time.Millisecond
}
c := &sshCarrierConn{
raw: raw,
flushBytes: flushBytes,
maxBuffered: maxBuffered,
flushDelay: flushDelay,
done: make(chan struct{}),
}
c.cond = sync.NewCond(&c.mu)
go c.writeLoop()
return c
}
func (c *sshCarrierConn) writeLoop() {
defer close(c.done)
defer c.raw.Close()
for {
c.mu.Lock()
for len(c.buf) == 0 && !c.closing && c.writeErr == nil {
c.cond.Wait()
}
if c.writeErr != nil {
c.mu.Unlock()
return
}
if len(c.buf) == 0 && c.closing {
c.mu.Unlock()
return
}
shouldDelay := len(c.buf) < c.flushBytes && !c.closing
c.mu.Unlock()
// Give consecutive SSH packets a very small window to accumulate. The
// delay is tiny compared with a WAN RTT, but it lets 32 KiB SSH packets
// become a 256 KiB-1 MiB DragonTCP write on a busy stream.
if shouldDelay {
time.Sleep(c.flushDelay)
}
c.mu.Lock()
if c.writeErr != nil {
c.mu.Unlock()
return
}
n := len(c.buf)
if n > c.flushBytes {
n = c.flushBytes
}
batch := make([]byte, n)
copy(batch, c.buf[:n])
if n == len(c.buf) {
c.buf = c.buf[:0]
} else {
copy(c.buf, c.buf[n:])
c.buf = c.buf[:len(c.buf)-n]
}
c.cond.Broadcast()
c.mu.Unlock()
if !c.coalesceLogged && len(batch) >= 128*1024 {
c.coalesceLogged = true
fmt.Printf("ssh carrier: packet coalescing active batch=%d\n", len(batch))
}
if err := writeCarrierFull(c.raw, batch); err != nil {
c.mu.Lock()
if c.writeErr == nil {
c.writeErr = err
}
c.closing = true
c.cond.Broadcast()
c.mu.Unlock()
return
}
}
}
func writeCarrierFull(w io.Writer, p []byte) error {
for len(p) > 0 {
n, err := w.Write(p)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
p = p[n:]
}
return nil
}
func (c *sshCarrierConn) Read(p []byte) (int, error) {
n, err := c.raw.Read(p)
if n > 0 || err == nil {
return n, err
}
c.mu.Lock()
writeErr := c.writeErr
c.mu.Unlock()
if writeErr != nil {
return 0, writeErr
}
return n, err
}
func (c *sshCarrierConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
written := 0
for written < len(p) {
c.mu.Lock()
for len(c.buf) >= c.maxBuffered && !c.closing && c.writeErr == nil {
c.cond.Wait()
}
if c.writeErr != nil {
err := c.writeErr
c.mu.Unlock()
return written, err
}
if c.closing {
c.mu.Unlock()
if written > 0 {
return written, net.ErrClosed
}
return 0, net.ErrClosed
}
room := c.maxBuffered - len(c.buf)
if room < 1 {
c.mu.Unlock()
continue
}
take := len(p) - written
if take > room {
take = room
}
c.buf = append(c.buf, p[written:written+take]...)
written += take
c.cond.Signal()
c.mu.Unlock()
}
return written, nil
}
func (c *sshCarrierConn) Close() error {
c.closeOnce.Do(func() {
c.mu.Lock()
c.closing = true
c.cond.Broadcast()
c.mu.Unlock()
<-c.done
})
c.mu.Lock()
err := c.writeErr
c.mu.Unlock()
return err
}
func (c *sshCarrierConn) LocalAddr() net.Addr { return c.raw.LocalAddr() }
func (c *sshCarrierConn) RemoteAddr() net.Addr { return c.raw.RemoteAddr() }
func (c *sshCarrierConn) SetDeadline(t time.Time) error { return c.raw.SetDeadline(t) }
func (c *sshCarrierConn) SetReadDeadline(t time.Time) error { return c.raw.SetReadDeadline(t) }
func (c *sshCarrierConn) SetWriteDeadline(t time.Time) error { return c.raw.SetWriteDeadline(t) }
@@ -0,0 +1,60 @@
package main
import (
"bytes"
"io"
"net"
"sync"
"testing"
"time"
)
type carrierTestConn struct {
mu sync.Mutex
writes int
buf bytes.Buffer
closed bool
}
func (c *carrierTestConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *carrierTestConn) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return 0, net.ErrClosed
}
c.writes++
return c.buf.Write(p)
}
func (c *carrierTestConn) Close() error { c.mu.Lock(); c.closed = true; c.mu.Unlock(); return nil }
func (c *carrierTestConn) LocalAddr() net.Addr { return dummyAddr("local") }
func (c *carrierTestConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
func (c *carrierTestConn) SetDeadline(time.Time) error { return nil }
func (c *carrierTestConn) SetReadDeadline(time.Time) error { return nil }
func (c *carrierTestConn) SetWriteDeadline(time.Time) error { return nil }
func TestSSHCarrierCombinesPacketWrites(t *testing.T) {
raw := &carrierTestConn{}
conn := newSSHCarrierConn(raw, 128*1024, 512*1024, 5*time.Millisecond)
want := make([]byte, 0, 128*1024)
for i := 0; i < 4; i++ {
part := bytes.Repeat([]byte{byte(i + 1)}, 32*1024)
want = append(want, part...)
if n, err := conn.Write(part); err != nil || n != len(part) {
t.Fatalf("Write %d = %d, %v", i, n, err)
}
}
if err := conn.Close(); err != nil {
t.Fatal(err)
}
raw.mu.Lock()
got := append([]byte(nil), raw.buf.Bytes()...)
writes := raw.writes
raw.mu.Unlock()
if !bytes.Equal(got, want) {
t.Fatal("carrier changed byte order/content")
}
if writes >= 4 {
t.Fatalf("expected packet writes to be combined, raw writes=%d", writes)
}
}
+222
View File
@@ -0,0 +1,222 @@
package main
import (
"errors"
"fmt"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/crypto/ssh"
)
type sshTunnelManager struct {
wires *wireSelector
username string
password string
internalHost string
internalPort int
pinFile string
udpgwHost string
udpgwPort int
mu sync.Mutex
client *ssh.Client
closed bool
firstTCPLogged atomic.Bool
udpLogged atomic.Bool
}
func newSSHTunnelManager(wires *wireSelector, username, password, internalHost string, internalPort int, pinFile, udpgwHost string, udpgwPort int) (*sshTunnelManager, error) {
username = strings.TrimSpace(username)
if username == "" {
return nil, errors.New("SSH username is required")
}
if password == "" {
return nil, errors.New("SSH password is required")
}
if internalHost == "" {
internalHost = defaultSSHInternalHostClient
}
if internalPort < 1 || internalPort > 65535 {
return nil, errors.New("invalid SSH internal port")
}
if udpgwHost == "" {
udpgwHost = "dragontcp-udpgw.internal"
}
if udpgwPort < 1 || udpgwPort > 65535 {
return nil, errors.New("invalid UDPGW port")
}
return &sshTunnelManager{
wires: wires, username: username, password: password,
internalHost: internalHost, internalPort: internalPort,
pinFile: pinFile, udpgwHost: udpgwHost, udpgwPort: udpgwPort,
}, nil
}
const (
defaultSSHInternalHostClient = "dragontcp-ssh.internal"
sshCarrierWriteBatch = 1024 * 1024
sshCarrierMaxBuffered = 4 * 1024 * 1024
sshCarrierFlushDelay = 2 * time.Millisecond
)
func (m *sshTunnelManager) hostKeyCallback() ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
fingerprint := ssh.FingerprintSHA256(key)
if strings.TrimSpace(m.pinFile) == "" {
return nil
}
data, err := os.ReadFile(m.pinFile)
if err == nil {
expected := strings.TrimSpace(string(data))
if expected == fingerprint {
return nil
}
return fmt.Errorf("SSH host key changed: expected %s got %s", expected, fingerprint)
}
if !os.IsNotExist(err) {
return fmt.Errorf("read SSH host key pin: %w", err)
}
if err := os.WriteFile(m.pinFile, []byte(fingerprint+"\n"), 0600); err != nil {
return fmt.Errorf("save SSH host key pin: %w", err)
}
fmt.Printf("ssh host key pinned: %s\n", fingerprint)
return nil
}
}
func (m *sshTunnelManager) connectLocked() (*ssh.Client, error) {
if m.closed {
return nil, net.ErrClosed
}
if m.client != nil {
return m.client, nil
}
fmt.Printf("ssh carrier: opening DragonTCP stream to %s:%d\n", m.internalHost, m.internalPort)
transport, err := m.wires.dial(m.internalHost, m.internalPort)
if err != nil {
return nil, fmt.Errorf("DragonTCP SSH carrier failed: %w", err)
}
// SSH emits encrypted packets in ~tens-of-KiB writes. Feeding each one
// directly into the transactional DragonTCP transport creates a full RTT per
// SSH packet. Combine them behind bounded backpressure so a busy SSH stream
// reaches DragonTCP's discovered 256 KiB-1 MiB chunk sizes instead.
transport = newSSHCarrierConn(transport, sshCarrierWriteBatch, sshCarrierMaxBuffered, sshCarrierFlushDelay)
fmt.Printf("ssh carrier: DragonTCP stream connected write_batch=%d max_buffer=%d flush_delay=%s\n", sshCarrierWriteBatch, sshCarrierMaxBuffered, sshCarrierFlushDelay)
cfg := &ssh.ClientConfig{
User: m.username,
Auth: []ssh.AuthMethod{ssh.Password(m.password)},
HostKeyCallback: m.hostKeyCallback(),
ClientVersion: "SSH-2.0-DragonTCP",
}
addr := net.JoinHostPort(m.internalHost, fmt.Sprintf("%d", m.internalPort))
cc, chans, reqs, err := ssh.NewClientConn(transport, addr, cfg)
if err != nil {
_ = transport.Close()
return nil, fmt.Errorf("SSH handshake/auth failed: %w", err)
}
client := ssh.NewClient(cc, chans, reqs)
m.client = client
fmt.Printf("ssh authenticated: user=%s transport=DragonTCP mode=tunnel-only\n", m.username)
go m.keepalive(client)
return client, nil
}
func (m *sshTunnelManager) getClient() (*ssh.Client, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.connectLocked()
}
func (m *sshTunnelManager) invalidate(client *ssh.Client) {
m.mu.Lock()
if m.client == client {
m.client = nil
_ = client.Close()
}
m.mu.Unlock()
}
func (m *sshTunnelManager) keepalive(client *ssh.Client) {
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
for range ticker.C {
_, _, err := client.SendRequest("keepalive@dragontcp", true, nil)
if err != nil {
m.invalidate(client)
return
}
m.mu.Lock()
same := m.client == client && !m.closed
m.mu.Unlock()
if !same {
return
}
}
}
func (m *sshTunnelManager) Warmup() error {
_, err := m.getClient()
return err
}
func (m *sshTunnelManager) DialTCP(host string, port int) (net.Conn, error) {
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
client, err := m.getClient()
if err != nil {
// Authentication, handshake, or physical-carrier failures are hard
// boundaries. Retrying them immediately would duplicate expensive
// DragonTCP/SSH connection attempts and can overload the server.
return nil, err
}
conn, err := client.Dial("tcp", target)
if err == nil {
if m.firstTCPLogged.CompareAndSwap(false, true) {
fmt.Printf("ssh traffic: direct-tcpip active\n")
}
return conn, nil
}
lastErr = err
// A direct-tcpip channel rejection means the SSH transport is healthy
// and only this destination failed (for example ECONNREFUSED or a
// server-side target-policy rejection). Do not tear down the persistent
// SSH carrier or redial the destination in that case.
var openErr *ssh.OpenChannelError
if errors.As(err, &openErr) {
return nil, err
}
m.invalidate(client)
}
if lastErr == nil {
lastErr = errors.New("SSH target dial failed")
}
return nil, lastErr
}
func (m *sshTunnelManager) DialUDPGW() (net.Conn, error) {
conn, err := m.DialTCP(m.udpgwHost, m.udpgwPort)
if err == nil && m.udpLogged.CompareAndSwap(false, true) {
fmt.Printf("ssh traffic: UDPGW active target=%s:%d\n", m.udpgwHost, m.udpgwPort)
}
return conn, err
}
func (m *sshTunnelManager) Close() error {
m.mu.Lock()
m.closed = true
client := m.client
m.client = nil
m.mu.Unlock()
if client != nil {
return client.Close()
}
return nil
}
+223 -138
View File
@@ -1,26 +1,25 @@
package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/wire"
"dragontcp/internal/xorchunk"
)
// DragonTCP speaks three wires that are not interchangeable:
//
// 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
// b — compact binary records (29/5-byte headers, SHA-256-masked payloads by default)
// bp — compatible registration/upload/download/ACK records, SHA-256-masked by default
// 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
// left on auto, which decides by actually fetching a URL through each wire and
// keeping the first that answers.
// left on auto. Startup performs a tiny server-local profile probe and caches
// the first wire/header profile that survives the carrier.
const (
WireBinary = "b"
WireBP = "bp"
@@ -28,14 +27,11 @@ const (
WireAuto = "auto"
)
// probeTarget is fetched through a candidate wire to decide whether it works.
// A plain HTTP host is used deliberately: it exercises OPEN, upload and
// download in one go, and a valid status line proves bytes survived intact.
const (
probeHost = "ip.dr2.site"
probePort = 80
probeTimeout = 8 * time.Second
)
// Header/profile discovery uses one tiny server-local protocol transaction.
// It does not open an Internet target and therefore measures only whether the
// candidate DragonTCP framing survives the carrier and is understood by the
// server. Path chunk calibration runs separately after a header is selected.
const profileProbeTimeout = 2 * time.Second
type wireSelector struct {
mu sync.Mutex
@@ -48,6 +44,7 @@ type wireSelector struct {
xorOpts xorchunk.Options
probeDelay time.Duration
probeThreads int
forceClear bool
// Test hooks are nil in production.
candidateOverride []wireChoice
@@ -60,6 +57,47 @@ type wireChoice struct {
cover cover.Profile
}
// forcedClearChoice builds a clear-payload cover profile with an explicit
// binary header mask. Clear payload and header masking are independent: a
// 0x00 header mask can still carry a fully clear payload because the cover
// preface advertises Clear=true to the server.
//
// --force-clear-payload deliberately tries mask 0x00 first, then 0x25. Both
// profiles keep SHA-256 payload masking disabled. A successful choice is cached
// for the process lifetime, so all reconnects use the same clear profile.
func forcedClearChoice(mode string, headerMask byte) wireChoice {
id := uint16(0x0000)
if headerMask == 0x25 {
// Retain the previously deployed clear profile ID for the 0x25 fallback.
id = 0x0065
}
profile := cover.Profile{
Enabled: true,
ID: id,
Padding: 0,
HeaderMask: headerMask,
XOR: false,
Clear: true,
}
return wireChoice{mode: mode, mask: profile.HeaderMask, cover: profile}
}
// validBinaryHeaderMask reports whether a direct B/BP header mask is
// unambiguous to the server's legacy classifier. B/BP encode the request mode
// in the low three bits (0..4), so those bits in the mask must be zero. This
// leaves exactly 32 valid masks: 00,08,10,...,F8.
func validBinaryHeaderMask(mask byte) bool {
return mask&0x07 == 0
}
// validXORHeaderMask reports whether a direct X mask remains in the X side of
// the server's first-byte partition. X starts with 'U'^mask and the server
// recognizes X only when those low three bits are 5, 6, or 7. There are 96
// such masks.
func validXORHeaderMask(mask byte) bool {
return ('U'^mask)&0x07 >= 5
}
func (c wireChoice) String() string {
if c.mode == WireBP {
if c.cover.Enabled {
@@ -73,7 +111,7 @@ func (c wireChoice) String() string {
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 {
func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options, probeDelay time.Duration, probeThreads int, forceClear bool) *wireSelector {
s := &wireSelector{
configured: configured,
serverAddr: serverAddr,
@@ -82,10 +120,90 @@ func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOp
xorOpts: xorOpts,
probeDelay: probeDelay,
probeThreads: probeThreads,
forceClear: forceClear,
}
return s
}
// resolveOnly validates and locks the wire/header profile without running UP/DW
// chunk calibration. Port-range discovery uses this to ensure a TCP-open port is
// actually a DragonTCP endpoint before it is exposed as the WORKING PORT.
func (s *wireSelector) resolveOnly() (wireChoice, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.hasChoice {
return s.resolved, nil
}
choice, ok := s.detectLocked()
if !ok {
return wireChoice{}, fmt.Errorf("no validated DragonTCP wire/header profile")
}
s.resolved = choice
s.hasChoice = true
return choice, nil
}
// prepare resolves/authenticates the wire and calibrates its carrier limits
// before latency-sensitive protocols (notably SSH) are allowed to start. The
// successful wire choice and path profile are cached, so the real SSH OPEN does
// not repeat discovery/calibration.
func (s *wireSelector) prepare() (wireChoice, error) {
fmt.Printf("[D-TCP] phase=AUTH state=starting configured_wire=%s force_clear_payload=%t\n", s.configured, s.forceClear)
s.mu.Lock()
choice := s.resolved
ok := s.hasChoice
if !ok {
choice, ok = s.detectLocked()
if ok {
s.resolved = choice
s.hasChoice = true
}
}
opts := s.binOpts
s.mu.Unlock()
if !ok {
fmt.Printf("[D-TCP] phase=AUTH state=failed reason=no_validated_wire\n")
return wireChoice{}, fmt.Errorf("DragonTCP authentication/wire validation failed")
}
fmt.Printf("[D-TCP] phase=AUTH state=success wire=%s header_mask=%02x clear_payload=%t cover_id=%04x\n", choice.mode, choice.mask, choice.cover.Clear, choice.cover.ID)
if choice.mode == WireXOR {
xopts := s.xorOpts
if choice.cover.Enabled {
xopts = xopts.WithCoverProfile(choice.cover)
} else {
xopts = xopts.WithHeaderMask(choice.mask)
}
fmt.Printf("[D-TCP] phase=CALIBRATION state=starting wire=x strategy=ascending min=%d max=%d growth=4x fine_resolution=%d up_down=sequential\n", xopts.MinSize(), xopts.MaxSize(), calibrationFineResolution)
up, down, persistent := xorchunk.Calibrate(s.serverAddr, s.token, xopts, calibrationFineResolution)
xopts = xopts.WithCalibratedChunks(up, down)
s.mu.Lock()
s.xorOpts = xopts
s.mu.Unlock()
fmt.Printf("[D-TCP] phase=CALIBRATION state=success wire=x upload=%d download=%d persistent=%t lock_runtime_chunks=true\n", up, down, persistent)
fmt.Printf("[D-TCP] phase=ACTIVE wire=%s header_mask=%02x clear_payload=%t upload_chunk=%d download_chunk=%d calibrated_locked=true runtime_adaptive=false\n", choice.mode, choice.mask, choice.cover.Clear, up, down)
return choice, nil
}
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
strategy := "ascending"
if opts.forceMaxStart {
strategy = "max-first"
}
if opts.forceMaxStart {
fmt.Printf("[D-TCP] phase=CALIBRATION state=starting strategy=%s min=%d max=%d coarse_step=%d fine_resolution=%d\n", strategy, opts.minSize, opts.maxSize, maxFirstCoarseStep, maxFirstFineResolution)
} else {
fmt.Printf("[D-TCP] phase=CALIBRATION state=starting strategy=%s min=%d max=%d growth=4x fine_resolution=%d up_down=sequential\n", strategy, opts.minSize, opts.maxSize, calibrationFineResolution)
}
profile := getPathProfile(s.serverAddr, s.token, opts)
fmt.Printf("[D-TCP] phase=CALIBRATION state=success upload=%d download=%d persistent=%t\n", profile.upload, profile.download, profile.persistent)
fmt.Printf("[D-TCP] phase=ACTIVE wire=%s header_mask=%02x clear_payload=%t upload_chunk=%d download_chunk=%d\n", choice.mode, choice.mask, choice.cover.Clear, profile.upload, profile.download)
return choice, nil
}
// 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) {
choice := s.mode()
@@ -121,6 +239,13 @@ func (s *wireSelector) mode() wireChoice {
s.hasChoice = true
return picked
}
if s.forceClear {
// Hard guarantee: never silently fall back to a legacy SHA-256-masked
// payload if the user explicitly requested clear payloads.
if candidates := s.profileCandidates(); len(candidates) > 0 {
return candidates[0]
}
}
// 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.
@@ -134,101 +259,69 @@ func (s *wireSelector) mode() wireChoice {
}
}
// 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.
// profileCandidates returns only masks that are mathematically valid for the
// direct server classifier. Numeric order keeps mask 0x00 first on permissive
// networks and avoids the old exhaustive covered 0x00..0xFF scan.
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)})
}
}
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})
// A forced clear payload is retained for CLI compatibility/testing only.
// Android does not expose it. Clear payload and header mask are independent.
if s.forceClear {
switch s.configured {
case WireBinary:
return []wireChoice{
forcedClearChoice(WireBinary, 0x00),
forcedClearChoice(WireBinary, 0x25),
}
case WireBP:
return []wireChoice{
forcedClearChoice(WireBP, 0x00),
forcedClearChoice(WireBP, 0x25),
}
case WireAuto:
return []wireChoice{
forcedClearChoice(WireBinary, 0x00),
forcedClearChoice(WireBP, 0x00),
forcedClearChoice(WireBinary, 0x25),
forcedClearChoice(WireBP, 0x25),
}
default:
return nil
}
}
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,
}
}
wantB := s.configured == WireAuto || s.configured == WireBinary
wantBP := s.configured == WireAuto || s.configured == WireBP
wantX := s.configured == WireAuto || s.configured == WireXOR
// 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.
// Normal masked discovery uses only masks that the direct wire classifier
// can decode without a cover preface. This removes the old 0x00..0xFF x 3
// covered scan. Auto stays ordered by numeric mask so 0x00 is tested first.
//
// B/BP: 32 masks (low 3 bits must be zero).
// X: 96 masks (('U'^mask)&7 must land in 5..7).
// Auto: 160 total candidates, rather than ~900 covered/direct probes.
out := make([]wireChoice, 0, 160)
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})
mask := byte(n)
if validBinaryHeaderMask(mask) {
if wantB {
out = append(out, wireChoice{mode: WireBinary, mask: mask})
}
if wantBP {
out = append(out, wireChoice{mode: WireBP, mask: mask})
}
}
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})
if wantX && validXORHeaderMask(mask) {
out = append(out, wireChoice{mode: WireXOR, mask: mask})
}
}
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.
// detectLocked validates header/profile candidates with tiny server-local
// protocol probes. The default is one worker. Users may explicitly allow more
// workers, while the launch delay still spaces new attempts globally.
func (s *wireSelector) detectLocked() (wireChoice, bool) {
candidates := s.profileCandidates()
if s.candidateOverride != nil {
@@ -243,7 +336,7 @@ func (s *wireSelector) detectLocked() (wireChoice, bool) {
}
delay := s.probeDelay
if delay <= 0 {
delay = time.Second
delay = 100 * time.Millisecond
}
type result struct {
choice wireChoice
@@ -295,59 +388,51 @@ func (s *wireSelector) detectLocked() (wireChoice, bool) {
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)
fmt.Printf("wire probe: selected=%s header_mask=%02x completed=%d launched=%d elapsed=%s protocol_probe=true threads=%d fixed_until_restart=true\n", got.choice, got.choice.mask, completed, next, time.Since(started).Round(time.Millisecond), 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: completed=%d/%d launched=%d elapsed=%s protocol_probe=true no validated profile yet\n", completed, len(candidates), next, time.Since(started).Round(time.Millisecond))
}
}
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))
fmt.Printf("wire probe: no header/profile validated after %d candidates in %s; retrying later\n", 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.
// probe performs one small server-local framing transaction. This is purposely
// separate from UP/DW fake-iperf calibration: header discovery answers "which
// byte/profile survives?", while calibration answers "what chunk size is safe?".
func (s *wireSelector) probe(choice wireChoice) bool {
var (
conn net.Conn
err error
)
if choice.mode == WireXOR {
switch choice.mode {
case WireXOR:
opts := s.xorOpts
if choice.cover.Enabled {
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithCoverProfile(choice.cover))
opts = opts.WithCoverProfile(choice.cover)
} else {
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithHeaderMask(choice.mask))
opts = opts.WithHeaderMask(choice.mask)
}
} 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))
return xorchunk.ProbeProfile(s.serverAddr, s.token, opts)
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
case WireBP:
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
if opts.txnTimeout <= 0 || opts.txnTimeout > profileProbeTimeout {
opts.txnTimeout = profileProbeTimeout
}
return probeBPProfile(s.serverAddr, opts)
default:
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
opts.skipPathProbe = true
opts.minSize = 32
opts.startSize = 32
opts.maxSize = 32
if opts.txnTimeout <= 0 || opts.txnTimeout > profileProbeTimeout {
opts.txnTimeout = profileProbeTimeout
}
return probeOne(s.serverAddr, s.token, opts, wire.ProbeKeepalive, 0)
}
statusLine, err := bufio.NewReader(conn).ReadString('\n')
return err == nil && strings.HasPrefix(statusLine, "HTTP/")
}
+193 -46
View File
@@ -6,79 +6,141 @@ import (
"time"
)
func TestProfileCandidatesCoverBothWireFamilies(t *testing.T) {
func expectedBinaryMasks() []byte {
out := make([]byte, 0, 32)
for n := 0; n < 256; n++ {
mask := byte(n)
if validBinaryHeaderMask(mask) {
out = append(out, mask)
}
}
return out
}
func expectedXORMasks() []byte {
out := make([]byte, 0, 96)
for n := 0; n < 256; n++ {
mask := byte(n)
if validXORHeaderMask(mask) {
out = append(out, mask)
}
}
return out
}
func TestProfileCandidatesUseOnlyMathematicallyValidDirectMasks(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
t.Fatalf("normal discovery must not use a cover profile: %s", candidate)
}
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
if !validBinaryHeaderMask(candidate.mask) {
t.Fatalf("invalid B mask: %02x", candidate.mask)
}
case WireBP:
bpCount++
if candidate.cover.Enabled && !candidate.cover.Clear {
t.Fatalf("covered BP profile must use clear payloads: %s", candidate)
if !validBinaryHeaderMask(candidate.mask) {
t.Fatalf("invalid BP mask: %02x", candidate.mask)
}
if !candidate.cover.Enabled && candidate.mask != 0 {
t.Fatalf("direct BP profile must keep a clear header: %s", candidate)
case WireXOR:
xorCount++
if !validXORHeaderMask(candidate.mask) {
t.Fatalf("invalid X mask: %02x", candidate.mask)
}
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 binaryCount != 32 || bpCount != 32 || xorCount != 96 {
t.Fatalf("profiles B=%d BP=%d X=%d, want B=32 BP=32 X=96", binaryCount, bpCount, xorCount)
}
if len(firstBytes) != 256 {
t.Fatalf("profiles cover %d first-byte values, want 256", len(firstBytes))
if len(candidates) != 160 {
t.Fatalf("auto candidates=%d, want 160", len(candidates))
}
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]))
}
func TestBinaryMasksAreExactly00ThroughF8InStepsOf08(t *testing.T) {
want := expectedBinaryMasks()
if len(want) != 32 {
t.Fatalf("binary mask count=%d, want 32", len(want))
}
for i, mask := range want {
if mask != byte(i*8) {
t.Fatalf("binary mask[%d]=%02x, want %02x", i, mask, byte(i*8))
}
if len(coveredPadding[mode]) != 16 {
t.Fatalf("mode %s covers %d padding lengths, want 16", mode, len(coveredPadding[mode]))
}
for _, mode := range []string{WireBinary, WireBP} {
candidates := (&wireSelector{configured: mode}).profileCandidates()
if len(candidates) != len(want) {
t.Fatalf("mode %s candidates=%d, want %d", mode, len(candidates), len(want))
}
for i, candidate := range candidates {
if candidate.mode != mode || candidate.mask != want[i] || candidate.cover.Enabled {
t.Fatalf("mode %s candidate[%d]=%s mask=%02x, want direct/%02x", mode, i, candidate, candidate.mask, want[i])
}
}
}
}
func TestManualWireStillDiscoversAllProfilesForThatFamily(t *testing.T) {
func TestXORMasksAreExactlyDirectClassifierValidSet(t *testing.T) {
want := expectedXORMasks()
if len(want) != 96 {
t.Fatalf("X mask count=%d, want 96", len(want))
}
candidates := (&wireSelector{configured: WireXOR}).profileCandidates()
if len(candidates) != len(want) {
t.Fatalf("X candidates=%d, want %d", len(candidates), len(want))
}
for i, candidate := range candidates {
if candidate.mode != WireXOR || candidate.mask != want[i] || candidate.cover.Enabled {
t.Fatalf("X candidate[%d]=%s mask=%02x, want direct/%02x", i, candidate, candidate.mask, want[i])
}
if ('U'^candidate.mask)&7 < 5 {
t.Fatalf("X candidate[%d] is classifier-invalid: %02x", i, candidate.mask)
}
}
}
func TestAutoInterleavesOnlyValidMasksInNumericOrder(t *testing.T) {
candidates := (&wireSelector{configured: WireAuto}).profileCandidates()
want := make([]wireChoice, 0, 160)
for n := 0; n < 256; n++ {
mask := byte(n)
if validBinaryHeaderMask(mask) {
want = append(want,
wireChoice{mode: WireBinary, mask: mask},
wireChoice{mode: WireBP, mask: mask},
)
}
if validXORHeaderMask(mask) {
want = append(want, wireChoice{mode: WireXOR, mask: mask})
}
}
if len(candidates) != len(want) {
t.Fatalf("auto candidates=%d, want %d", len(candidates), len(want))
}
for i := range want {
if candidates[i] != want[i] {
t.Fatalf("auto candidate[%d]=%s/%02x, want %s/%02x", i, candidates[i].mode, candidates[i].mask, want[i].mode, want[i].mask)
}
}
}
func TestManualWireUsesOnlyValidProfilesForThatFamily(t *testing.T) {
for _, tc := range []struct {
mode string
want int
}{{WireBinary, 544}, {WireBP, 257}, {WireXOR, 352}} {
}{{WireBinary, 32}, {WireBP, 32}, {WireXOR, 96}} {
selector := &wireSelector{configured: tc.mode}
candidates := selector.profileCandidates()
if len(candidates) != tc.want {
@@ -88,22 +150,107 @@ func TestManualWireStillDiscoversAllProfilesForThatFamily(t *testing.T) {
if candidate.mode != tc.mode {
t.Fatalf("mode %s included %s", tc.mode, candidate)
}
if candidate.cover.Enabled {
t.Fatalf("mode %s normal discovery included cover profile %s", tc.mode, candidate)
}
}
}
}
func TestClearProfilesAreTriedBeforeLegacyFallbacks(t *testing.T) {
for _, mode := range []string{WireBinary, WireBP} {
func TestNormalProfilesNeverUseClearPayload(t *testing.T) {
for _, mode := range []string{WireAuto, WireBinary, WireBP, WireXOR} {
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 len(candidates) == 0 {
t.Fatalf("mode %s has no candidates", mode)
}
if candidates[1].cover.Enabled {
t.Fatalf("mode %s does not fall back immediately to a legacy direct profile", mode)
for _, candidate := range candidates {
if candidate.cover.Clear {
t.Fatalf("mode %s normal discovery included clear payload profile: %s", mode, candidate)
}
}
}
}
func TestForceClearProfilesTry00Before25WithoutMaskedFallbacks(t *testing.T) {
selector := &wireSelector{configured: WireAuto, forceClear: true}
candidates := selector.profileCandidates()
if len(candidates) != 4 {
t.Fatalf("force-clear auto profiles=%d, want B00/BP00/B25/BP25", len(candidates))
}
wantModes := []string{WireBinary, WireBP, WireBinary, WireBP}
wantMasks := []byte{0x00, 0x00, 0x25, 0x25}
for i, candidate := range candidates {
if candidate.mode != wantModes[i] || candidate.mask != wantMasks[i] {
t.Fatalf("candidate %d=%s mask=%02x, want mode=%s mask=%02x", i, candidate.mode, candidate.mask, wantModes[i], wantMasks[i])
}
if candidate.mode == WireXOR {
t.Fatalf("force-clear mode included X candidate: %s", candidate)
}
if !candidate.cover.Enabled || !candidate.cover.Clear {
t.Fatalf("force-clear mode included masked/direct candidate: %s", candidate)
}
if candidate.cover.HeaderMask != candidate.mask {
t.Fatalf("candidate %d cover/header mismatch: %+v", i, candidate.cover)
}
}
if candidates[0].cover.ID != 0x0000 || candidates[2].cover.ID != 0x0065 {
t.Fatalf("unexpected clear profile IDs: 00=%04x 25=%04x", candidates[0].cover.ID, candidates[2].cover.ID)
}
}
func TestForceClearPinnedFamilyTries00Then25(t *testing.T) {
for _, mode := range []string{WireBinary, WireBP} {
candidates := (&wireSelector{configured: mode, forceClear: true}).profileCandidates()
if len(candidates) != 2 {
t.Fatalf("mode %s force-clear candidates=%d, want 2", mode, len(candidates))
}
if candidates[0].mode != mode || candidates[0].mask != 0x00 || !candidates[0].cover.Clear {
t.Fatalf("mode %s first forced clear profile is not clear/00: %+v", mode, candidates[0])
}
if candidates[1].mode != mode || candidates[1].mask != 0x25 || !candidates[1].cover.Clear {
t.Fatalf("mode %s second forced clear profile is not clear/25: %+v", mode, candidates[1])
}
}
}
func TestForceClearDiscoveryFallsBackFrom00To25(t *testing.T) {
var seen []byte
selector := &wireSelector{
configured: WireBinary,
forceClear: true,
probeDelay: time.Nanosecond,
probeThreads: 1,
probeOverride: func(choice wireChoice) bool {
seen = append(seen, choice.mask)
return choice.mask == 0x25
},
}
choice := selector.mode()
if choice.mask != 0x25 || !choice.cover.Clear {
t.Fatalf("selected=%s, want clear mask 25 fallback", choice)
}
if len(seen) != 2 || seen[0] != 0x00 || seen[1] != 0x25 {
t.Fatalf("probe order=%v, want [0 37]", seen)
}
}
func TestForceClearFailedDiscoveryStillFallsBackToClear00(t *testing.T) {
selector := &wireSelector{
configured: WireAuto,
forceClear: true,
candidateOverride: []wireChoice{{mode: WireBinary}},
probeDelay: time.Nanosecond,
probeOverride: func(wireChoice) bool { return false },
}
choice := selector.mode()
if !choice.cover.Enabled || !choice.cover.Clear || choice.mode == WireXOR {
t.Fatalf("force-clear discovery failure fell back to non-clear wire: %s", choice)
}
if choice.mask != 0x00 {
t.Fatalf("force-clear discovery failure did not keep first clear mask 00: %02x", choice.mask)
}
}
func measureDiscoveryConcurrency(t *testing.T, threads int) int32 {
t.Helper()
candidates := make([]wireChoice, 24)