package main import ( "bufio" "fmt" "net" "strings" "sync" "time" "dragontcp/internal/cover" "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 // 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. const ( WireBinary = "b" WireBP = "bp" WireXOR = "x" 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 ) type wireSelector struct { 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 } 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 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) { 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) } 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() wireChoice { s.mu.Lock() defer s.mu.Unlock() if s.hasChoice { return s.resolved } if picked, ok := s.detectLocked(); ok { s.resolved = picked s.hasChoice = true return picked } // 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} } } // 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)}) } } 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(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 = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts.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)) 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/") }