Mult Protocol
This commit is contained in:
@@ -571,41 +571,40 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
// 0 = persistent (CLI explicit)
|
||||
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
|
||||
// N>=2 = force connection rotation after N logical requests
|
||||
if reconnect == 1 {
|
||||
if profile.persistent {
|
||||
reconnect = 0
|
||||
fmt.Printf("path probe: reconnect mode auto -> persistent\n")
|
||||
} else {
|
||||
fmt.Printf("path probe: reconnect mode auto -> every request\n")
|
||||
}
|
||||
// Resolved silently: this runs once per proxied flow, so it must never log.
|
||||
if reconnect == 1 && profile.persistent {
|
||||
reconnect = 0
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
control := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
||||
// OPEN rides the upload lane instead of a throwaway connection. A dedicated
|
||||
// control connection cost one extra dial per proxied flow, which shows up on
|
||||
// the server as connection churn on top of the steady-state count.
|
||||
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
||||
payload, err := encodeOpen(token, targetHost, targetPort)
|
||||
if err != nil {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
status, body, err := control.single(wire.ModeOpen, sid, 0, payload)
|
||||
status, body, err := uploadLane.single(wire.ModeOpen, sid, 0, payload)
|
||||
if err != nil {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("%s", string(body))
|
||||
}
|
||||
if status != wire.StatusOK || len(body) != 4 {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("bad OPEN response")
|
||||
}
|
||||
serverMax := int(binary.BigEndian.Uint32(body))
|
||||
control.Close()
|
||||
if serverMax < opts.minSize {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("server maximum chunk %d is below client minimum %d", serverMax, opts.minSize)
|
||||
}
|
||||
if opts.maxSize > serverMax {
|
||||
@@ -624,13 +623,12 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
sid: sid,
|
||||
opts: opts,
|
||||
serverMax: serverMax,
|
||||
uploadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
uploadLane: uploadLane,
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
// Start at the user-configured ceiling. On transport failures the
|
||||
// pipeline is halved; successful data responses grow it back by one,
|
||||
// always staying inside minPipeline..maxPipeline. When the two bounds
|
||||
// are equal the depth is pinned and never adapts, which is what paths
|
||||
// that only work at one specific batch size need.
|
||||
// Start at the configured ceiling. On transport failure the batch is
|
||||
// halved but never below minPipeline; successful data grows it back by
|
||||
// one. When min == max the depth is pinned and never adapts, which is
|
||||
// what paths that only work at one specific batch size need.
|
||||
pipeline: opts.maxPipeline,
|
||||
minPipeline: opts.minPipeline,
|
||||
maxPipeline: opts.maxPipeline,
|
||||
@@ -640,53 +638,6 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// pinnedBatch reports whether the batch depth is fixed. A pinned depth never
|
||||
// grows or shrinks: some paths only deliver correctly at one specific number of
|
||||
// records per request, so the adaptive controller must stay out of the way.
|
||||
func (c *chunkConn) pinnedBatch() bool { return c.minPipeline >= c.maxPipeline }
|
||||
|
||||
// batchCount is how many records the next download request will ask for.
|
||||
func (c *chunkConn) batchCount(chunk int) int {
|
||||
count := c.pipeline
|
||||
if count < c.minPipeline {
|
||||
count = c.minPipeline
|
||||
}
|
||||
if count > c.maxPipeline {
|
||||
count = c.maxPipeline
|
||||
}
|
||||
// Bound each batch to roughly 1 MiB of useful data, but never below the
|
||||
// configured floor: a pinned depth is a path requirement, not a hint.
|
||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
||||
count = maxInt(maxCount, c.minPipeline)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// growPipeline widens the batch by one after a successful data response.
|
||||
func (c *chunkConn) growPipeline() {
|
||||
if c.pinnedBatch() {
|
||||
return
|
||||
}
|
||||
if c.pipeline < c.maxPipeline {
|
||||
c.pipeline++
|
||||
}
|
||||
}
|
||||
|
||||
// shrinkPipeline halves the batch after a transport failure. It reports the old
|
||||
// and new depth, and whether anything actually changed; when it returns false
|
||||
// the caller should shrink the record size instead.
|
||||
func (c *chunkConn) shrinkPipeline() (int, int, bool) {
|
||||
if c.pinnedBatch() || c.pipeline <= c.minPipeline {
|
||||
return c.pipeline, c.pipeline, false
|
||||
}
|
||||
old := c.pipeline
|
||||
c.pipeline /= 2
|
||||
if c.pipeline < c.minPipeline {
|
||||
c.pipeline = c.minPipeline
|
||||
}
|
||||
return old, c.pipeline, old != c.pipeline
|
||||
}
|
||||
|
||||
func (c *chunkConn) fillReadBuffer() error {
|
||||
if c.eof {
|
||||
return io.EOF
|
||||
@@ -694,7 +645,18 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
minFailures := 0
|
||||
for len(c.readBuf) == 0 && !c.eof {
|
||||
chunk := c.downSizer.Current()
|
||||
count := c.batchCount(chunk)
|
||||
count := c.pipeline
|
||||
if count < c.minPipeline {
|
||||
count = c.minPipeline
|
||||
}
|
||||
if count > c.maxPipeline {
|
||||
count = c.maxPipeline
|
||||
}
|
||||
// Bound each batch to roughly 1 MiB of useful data, but never below the
|
||||
// configured floor: a pinned depth is a path requirement, not a hint.
|
||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
||||
count = maxInt(maxCount, c.minPipeline)
|
||||
}
|
||||
|
||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
||||
for _, part := range data {
|
||||
@@ -703,16 +665,20 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
}
|
||||
if len(data) > 0 {
|
||||
c.downSizer.Success(chunk)
|
||||
c.growPipeline()
|
||||
if c.pipeline < c.maxPipeline {
|
||||
c.pipeline++
|
||||
}
|
||||
minFailures = 0
|
||||
}
|
||||
if err != nil {
|
||||
// Shrink the batch first, then the record size. When the batch is
|
||||
// pinned (min == max) the depth is left alone entirely and only the
|
||||
// record size adapts.
|
||||
if old, next, shrank := c.shrinkPipeline(); shrank {
|
||||
if c.opts.adaptLog {
|
||||
fmt.Printf("adaptive download batch: %d -> %d after transport failure\n", old, next)
|
||||
if c.pipeline > c.minPipeline {
|
||||
old := c.pipeline
|
||||
c.pipeline /= 2
|
||||
if c.pipeline < c.minPipeline {
|
||||
c.pipeline = c.minPipeline
|
||||
}
|
||||
if c.opts.adaptLog && old != c.pipeline {
|
||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure\n", old, c.pipeline)
|
||||
}
|
||||
} else {
|
||||
old, next := c.downSizer.Failure(chunk)
|
||||
@@ -808,9 +774,9 @@ func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
|
||||
func (c *chunkConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
lane := newRequestLane(c.uploadLane.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
||||
_, _, _ = lane.single(wire.ModeClose, c.sid, 0, nil)
|
||||
lane.Close()
|
||||
// Reuse the upload lane rather than dialling a connection just to say
|
||||
// goodbye; that was a second wasted dial per flow.
|
||||
_, _, _ = c.uploadLane.single(wire.ModeClose, c.sid, 0, nil)
|
||||
c.uploadLane.Close()
|
||||
c.downloadLane.Close()
|
||||
})
|
||||
|
||||
@@ -23,76 +23,6 @@ func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newTestConn(min, max int) *chunkConn {
|
||||
return &chunkConn{pipeline: max, minPipeline: min, maxPipeline: max}
|
||||
}
|
||||
|
||||
func TestPinnedBatchNeverAdapts(t *testing.T) {
|
||||
c := newTestConn(5, 5)
|
||||
if got := c.batchCount(1400); got != 5 {
|
||||
t.Fatalf("pinned batch should request 5 records, got %d", got)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if _, _, shrank := c.shrinkPipeline(); shrank {
|
||||
t.Fatal("pinned batch shrank on transport failure")
|
||||
}
|
||||
c.growPipeline()
|
||||
}
|
||||
if c.pipeline != 5 {
|
||||
t.Fatalf("pinned batch drifted to %d", c.pipeline)
|
||||
}
|
||||
if got := c.batchCount(1400); got != 5 {
|
||||
t.Fatalf("pinned batch should still request 5 records, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinnedBatchSurvivesOneMiBCap(t *testing.T) {
|
||||
// 8 x 1 MiB records exceed the ~1 MiB useful-data cap. A pinned depth must
|
||||
// win anyway, otherwise a path that needs exactly 8 records is broken by
|
||||
// an unrelated size heuristic.
|
||||
c := newTestConn(8, 8)
|
||||
if got := c.batchCount(1024 * 1024); got != 8 {
|
||||
t.Fatalf("pinned batch should ignore the 1 MiB cap, got %d", got)
|
||||
}
|
||||
// An unpinned batch is still capped.
|
||||
c = newTestConn(1, 8)
|
||||
if got := c.batchCount(1024 * 1024); got != 1 {
|
||||
t.Fatalf("unpinned batch should be capped to 1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdaptiveBatchStopsAtFloor(t *testing.T) {
|
||||
c := newTestConn(4, 32)
|
||||
seen := map[int]bool{}
|
||||
for i := 0; i < 12; i++ {
|
||||
_, next, _ := c.shrinkPipeline()
|
||||
seen[next] = true
|
||||
}
|
||||
if c.pipeline != 4 {
|
||||
t.Fatalf("batch fell to %d, want the floor 4", c.pipeline)
|
||||
}
|
||||
if !seen[16] || !seen[8] {
|
||||
t.Fatalf("expected halving through 16 and 8, saw %v", seen)
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
c.growPipeline()
|
||||
}
|
||||
if c.pipeline != 32 {
|
||||
t.Fatalf("batch grew to %d, want the ceiling 32", c.pipeline)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleBatchIsFixed(t *testing.T) {
|
||||
c := newTestConn(1, 1)
|
||||
if !c.pinnedBatch() {
|
||||
t.Fatal("a 1..1 batch must be treated as pinned")
|
||||
}
|
||||
c.growPipeline()
|
||||
if c.pipeline != 1 {
|
||||
t.Fatalf("batch of 1 grew to %d", c.pipeline)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnectZeroMeansPersistent(t *testing.T) {
|
||||
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
|
||||
if lane.reconnectEvery != 0 {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
"dragontcp/internal/xorchunk"
|
||||
)
|
||||
|
||||
const maxHeader = 128 * 1024
|
||||
@@ -251,7 +252,7 @@ func writeHTTPError(conn net.Conn, code int, reason, detail string) {
|
||||
_, _ = conn.Write(body)
|
||||
}
|
||||
|
||||
func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer int, chunkOpts chunkClientOptions, slots chan struct{}) {
|
||||
func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer int, wires *wireSelector, slots chan struct{}) {
|
||||
defer func() {
|
||||
<-slots
|
||||
_ = conn.Close()
|
||||
@@ -285,7 +286,7 @@ func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer i
|
||||
|
||||
var remote net.Conn
|
||||
if transport == "chunk" {
|
||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
||||
remote, err = wires.dial(host, port)
|
||||
} else {
|
||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
||||
}
|
||||
@@ -327,7 +328,7 @@ func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer i
|
||||
|
||||
var remote net.Conn
|
||||
if transport == "chunk" {
|
||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
||||
remote, err = wires.dial(host, port)
|
||||
} else {
|
||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
||||
}
|
||||
@@ -358,27 +359,28 @@ func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer i
|
||||
|
||||
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")
|
||||
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)")
|
||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
||||
chunkStart = flag.Int("chunk-start", 1048576, "initial adaptive chunk payload bytes")
|
||||
chunkMin = flag.Int("chunk-min", 32, "minimum adaptive chunk payload bytes")
|
||||
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")
|
||||
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")
|
||||
chunkConcurrency = flag.Int("chunk-concurrency", 1, "maximum download records per request (1-256)")
|
||||
chunkConcurrencyMin = flag.Int("chunk-concurrency-min", 1, "minimum download records per request (1-256); equal to --chunk-concurrency pins the depth and disables batch adaptation")
|
||||
chunkReconnect = flag.Int("chunk-reconnect-every", 0, "force reconnect after N logical requests; 0 = persistent/automatic")
|
||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||
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")
|
||||
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)")
|
||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
||||
chunkStart = flag.Int("chunk-start", 1048576, "initial adaptive chunk payload bytes")
|
||||
chunkMin = flag.Int("chunk-min", 32, "minimum adaptive chunk payload bytes")
|
||||
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")
|
||||
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")
|
||||
chunkConcurrency = flag.Int("chunk-concurrency", 1, "maximum download records per request (1-256)")
|
||||
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, "force reconnect after N logical requests; 0 = persistent/automatic")
|
||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||
wireMode = flag.String("wire", "auto", "wire mode: b, x, or auto (probe and pick)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -426,6 +428,17 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-concurrency-min must not exceed --chunk-concurrency")
|
||||
os.Exit(2)
|
||||
}
|
||||
*wireMode = strings.ToLower(strings.TrimSpace(*wireMode))
|
||||
switch *wireMode {
|
||||
case WireBinary, WireXOR, WireAuto:
|
||||
case "binary":
|
||||
*wireMode = WireBinary
|
||||
case "xor":
|
||||
*wireMode = WireXOR
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "--wire must be b, x or auto")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkReconnect < 0 {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-reconnect-every must be 0 or greater")
|
||||
os.Exit(2)
|
||||
@@ -438,14 +451,19 @@ func main() {
|
||||
adaptSuccesses: *chunkSuccesses,
|
||||
adaptLog: *chunkAdaptLog,
|
||||
pollers: *chunkPollers,
|
||||
minPipeline: *chunkConcurrencyMin,
|
||||
maxPipeline: *chunkConcurrency,
|
||||
reconnectEvery: *chunkReconnect,
|
||||
pollDelay: *chunkPollDelay,
|
||||
txnTimeout: *chunkTimeout,
|
||||
tcpBuffer: *tcpBuffer,
|
||||
minPipeline: *chunkConcurrencyMin,
|
||||
maxPipeline: *chunkConcurrency,
|
||||
}
|
||||
|
||||
xorOpts := xorchunk.NewOptions(
|
||||
*chunkStart, *chunkMin, *chunkMax, *chunkAdaptive, *chunkSuccesses, *chunkAdaptLog,
|
||||
*chunkPollers, *chunkReconnect, *chunkPollDelay, *chunkTimeout, *tcpBuffer,
|
||||
)
|
||||
|
||||
listenAddr := net.JoinHostPort(*listenHost, strconv.Itoa(*listenPort))
|
||||
serverAddr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
|
||||
|
||||
@@ -480,6 +498,16 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
wires := newWireSelector(*wireMode, serverAddr, *token, chunkOpts, xorOpts)
|
||||
if *wireMode == WireAuto {
|
||||
fmt.Printf("wire=auto probing %s\n", probeHost)
|
||||
// Resolve in the background so startup is not blocked; a connection that
|
||||
// arrives first simply waits for the same result.
|
||||
go wires.mode()
|
||||
} else {
|
||||
fmt.Printf("wire=%s (manual)\n", *wireMode)
|
||||
}
|
||||
|
||||
slots := make(chan struct{}, *maxConnections)
|
||||
|
||||
for {
|
||||
@@ -491,7 +519,7 @@ func main() {
|
||||
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
go handleLocal(conn, serverAddr, *token, *transport, *tcpBuffer, chunkOpts, slots)
|
||||
go handleLocal(conn, serverAddr, *token, *transport, *tcpBuffer, wires, slots)
|
||||
default:
|
||||
writeHTTPError(
|
||||
conn,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -160,8 +160,10 @@ func handle(
|
||||
tcpBuffer int,
|
||||
slots chan struct{},
|
||||
manager *streamManager,
|
||||
xorManager *chunkManager,
|
||||
chunkMax int,
|
||||
bufferBytes int,
|
||||
chunkBuffered int,
|
||||
chunkPollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) {
|
||||
@@ -174,6 +176,26 @@ func handle(
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
||||
|
||||
// One listener serves both wires. The legacy XOR framing starts every
|
||||
// request with the ASCII magic "UP"; the binary framing starts with a mode
|
||||
// byte of 0-4, so the two are never ambiguous.
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
conn, isXOR, err := sniffWire(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if isXOR {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("WIRE peer=%v mode=xor", conn.RemoteAddr())
|
||||
}
|
||||
handleXOR(conn, token, allowPrivate, cache, tcpBuffer, xorManager,
|
||||
chunkMax, chunkBuffered, chunkPollWait, debug)
|
||||
return
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("WIRE peer=%v mode=binary", conn.RemoteAddr())
|
||||
}
|
||||
|
||||
for {
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
req, err := wire.ReadRequest(conn)
|
||||
@@ -249,6 +271,7 @@ func main() {
|
||||
bufferBytes = 64 * 1024 * 1024
|
||||
}
|
||||
manager := newStreamManager(*sessionTimeout, debug)
|
||||
xorManager := newChunkManager(*sessionTimeout, debug)
|
||||
fmt.Printf("binary_transport=true chunk_max=%d buffer_bytes=%d poll_wait=%s\n", *chunkMax, bufferBytes, chunkPollWait.String())
|
||||
if debug.enabled {
|
||||
fmt.Printf("debug=true debug_chunks=%t stats_interval=%s\n", debug.chunks, debug.statsEvery)
|
||||
@@ -275,8 +298,10 @@ func main() {
|
||||
*tcpBuffer,
|
||||
slots,
|
||||
manager,
|
||||
xorManager,
|
||||
*chunkMax,
|
||||
bufferBytes,
|
||||
*chunkBuffered,
|
||||
*chunkPollWait,
|
||||
debug,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
package main
|
||||
|
||||
// This file is the LiteVPN v4 XOR chunk server, carried over verbatim. It
|
||||
// handles the legacy UP/OK + XOR 0xAD wire (COPEN / CPUSH / CPULL / CCLOSE and
|
||||
// the TUNNEL stream commands) so one server accepts both wire formats. The
|
||||
// binary record handler lives in chunk.go; nothing here is shared with it
|
||||
// except the target dialler, DNS cache, token check and debug counters.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
type chunkSession struct {
|
||||
id string
|
||||
target net.Conn
|
||||
maxChunk int
|
||||
maxChunks int
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
chunks map[uint64][]byte
|
||||
nextDown uint64
|
||||
eof bool
|
||||
closed bool
|
||||
lastSeen time.Time
|
||||
debug *serverDebug
|
||||
|
||||
upMu sync.Mutex
|
||||
expectedUp uint64
|
||||
lastUpSeq uint64
|
||||
lastUpLen int
|
||||
haveLastUp bool
|
||||
}
|
||||
|
||||
func newChunkSession(id string, target net.Conn, maxChunk, maxChunks int, debug *serverDebug) *chunkSession {
|
||||
s := &chunkSession{
|
||||
id: id,
|
||||
target: target,
|
||||
maxChunk: maxChunk,
|
||||
maxChunks: maxChunks,
|
||||
notify: make(chan struct{}),
|
||||
chunks: make(map[uint64][]byte, maxChunks),
|
||||
lastSeen: time.Now(),
|
||||
debug: debug,
|
||||
}
|
||||
go s.readTarget()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *chunkSession) signalLocked() {
|
||||
close(s.notify)
|
||||
s.notify = make(chan struct{})
|
||||
}
|
||||
|
||||
func (s *chunkSession) touchLocked() {
|
||||
s.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (s *chunkSession) touch() {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *chunkSession) readTarget() {
|
||||
buf := make([]byte, s.maxChunk)
|
||||
|
||||
for {
|
||||
n, err := s.target.Read(buf)
|
||||
if n > 0 {
|
||||
data := append([]byte(nil), buf[:n]...)
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesDown.Add(uint64(n))
|
||||
}
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(s.chunks) < s.maxChunks {
|
||||
seq := s.nextDown
|
||||
s.nextDown++
|
||||
s.chunks[seq] = data
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
break
|
||||
}
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.logf("TARGET EOF session=%s err=%v", s.id, err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
if !s.closed {
|
||||
s.eof = true
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// push is idempotent for the most recently accepted sequence. This matters
|
||||
// when the server receives a record but the tiny ACK is lost: the client can
|
||||
// retry the same sequence at a smaller adaptive size without duplicating bytes
|
||||
// in the target stream. The ACK reports the length that was actually accepted.
|
||||
func (s *chunkSession) push(seq uint64, data []byte) (int, error) {
|
||||
s.upMu.Lock()
|
||||
defer s.upMu.Unlock()
|
||||
|
||||
if len(data) == 0 || len(data) > s.maxChunk {
|
||||
return 0, fmt.Errorf("upload record size %d is invalid", len(data))
|
||||
}
|
||||
|
||||
if s.haveLastUp && seq == s.lastUpSeq {
|
||||
s.touch()
|
||||
return s.lastUpLen, nil
|
||||
}
|
||||
|
||||
if seq < s.expectedUp {
|
||||
return 0, fmt.Errorf("upload sequence %d is too old", seq)
|
||||
}
|
||||
if seq > s.expectedUp {
|
||||
return 0, fmt.Errorf("unexpected upload sequence %d, expected %d", seq, s.expectedUp)
|
||||
}
|
||||
|
||||
if _, err := s.target.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesUp.Add(uint64(len(data)))
|
||||
s.debug.pushRecords.Add(1)
|
||||
}
|
||||
|
||||
s.lastUpSeq = seq
|
||||
s.lastUpLen = len(data)
|
||||
s.haveLastUp = true
|
||||
s.expectedUp++
|
||||
s.touch()
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// pull returns at most limit bytes from the requested stored chunk, beginning
|
||||
// at offset. The chunk sequence stays stable while the client retries smaller
|
||||
// fragments, so a large queued chunk can always be recovered after an MTU-like
|
||||
// failure without reopening the proxied destination connection.
|
||||
func (s *chunkSession) pull(want uint64, ack int64, offset, limit int, wait time.Duration) (data []byte, total int, eof bool, final uint64, waitExpired bool, err error) {
|
||||
if offset < 0 || limit <= 0 || limit > s.maxChunk {
|
||||
return nil, 0, false, 0, false, fmt.Errorf("invalid pull offset/limit")
|
||||
}
|
||||
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
|
||||
if ack >= 0 {
|
||||
removed := false
|
||||
for seq := range s.chunks {
|
||||
if seq <= uint64(ack) {
|
||||
delete(s.chunks, seq)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
s.signalLocked()
|
||||
}
|
||||
}
|
||||
|
||||
if chunk, ok := s.chunks[want]; ok {
|
||||
if offset >= len(chunk) {
|
||||
s.mu.Unlock()
|
||||
return nil, len(chunk), false, 0, false, fmt.Errorf("pull offset %d beyond chunk size %d", offset, len(chunk))
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(chunk) {
|
||||
end = len(chunk)
|
||||
}
|
||||
out := append([]byte(nil), chunk[offset:end]...)
|
||||
total = len(chunk)
|
||||
s.mu.Unlock()
|
||||
return out, total, false, 0, false, nil
|
||||
}
|
||||
|
||||
if s.eof && want >= s.nextDown {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
if s.closed {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
continue
|
||||
case <-timer.C:
|
||||
return nil, 0, false, 0, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chunkSession) close() {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
_ = s.target.Close()
|
||||
}
|
||||
|
||||
type chunkManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*chunkSession
|
||||
timeout time.Duration
|
||||
debug *serverDebug
|
||||
}
|
||||
|
||||
func newChunkManager(timeout time.Duration, debug *serverDebug) *chunkManager {
|
||||
m := &chunkManager{
|
||||
sessions: make(map[string]*chunkSession),
|
||||
timeout: timeout,
|
||||
debug: debug,
|
||||
}
|
||||
go m.cleanupLoop()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *chunkManager) get(id string) *chunkSession {
|
||||
m.mu.RLock()
|
||||
s := m.sessions[id]
|
||||
m.mu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *chunkManager) count() int {
|
||||
m.mu.RLock()
|
||||
n := len(m.sessions)
|
||||
m.mu.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *chunkManager) add(id string, s *chunkSession) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, exists := m.sessions[id]; exists {
|
||||
return fmt.Errorf("session already exists")
|
||||
}
|
||||
m.sessions[id] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *chunkManager) remove(id string) {
|
||||
m.mu.Lock()
|
||||
s := m.sessions[id]
|
||||
delete(m.sessions, id)
|
||||
m.mu.Unlock()
|
||||
if s != nil {
|
||||
s.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *chunkManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
cutoff := time.Now().Add(-m.timeout)
|
||||
var stale []string
|
||||
|
||||
m.mu.RLock()
|
||||
for id, s := range m.sessions {
|
||||
s.mu.Lock()
|
||||
last := s.lastSeen
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if closed || last.Before(cutoff) {
|
||||
stale = append(stale, id)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, id := range stale {
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.logf("SESSION timeout-close id=%s active_sessions=%d", id, m.count())
|
||||
}
|
||||
m.remove(id)
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.sessionsClosed.Add(1)
|
||||
m.debug.activeSessions.Add(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeWireToken(token string) string {
|
||||
if token == "-" {
|
||||
return ""
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func isChunkCommand(payload []byte) bool {
|
||||
return bytes.HasPrefix(payload, []byte("COPEN ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPUSH ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPULL ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CCLOSE "))
|
||||
}
|
||||
|
||||
func processChunkCommand(
|
||||
conn net.Conn,
|
||||
requestID uint32,
|
||||
payload []byte,
|
||||
token string,
|
||||
allowPrivate bool,
|
||||
cache *dnsCache,
|
||||
tcpBuffer int,
|
||||
manager *chunkManager,
|
||||
maxChunk int,
|
||||
maxBufferedChunks int,
|
||||
pollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) error {
|
||||
if bytes.HasPrefix(payload, []byte("COPEN ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad COPEN"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := parts[2]
|
||||
if len(sid) < 16 || len(sid) > 64 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid session id"))
|
||||
}
|
||||
host := parts[3]
|
||||
port, err := strconv.Atoi(parts[4])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, host, port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
|
||||
session := newChunkSession(sid, target, maxChunk, maxBufferedChunks, debug)
|
||||
if err := manager.add(sid, session); err != nil {
|
||||
session.close()
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("COPEN session=%s target=%s:%d failed: %v", sid, host, port, err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsOpened.Add(1)
|
||||
debug.activeSessions.Add(1)
|
||||
debug.logf("SESSION OPEN id=%s peer=%v target=%s:%d max_chunk=%d active_sessions=%d", sid, conn.RemoteAddr(), host, port, maxChunk, manager.count())
|
||||
debug.chunkf("COPEN id=%s target=%s:%d -> OPENED max=%d", sid, host, port, maxChunk)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("OPENED %d", maxChunk)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPUSH ")) {
|
||||
parts := bytes.SplitN(payload, []byte(" "), 5)
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPUSH"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(string(parts[1])), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := string(parts[2])
|
||||
seq, err := strconv.ParseUint(string(parts[3]), 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid sequence"))
|
||||
}
|
||||
s := manager.get(sid)
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
accepted, err := s.push(seq, parts[4])
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("CPUSH id=%s seq=%d bytes=%d: %v", sid, seq, len(parts[4]), err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil {
|
||||
debug.chunkf("CPUSH id=%s seq=%d bytes=%d -> ACK accepted=%d", sid, seq, len(parts[4]), accepted)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("ACK %d %d", seq, accepted)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPULL ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 7 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPULL"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
s := manager.get(parts[2])
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
ack, err := strconv.ParseInt(parts[3], 10, 64)
|
||||
if err != nil || ack < -1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid ack"))
|
||||
}
|
||||
want, err := strconv.ParseUint(parts[4], 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid want"))
|
||||
}
|
||||
offset, err := strconv.Atoi(parts[5])
|
||||
if err != nil || offset < 0 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid offset"))
|
||||
}
|
||||
limit, err := strconv.Atoi(parts[6])
|
||||
if err != nil || limit < 1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid limit"))
|
||||
}
|
||||
if limit > maxChunk {
|
||||
limit = maxChunk
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.pullRequests.Add(1)
|
||||
debug.chunkf("CPULL id=%s ack=%d want=%d offset=%d limit=%d", parts[2], ack, want, offset, limit)
|
||||
}
|
||||
|
||||
data, total, eof, final, waitExpired, err := s.pull(want, ack, offset, limit, pollWait)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if waitExpired {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.waitRecords.Add(1)
|
||||
debug.chunkf("CPULL id=%s want=%d -> WAIT", parts[2], want)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("WAIT"))
|
||||
}
|
||||
if eof {
|
||||
if debug != nil {
|
||||
debug.chunkf("CPULL id=%s want=%d -> EOF final=%d", parts[2], want, final)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("EOF %d", final)))
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.dataRecords.Add(1)
|
||||
debug.chunkf("DATA id=%s seq=%d offset=%d bytes=%d total=%d", parts[2], want, offset, len(data), total)
|
||||
}
|
||||
prefix := []byte(fmt.Sprintf("DATA %d %d %d ", want, offset, total))
|
||||
out := make([]byte, len(prefix)+len(data))
|
||||
copy(out, prefix)
|
||||
copy(out[len(prefix):], data)
|
||||
return protocol.WriteResponseFrame(conn, requestID, out)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CCLOSE ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 3 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CCLOSE"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
manager.remove(parts[2])
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsClosed.Add(1)
|
||||
debug.activeSessions.Add(-1)
|
||||
debug.logf("SESSION CLOSE id=%s peer=%v active_sessions=%d", parts[2], conn.RemoteAddr(), manager.count())
|
||||
debug.chunkf("CCLOSE id=%s -> CLOSED", parts[2])
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("CLOSED"))
|
||||
}
|
||||
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown chunk command"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wire detection and the XOR connection loop.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// prefixedConn replays bytes already consumed for wire detection before falling
|
||||
// through to the socket. Using a plain bufio.Reader would be wrong here: the
|
||||
// TUNNEL path relays the raw connection, so anything the detector buffered
|
||||
// beyond the magic would be lost.
|
||||
type prefixedConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (p *prefixedConn) Read(b []byte) (int, error) { return p.r.Read(b) }
|
||||
|
||||
// sniffWire reads the two magic bytes and reports whether this connection
|
||||
// speaks the legacy XOR framing. The returned conn replays them.
|
||||
func sniffWire(conn net.Conn) (net.Conn, bool, error) {
|
||||
var magic [2]byte
|
||||
if _, err := io.ReadFull(conn, magic[:]); err != nil {
|
||||
return conn, false, err
|
||||
}
|
||||
replayed := &prefixedConn{Conn: conn, r: io.MultiReader(bytes.NewReader(magic[:]), conn)}
|
||||
return replayed, magic[0] == 'U' && magic[1] == 'P', nil
|
||||
}
|
||||
|
||||
// handleXOR serves one connection speaking UP/OK + XOR 0xAD: the v4 chunk
|
||||
// commands, plus the TUNNEL/TUNNEL2 stream commands.
|
||||
func handleXOR(
|
||||
conn net.Conn,
|
||||
token string,
|
||||
allowPrivate bool,
|
||||
cache *dnsCache,
|
||||
tcpBuffer int,
|
||||
manager *chunkManager,
|
||||
chunkMax int,
|
||||
chunkBuffered int,
|
||||
chunkPollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) {
|
||||
for {
|
||||
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
|
||||
|
||||
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled && err != io.EOF {
|
||||
debug.errorf("peer=%v read XOR request: %v", conn.RemoteAddr(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isChunkCommand(payload) {
|
||||
if err := processChunkCommand(
|
||||
conn, requestID, payload, token, allowPrivate, cache, tcpBuffer,
|
||||
manager, chunkMax, chunkBuffered, chunkPollWait, debug,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(string(payload))
|
||||
transport := "xor"
|
||||
if len(parts) == 4 && parts[0] == "TUNNEL" {
|
||||
transport = "xor"
|
||||
} else if len(parts) == 5 && parts[0] == "TUNNEL2" {
|
||||
transport = strings.ToLower(parts[4])
|
||||
if transport != "raw" && transport != "xor" {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR transport must be RAW or XOR"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR expected TUNNEL, TUNNEL2, or chunk command"))
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenEqual(parts[1], token) {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
return
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(parts[3])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
||||
return
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, parts[2], port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("TUNNEL target=%s:%d connect failed: %v", parts[2], port, err)
|
||||
}
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
if err := protocol.WriteResponseFrame(conn, requestID, []byte("CONNECTED")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
if transport == "raw" {
|
||||
protocol.RelayRaw(conn, target)
|
||||
} else {
|
||||
protocol.RelayXOR(conn, target)
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL closed peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user