This commit is contained in:
2026-08-16 19:02:48 -03:00
parent 96fe00eb2b
commit c8e3011f21
31 changed files with 3457 additions and 351 deletions
+283 -72
View File
@@ -1,18 +1,21 @@
package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/xorchunk"
)
// DragonTCP speaks two wires that are not interchangeable:
// DragonTCP speaks three wires that are not interchangeable:
//
// b — compact binary records (29/5-byte headers, SHA-256 keystream mask)
// b — compact binary records (29/5-byte headers, clear or SHA-256-compatible payloads)
// bp — compatible registration/upload/download/ACK records, clear or SHA-256-compatible
// x — legacy UP/OK framing with XOR 0xAD over ASCII chunk commands
//
// Networks differ in which they pass, so the client can be pinned to either or
@@ -20,6 +23,7 @@ import (
// keeping the first that answers.
const (
WireBinary = "b"
WireBP = "bp"
WireXOR = "x"
WireAuto = "auto"
)
@@ -34,109 +38,316 @@ const (
)
type wireSelector struct {
mu sync.Mutex
configured string // b, x or auto
resolved string // b or x once decided
serverAddr string
token string
binOpts chunkClientOptions
xorOpts xorchunk.Options
mu sync.Mutex
configured string // b, x or auto
resolved wireChoice
hasChoice bool
serverAddr string
token string
binOpts chunkClientOptions
xorOpts xorchunk.Options
probeDelay time.Duration
probeThreads int
// Test hooks are nil in production.
candidateOverride []wireChoice
probeOverride func(wireChoice) bool
}
func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options) *wireSelector {
s := &wireSelector{
configured: configured,
serverAddr: serverAddr,
token: token,
binOpts: binOpts,
xorOpts: xorOpts,
type wireChoice struct {
mode string
mask byte
cover cover.Profile
}
func (c wireChoice) String() string {
if c.mode == WireBP {
if c.cover.Enabled {
return fmt.Sprintf("bp/%s", c.cover)
}
return "bp/direct"
}
if configured != WireAuto {
s.resolved = configured
if c.cover.Enabled {
return fmt.Sprintf("%s/mask-%02x/%s", c.mode, c.mask, c.cover)
}
return fmt.Sprintf("%s/mask-%02x/direct", c.mode, c.mask)
}
func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options, probeDelay time.Duration, probeThreads int) *wireSelector {
s := &wireSelector{
configured: configured,
serverAddr: serverAddr,
token: token,
binOpts: binOpts,
xorOpts: xorOpts,
probeDelay: probeDelay,
probeThreads: probeThreads,
}
return s
}
// dial opens a tunnel over the active wire, resolving the wire first if needed.
func (s *wireSelector) dial(host string, port int) (net.Conn, error) {
mode := s.mode()
if mode == WireXOR {
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts)
choice := s.mode()
if choice.mode == WireBP {
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
return openBPTunnel(s.serverAddr, s.token, host, port, opts)
}
return openChunkTunnel(s.serverAddr, s.token, host, port, s.binOpts)
if choice.mode == WireXOR {
if choice.cover.Enabled {
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts.WithCoverProfile(choice.cover))
}
return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts.WithHeaderMask(choice.mask))
}
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
return openChunkTunnel(s.serverAddr, s.token, host, port, opts)
}
// mode returns the wire to use, running detection once if configured as auto.
// Detection failure is not cached, so a client that starts before the network
// is usable retries on the next connection instead of latching a bad guess.
func (s *wireSelector) mode() string {
func (s *wireSelector) mode() wireChoice {
s.mu.Lock()
defer s.mu.Unlock()
if s.resolved != "" {
if s.hasChoice {
return s.resolved
}
if picked, ok := s.detectLocked(); ok {
s.resolved = picked
s.hasChoice = true
return picked
}
// Undecided: use the binary wire for this attempt without caching it.
return WireBinary
// Undecided: honor an explicitly pinned family for this attempt without
// caching it. Auto retains the original B fallback and retries discovery on
// the next connection.
switch s.configured {
case WireBP:
return wireChoice{mode: WireBP}
case WireXOR:
return wireChoice{mode: WireXOR}
default:
return wireChoice{mode: WireBinary}
}
}
func (s *wireSelector) detectLocked() (string, bool) {
for _, candidate := range []string{WireBinary, WireXOR} {
if s.probe(candidate) {
fmt.Printf("wire probe: %s selected via %s\n", candidate, probeHost)
return candidate, true
// profileCandidates covers all compatible B first-byte bases and all X magic
// masks that cannot be confused with B. Profile zero for each wire is first so
// existing permissive networks complete discovery quickly.
func (s *wireSelector) profileCandidates() []wireChoice {
var binaryProfiles []wireChoice
var xorProfiles []wireChoice
if s.configured == WireAuto || s.configured == WireBinary {
for n := 0; n < 256; n += 8 {
binaryProfiles = append(binaryProfiles, wireChoice{mode: WireBinary, mask: byte(n)})
}
fmt.Printf("wire probe: %s failed\n", candidate)
}
fmt.Printf("wire probe: neither wire reached %s; retrying later\n", probeHost)
return "", false
if s.configured == WireAuto || s.configured == WireXOR {
for n := 0; n < 256; n++ {
mask := byte(n)
if ('U'^mask)&7 >= 5 {
xorProfiles = append(xorProfiles, wireChoice{mode: WireXOR, mask: mask})
}
}
}
paddingRange := []uint16{0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 768, 1024, 1400, 2048, 4096}
makeCovered := func(n int, xor, clear bool) cover.Profile {
first := byte(n)
second := byte(n*197 + 101)
mask := byte(n*149 + 37)
return cover.Profile{
Enabled: true,
ID: uint16(first)<<8 | uint16(second),
Padding: paddingRange[n%len(paddingRange)],
HeaderMask: mask,
XOR: xor,
Clear: clear,
}
}
// New peers try the clear-payload profile first. The next candidates are
// legacy direct profiles, so an older server falls back immediately instead
// of screening the complete expanded profile range.
out := make([]wireChoice, 0, len(binaryProfiles)+len(xorProfiles)+1025)
if s.configured == WireAuto || s.configured == WireBinary {
profile := makeCovered(0, false, true)
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
}
if s.configured == WireAuto || s.configured == WireBP {
profile := makeCovered(0, false, true)
out = append(out, wireChoice{mode: WireBP, mask: profile.HeaderMask, cover: profile})
}
// Interleave formats so neither family can consume the entire discovery
// window before the other one gets a chance.
for i := 0; i < len(binaryProfiles) || i < len(xorProfiles); i++ {
if i < len(binaryProfiles) {
out = append(out, binaryProfiles[i])
}
if i < len(xorProfiles) {
out = append(out, xorProfiles[i])
}
if i == 0 && s.configured == WireAuto {
out = append(out, wireChoice{mode: WireBP})
}
}
if s.configured == WireBP {
out = append(out, wireChoice{mode: WireBP})
}
// Covered profiles expand discovery beyond the one-byte direct formats
// without taking the Cartesian product (which would create thousands of
// connections). Across this distributed range each wire still exercises all
// 256 first bytes, all 256 frame masks, and every padding length repeatedly.
for n := 0; n < 256; n++ {
if s.configured == WireAuto || s.configured == WireBinary {
profile := makeCovered(n, false, false)
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
if n != 0 {
profile = makeCovered(n, false, true)
out = append(out, wireChoice{mode: WireBinary, mask: profile.HeaderMask, cover: profile})
}
}
if s.configured == WireAuto || s.configured == WireBP {
if n != 0 {
profile := makeCovered(n, false, true)
out = append(out, wireChoice{mode: WireBP, mask: profile.HeaderMask, cover: profile})
}
}
if s.configured == WireAuto || s.configured == WireXOR {
profile := makeCovered(n, true, false)
out = append(out, wireChoice{mode: WireXOR, mask: profile.HeaderMask, cover: profile})
}
}
return out
}
// detectLocked validates candidates with real HTTP traffic through ip.dr2.site.
// The default is one worker. Users may explicitly allow more workers, while the
// launch delay still spaces new attempts globally to avoid a connection burst.
func (s *wireSelector) detectLocked() (wireChoice, bool) {
candidates := s.profileCandidates()
if s.candidateOverride != nil {
candidates = s.candidateOverride
}
threads := s.probeThreads
if threads < 1 {
threads = 1
}
if threads > 16 {
threads = 16
}
delay := s.probeDelay
if delay <= 0 {
delay = time.Second
}
type result struct {
choice wireChoice
ok bool
}
results := make(chan result, threads)
next := 0
inflight := 0
completed := 0
started := time.Now()
var lastLaunch time.Time
for next < len(candidates) || inflight > 0 {
canLaunch := next < len(candidates) && inflight < threads
if canLaunch && (lastLaunch.IsZero() || time.Since(lastLaunch) >= delay) {
candidate := candidates[next]
next++
inflight++
lastLaunch = time.Now()
go func(choice wireChoice) {
validated := false
if s.probeOverride != nil {
validated = s.probeOverride(choice)
} else {
validated = s.probe(choice)
}
results <- result{choice: choice, ok: validated}
}(candidate)
continue
}
var got result
if canLaunch {
wait := delay - time.Since(lastLaunch)
timer := time.NewTimer(wait)
select {
case got = <-results:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
case <-timer.C:
continue
}
} else {
got = <-results
}
inflight--
completed++
if got.ok {
fmt.Printf("wire probe: selected=%s completed=%d launched=%d elapsed=%s target=http://%s/ validated=true threads=%d fixed_until_restart=true\n", got.choice, completed, next, time.Since(started).Round(time.Millisecond), probeHost, threads)
return got.choice, true
}
if completed%32 == 0 {
fmt.Printf("wire probe: completed=%d/%d launched=%d elapsed=%s target=http://%s/ no validated profile yet\n", completed, len(candidates), next, time.Since(started).Round(time.Millisecond), probeHost)
}
}
fmt.Printf("wire probe: no profile validated through http://%s/ after %d candidates in %s; retrying later\n", probeHost, completed, time.Since(started).Round(time.Millisecond))
return wireChoice{}, false
}
// probe fetches probeHost through one wire and reports whether a well-formed
// HTTP status line came back.
func (s *wireSelector) probe(mode string) bool {
type result struct{ ok bool }
done := make(chan result, 1)
go func() {
var (
conn net.Conn
err error
)
if mode == WireXOR {
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts)
func (s *wireSelector) probe(choice wireChoice) bool {
var (
conn net.Conn
err error
)
if choice.mode == WireXOR {
if choice.cover.Enabled {
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithCoverProfile(choice.cover))
} else {
conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, s.binOpts)
conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.WithHeaderMask(choice.mask))
}
if err != nil {
done <- result{false}
return
}
defer conn.Close()
request := "GET / HTTP/1.1\r\nHost: " + probeHost + "\r\nUser-Agent: dragontcp\r\nConnection: close\r\n\r\n"
if _, err := conn.Write([]byte(request)); err != nil {
done <- result{false}
return
}
buf := make([]byte, 64)
n, err := conn.Read(buf)
if n <= 0 || (err != nil && n == 0) {
done <- result{false}
return
}
done <- result{strings.HasPrefix(string(buf[:n]), "HTTP/")}
}()
select {
case r := <-done:
return r.ok
case <-time.After(probeTimeout):
// The tunnel goroutine is left to unwind on its own; the wire simply
// did not answer in time, which is all the caller needs to know.
} else if choice.mode == WireBP {
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
opts.skipPathProbe = true
opts.minSize = 32
opts.startSize = 32
opts.maxSize = 32
conn, err = openBPTunnel(s.serverAddr, s.token, probeHost, probePort, opts)
} else {
opts := s.binOpts
opts.headerMask = choice.mask
opts.coverProfile = choice.cover
opts.skipPathProbe = true
opts.minSize = 32
opts.startSize = 32
opts.maxSize = 32
conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, opts)
}
if err != nil {
return false
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(probeTimeout))
request := "GET / HTTP/1.1\r\nHost: " + probeHost + "\r\nUser-Agent: dragontcp\r\nConnection: close\r\n\r\n"
if _, err := conn.Write([]byte(request)); err != nil {
return false
}
statusLine, err := bufio.NewReader(conn).ReadString('\n')
return err == nil && strings.HasPrefix(statusLine, "HTTP/")
}