Mult Port + TCP Calibration (SSH DEAD)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user