package main import ( "fmt" "net" "strings" "sync" "time" "dragontcp/internal/xorchunk" ) // DragonTCP speaks two wires that are not interchangeable: // // b — compact binary records (29/5-byte headers, SHA-256 keystream mask) // 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" 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 string // b or x once decided serverAddr string token string binOpts chunkClientOptions xorOpts xorchunk.Options } func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options) *wireSelector { s := &wireSelector{ configured: configured, serverAddr: serverAddr, token: token, binOpts: binOpts, xorOpts: xorOpts, } if configured != WireAuto { s.resolved = configured } 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) } return openChunkTunnel(s.serverAddr, s.token, host, port, s.binOpts) } // 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 { s.mu.Lock() defer s.mu.Unlock() if s.resolved != "" { return s.resolved } if picked, ok := s.detectLocked(); ok { s.resolved = picked return picked } // Undecided: use the binary wire for this attempt without caching it. return 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 } fmt.Printf("wire probe: %s failed\n", candidate) } fmt.Printf("wire probe: neither wire reached %s; retrying later\n", probeHost) return "", 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) } else { conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, s.binOpts) } 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. return false } }