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
+499
View File
@@ -0,0 +1,499 @@
package main
import (
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/protocol"
"dragontcp/internal/wire"
)
const (
bpModeProbe byte = 0
bpModeUpload byte = 1
bpModeDownload byte = 2
bpModeBatchDownload byte = 3
bpModeACK byte = 4
bpHeaderSize = 29
)
var bpOpenMagic = [4]byte{'D', 'O', 'P', '1'}
var bpCloseMagic = [4]byte{'D', 'C', 'L', '1'}
type bpPhysicalConn struct {
conn net.Conn
requests int
}
type bpLane struct {
mu sync.Mutex
serverAddr string
tcpBuffer int
reconnectEvery int
timeout time.Duration
coverProfile cover.Profile
autoReconnect bool
pc *bpPhysicalConn
closed bool
}
func newBPLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, coverProfile cover.Profile) *bpLane {
autoReconnect := reconnectEvery == 1
if autoReconnect {
reconnectEvery = 0
}
return &bpLane{
serverAddr: serverAddr,
tcpBuffer: tcpBuffer,
reconnectEvery: reconnectEvery,
timeout: timeout,
coverProfile: coverProfile,
autoReconnect: autoReconnect,
}
}
func (l *bpLane) transportFailureLocked(reused bool) {
if l.autoReconnect && reused {
l.reconnectEvery = 1
}
l.discardLocked()
}
func (l *bpLane) discardLocked() {
if l.pc != nil {
_ = l.pc.conn.Close()
l.pc = nil
}
}
func (l *bpLane) closeAfterLocked() {
if l.pc != nil && l.reconnectEvery > 0 && l.pc.requests >= l.reconnectEvery {
l.discardLocked()
}
}
func (l *bpLane) ensureLocked() error {
if l.closed {
return net.ErrClosed
}
if l.pc != nil {
if l.reconnectEvery <= 0 || l.pc.requests < l.reconnectEvery {
return nil
}
l.discardLocked()
}
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
conn, err := d.Dial("tcp", l.serverAddr)
if err != nil {
return err
}
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
_ = conn.Close()
return err
}
protocol.TuneTCP(conn)
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
l.pc = &bpPhysicalConn{conn: conn}
return nil
}
func (l *bpLane) Close() {
l.mu.Lock()
l.closed = true
l.discardLocked()
l.mu.Unlock()
}
func writeBPRequest(w io.Writer, mode byte, sid wire.SessionID, seq uint64, payload []byte, downloadHint uint32, headerMask byte, clear bool) error {
n := uint32(len(payload))
if mode == bpModeDownload {
n = downloadHint
payload = nil
}
if len(payload) > wire.MaxPayload {
return fmt.Errorf("BP payload too large: %d", len(payload))
}
var header [bpHeaderSize]byte
header[0] = mode ^ headerMask
copy(header[1:17], sid[:])
binary.BigEndian.PutUint64(header[17:25], seq)
binary.BigEndian.PutUint32(header[25:29], n)
if clear {
buffers := net.Buffers{header[:], payload}
_, err := buffers.WriteTo(w)
return err
}
packet := make([]byte, bpHeaderSize+len(payload))
copy(packet[:bpHeaderSize], header[:])
copy(packet[bpHeaderSize:], payload)
wire.MaskInPlace(packet[bpHeaderSize:], sid, mode, seq, false)
for len(packet) > 0 {
written, err := w.Write(packet)
if err != nil {
return err
}
if written <= 0 {
return io.ErrShortWrite
}
packet = packet[written:]
}
return nil
}
func readBPResponse(r io.Reader, sid wire.SessionID, mode byte, seq uint64, headerMask byte, clear bool) (byte, []byte, error) {
status, body, err := wire.ReadResponseProfile(r, headerMask)
if err == nil && status != wire.StatusError && len(body) > 0 && !clear {
wire.MaskInPlace(body, sid, mode, seq, true)
}
return status, body, err
}
func (l *bpLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte, downloadHint uint32) (byte, []byte, error) {
l.mu.Lock()
defer l.mu.Unlock()
timeout := l.timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
if err := l.ensureLocked(); err != nil {
lastErr = err
continue
}
reused := l.pc.requests > 0
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
if err := writeBPRequest(l.pc.conn, mode, sid, seq, payload, downloadHint, l.coverProfile.HeaderMask, l.coverProfile.Clear); err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
status, body, err := readBPResponse(l.pc.conn, sid, mode, seq, l.coverProfile.HeaderMask, l.coverProfile.Clear)
if err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
return status, body, nil
}
return 0, nil, fmt.Errorf("BP request failed after reconnect: %w", lastErr)
}
func decodeBPData(body []byte) ([]byte, error) {
if len(body) < 4 {
return nil, fmt.Errorf("short BP DATA body")
}
n := int(binary.BigEndian.Uint32(body[:4]))
if n < 0 || n > len(body)-4 {
return nil, fmt.Errorf("bad BP DATA length")
}
return append([]byte(nil), body[4:4+n]...), nil
}
func (l *bpLane) download(sid wire.SessionID, offset uint64, maxChunk, count int) ([][]byte, byte, error) {
l.mu.Lock()
defer l.mu.Unlock()
timeout := l.timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
mode := bpModeDownload
payload := []byte(nil)
hint := uint32(maxChunk)
if count > 1 {
mode = bpModeBatchDownload
payload = make([]byte, 6)
binary.BigEndian.PutUint32(payload[:4], uint32(maxChunk))
binary.BigEndian.PutUint16(payload[4:6], uint16(count))
hint = 0
}
responses := 1
if mode == bpModeBatchDownload {
responses = count
}
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
if err := l.ensureLocked(); err != nil {
lastErr = err
continue
}
reused := l.pc.requests > 0
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
if err := writeBPRequest(l.pc.conn, mode, sid, offset, payload, hint, l.coverProfile.HeaderMask, l.coverProfile.Clear); err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
out := make([][]byte, 0, responses)
lastStatus := wire.StatusOK
for i := 0; i < responses; i++ {
status, body, err := readBPResponse(l.pc.conn, sid, mode, offset, l.coverProfile.HeaderMask, l.coverProfile.Clear)
if err != nil {
lastErr = err
l.transportFailureLocked(reused)
goto retry
}
lastStatus = status
switch status {
case wire.StatusData:
data, err := decodeBPData(body)
if err != nil {
l.discardLocked()
return out, status, err
}
if len(data) > 0 {
out = append(out, data)
}
case wire.StatusOK, wire.StatusWait:
case wire.StatusEOF:
case wire.StatusError:
l.discardLocked()
return out, status, fmt.Errorf("%s", string(body))
default:
l.discardLocked()
return out, status, fmt.Errorf("unexpected BP download status %d", status)
}
}
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
return out, lastStatus, nil
retry:
}
return nil, 0, fmt.Errorf("BP download request failed after reconnect: %w", lastErr)
}
type bpConn struct {
sid wire.SessionID
opts chunkClientOptions
uploadLane *bpLane
downloadLane *bpLane
upSizer *adaptiveSizer
downSizer *adaptiveSizer
writeMu sync.Mutex
upOffset uint64
readMu sync.Mutex
readBuf []byte
downloadOffset uint64
consumedOffset uint64
lastAck uint64
eof bool
pipeline int
closeOnce sync.Once
}
func openBPTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
if opts.minSize < 32 {
opts.minSize = 32
}
if opts.maxSize < opts.minSize {
opts.maxSize = opts.minSize
}
if opts.maxSize > 1024*1024 {
opts.maxSize = 1024 * 1024
}
if opts.startSize < opts.minSize || opts.startSize > opts.maxSize {
opts.startSize = opts.maxSize
}
if opts.txnTimeout <= 0 {
opts.txnTimeout = 5 * time.Second
}
if opts.maxPipeline < 1 {
opts.maxPipeline = 1
}
if opts.maxPipeline > 256 {
opts.maxPipeline = 256
}
if opts.minPipeline < 1 {
opts.minPipeline = 1
}
if opts.minPipeline > opts.maxPipeline {
opts.minPipeline = opts.maxPipeline
}
reconnect := opts.reconnectEvery
if reconnect < 0 {
reconnect = 0
}
sid, err := randomSessionID()
if err != nil {
return nil, err
}
uploadLane := newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile)
status, body, err := uploadLane.single(bpModeUpload, sid, 0, nil, 0)
if err != nil {
uploadLane.Close()
return nil, err
}
if status == wire.StatusError {
uploadLane.Close()
return nil, fmt.Errorf("%s", string(body))
}
if status != wire.StatusOK {
uploadLane.Close()
return nil, fmt.Errorf("bad BP registration response %d", status)
}
openPayload, err := encodeOpen(token, targetHost, targetPort)
if err != nil {
uploadLane.Close()
return nil, err
}
openPayload = append(append([]byte(nil), bpOpenMagic[:]...), openPayload...)
status, body, err = uploadLane.single(bpModeUpload, sid, 1, openPayload, 0)
if err != nil {
uploadLane.Close()
return nil, err
}
if status == wire.StatusError {
uploadLane.Close()
return nil, fmt.Errorf("%s", string(body))
}
if status != wire.StatusOK {
uploadLane.Close()
return nil, fmt.Errorf("bad BP OPEN response %d", status)
}
c := &bpConn{
sid: sid,
opts: opts,
uploadLane: uploadLane,
downloadLane: newBPLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.coverProfile),
pipeline: opts.maxPipeline,
}
c.upSizer = newAdaptiveSizer("BP upload", opts.startSize, opts)
c.downSizer = newAdaptiveSizer("BP download", opts.startSize, opts)
return c, nil
}
func (c *bpConn) fillReadBuffer() error {
if c.eof {
return io.EOF
}
for len(c.readBuf) == 0 && !c.eof {
if c.consumedOffset > c.lastAck {
status, body, err := c.downloadLane.single(bpModeACK, c.sid, c.consumedOffset, nil, 0)
if err != nil {
return err
}
if status == wire.StatusError {
return fmt.Errorf("%s", string(body))
}
c.lastAck = c.consumedOffset
}
chunk := c.downSizer.Current()
count := c.pipeline
if count < c.opts.minPipeline {
count = c.opts.minPipeline
}
if count > c.opts.maxPipeline {
count = c.opts.maxPipeline
}
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, chunk, count)
if err != nil {
old, next := c.downSizer.FailureReason(chunk, err)
if old == next && next == c.opts.minSize {
return err
}
time.Sleep(30 * time.Millisecond)
continue
}
c.readBuf = appendChunkParts(c.readBuf, data)
for _, part := range data {
c.downloadOffset += uint64(len(part))
}
if len(data) > 0 {
c.downSizer.Success(chunk)
if c.pipeline < c.opts.maxPipeline {
c.pipeline++
}
}
if status == wire.StatusEOF {
c.eof = true
}
if len(c.readBuf) == 0 && !c.eof {
delay := c.opts.pollDelay
if delay <= 0 {
delay = 5 * time.Millisecond
}
time.Sleep(delay)
}
}
if c.eof && len(c.readBuf) == 0 {
return io.EOF
}
return nil
}
func (c *bpConn) Read(p []byte) (int, error) {
c.readMu.Lock()
defer c.readMu.Unlock()
if len(p) == 0 {
return 0, nil
}
if len(c.readBuf) == 0 {
if err := c.fillReadBuffer(); err != nil {
return 0, err
}
}
n := copy(p, c.readBuf)
c.readBuf = c.readBuf[n:]
c.consumedOffset += uint64(n)
return n, nil
}
func (c *bpConn) Write(p []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
total := 0
for len(p) > 0 {
size := c.upSizer.Current()
n := minInt(size, len(p))
status, body, err := c.uploadLane.single(bpModeUpload, c.sid, c.upOffset+2, p[:n], 0)
if err != nil {
old, next := c.upSizer.FailureReason(size, err)
if old == next && next == c.opts.minSize {
return total, err
}
time.Sleep(30 * time.Millisecond)
continue
}
if status == wire.StatusError {
return total, fmt.Errorf("%s", string(body))
}
if status != wire.StatusOK {
return total, fmt.Errorf("unexpected BP upload status %d", status)
}
c.upOffset += uint64(n)
total += n
p = p[n:]
c.upSizer.Success(size)
}
return total, nil
}
func (c *bpConn) Close() error {
c.closeOnce.Do(func() {
_, _, _ = c.downloadLane.single(bpModeACK, c.sid, c.consumedOffset, bpCloseMagic[:], 0)
c.uploadLane.Close()
c.downloadLane.Close()
})
return nil
}
func (c *bpConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-bp-local") }
func (c *bpConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-bp-remote") }
func (c *bpConn) SetDeadline(time.Time) error { return nil }
func (c *bpConn) SetReadDeadline(time.Time) error { return nil }
func (c *bpConn) SetWriteDeadline(time.Time) error { return nil }
+162 -78
View File
@@ -11,6 +11,7 @@ import (
"sync/atomic"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/protocol"
"dragontcp/internal/wire"
)
@@ -29,6 +30,9 @@ type chunkClientOptions struct {
tcpBuffer int
minPipeline int
maxPipeline int
headerMask byte
coverProfile cover.Profile
skipPathProbe bool
}
type adaptiveSizer struct {
@@ -120,6 +124,10 @@ func (s *adaptiveSizer) Success(attempted int) {
}
func (s *adaptiveSizer) Failure(attempted int) (int, int) {
return s.FailureReason(attempted, nil)
}
func (s *adaptiveSizer) FailureReason(attempted int, cause error) (int, int) {
s.mu.Lock()
defer s.mu.Unlock()
old := s.current
@@ -147,7 +155,11 @@ func (s *adaptiveSizer) Failure(attempted int) (int, int) {
}
s.current = next
if s.logChanges && old != next {
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
if cause != nil {
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure: %v\n", s.name, old, next, cause)
} else {
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
}
}
return old, next
}
@@ -163,19 +175,39 @@ type requestLane struct {
tcpBuffer int
reconnectEvery int
timeout time.Duration
headerMask byte
coverProfile cover.Profile
autoReconnect bool
pc *physicalConn
closed bool
}
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *requestLane {
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *requestLane {
autoReconnect := reconnectEvery == 1
if autoReconnect {
// Auto starts persistent. If a request fails only after this lane has
// already completed traffic on the connection, it learns that reuse is
// unsafe and switches itself to one request per connection.
reconnectEvery = 0
}
return &requestLane{
serverAddr: serverAddr,
tcpBuffer: tcpBuffer,
reconnectEvery: reconnectEvery,
timeout: timeout,
headerMask: headerMask,
coverProfile: coverProfile,
autoReconnect: autoReconnect,
}
}
func (l *requestLane) transportFailureLocked(reused bool) {
if l.autoReconnect && reused {
l.reconnectEvery = 1
}
l.discardLocked()
}
func (l *requestLane) discardLocked() {
if l.pc != nil {
_ = l.pc.conn.Close()
@@ -204,6 +236,10 @@ func (l *requestLane) ensureLocked() error {
if err != nil {
return err
}
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
_ = conn.Close()
return err
}
protocol.TuneTCP(conn)
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
l.pc = &physicalConn{conn: conn}
@@ -220,30 +256,38 @@ func (l *requestLane) Close() {
func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte) (byte, []byte, error) {
l.mu.Lock()
defer l.mu.Unlock()
if err := l.ensureLocked(); err != nil {
return 0, nil, err
}
timeout := l.timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
if err := wire.WriteRequest(l.pc.conn, mode, sid, seq, payload); err != nil {
l.discardLocked()
return 0, nil, err
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
if err := l.ensureLocked(); err != nil {
lastErr = err
continue
}
reused := l.pc.requests > 0
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
if err := wire.WriteRequestProfileEncoding(l.pc.conn, mode, sid, seq, payload, l.headerMask, l.coverProfile.Clear); err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
if err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
if status != wire.StatusError && len(body) > 0 && !l.coverProfile.Clear {
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
}
return status, body, nil
}
status, body, err := wire.ReadResponse(l.pc.conn)
if err != nil {
l.discardLocked()
return 0, nil, err
}
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
if status != wire.StatusError && len(body) > 0 {
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
}
return status, body, nil
return 0, nil, fmt.Errorf("request failed after reconnect: %w", lastErr)
}
// download sends one compact request and consumes up to count response records.
@@ -252,61 +296,71 @@ func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload
func (l *requestLane) download(sid wire.SessionID, startOffset, ackOffset uint64, maxChunk, count int) ([][]byte, byte, error) {
l.mu.Lock()
defer l.mu.Unlock()
if err := l.ensureLocked(); err != nil {
return nil, 0, err
}
timeout := l.timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
payload := make([]byte, 14)
binary.BigEndian.PutUint64(payload[0:8], ackOffset)
binary.BigEndian.PutUint32(payload[8:12], uint32(maxChunk))
binary.BigEndian.PutUint16(payload[12:14], uint16(count))
if err := wire.WriteRequest(l.pc.conn, wire.ModeDownload, sid, startOffset, payload); err != nil {
l.discardLocked()
return nil, 0, err
}
out := make([][]byte, 0, count)
offset := startOffset
lastStatus := wire.StatusOK
for i := 0; i < count; i++ {
status, body, err := wire.ReadResponse(l.pc.conn)
if err != nil {
l.discardLocked()
return out, lastStatus, err
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
if err := l.ensureLocked(); err != nil {
lastErr = err
continue
}
lastStatus = status
switch status {
case wire.StatusData:
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
if len(body) == 0 {
reused := l.pc.requests > 0
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
if err := wire.WriteRequestProfileEncoding(l.pc.conn, wire.ModeDownload, sid, startOffset, payload, l.headerMask, l.coverProfile.Clear); err != nil {
lastErr = err
l.transportFailureLocked(reused)
continue
}
out := make([][]byte, 0, count)
offset := startOffset
lastStatus := wire.StatusOK
for i := 0; i < count; i++ {
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
if err != nil {
lastErr = err
l.transportFailureLocked(reused)
goto retry
}
lastStatus = status
switch status {
case wire.StatusData:
if !l.coverProfile.Clear {
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
}
if len(body) == 0 {
l.discardLocked()
return out, status, fmt.Errorf("empty DATA response")
}
out = append(out, body)
offset += uint64(len(body))
case wire.StatusWait, wire.StatusEOF:
i = count // stop after this response
case wire.StatusError:
l.discardLocked()
return out, status, fmt.Errorf("empty DATA response")
return out, status, fmt.Errorf("%s", string(body))
default:
l.discardLocked()
return out, status, fmt.Errorf("unknown response status %d", status)
}
if status == wire.StatusWait || status == wire.StatusEOF {
break
}
out = append(out, body)
offset += uint64(len(body))
case wire.StatusWait, wire.StatusEOF:
i = count // stop after this response
case wire.StatusError:
l.discardLocked()
return out, status, fmt.Errorf("%s", string(body))
default:
l.discardLocked()
return out, status, fmt.Errorf("unknown response status %d", status)
}
if status == wire.StatusWait || status == wire.StatusEOF {
break
}
}
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
return out, lastStatus, nil
l.pc.requests++
_ = l.pc.conn.SetDeadline(time.Time{})
l.closeAfterLocked()
return out, lastStatus, nil
retry:
}
return nil, 0, fmt.Errorf("download request failed after reconnect: %w", lastErr)
}
type pathProfile struct {
@@ -364,7 +418,7 @@ func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, cand
if timeout <= 0 || timeout > 2500*time.Millisecond {
timeout = 2500 * time.Millisecond
}
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout)
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
defer lane.Close()
seq := probeSeq.Add(1)
@@ -405,7 +459,7 @@ func probePersistent(serverAddr, token string, opts chunkClientOptions) bool {
if timeout <= 0 || timeout > 2500*time.Millisecond {
timeout = 2500 * time.Millisecond
}
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout)
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout, opts.headerMask, opts.coverProfile)
defer lane.Close()
for i := 0; i < 8; i++ {
seq := probeSeq.Add(1)
@@ -459,7 +513,7 @@ func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte)
}
func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile {
key := fmt.Sprintf("%s|%s|%d|%d", serverAddr, token, opts.minSize, opts.maxSize)
key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear)
profileState.Lock()
if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute {
p := profileState.p
@@ -533,6 +587,31 @@ type chunkConn struct {
closeOnce sync.Once
}
// appendChunkParts keeps the single-response fast path zero-copy. For a batch,
// it reserves the complete size once rather than repeatedly growing and copying
// the aggregate read buffer.
func appendChunkParts(dst []byte, parts [][]byte) []byte {
if len(parts) == 0 {
return dst
}
if len(dst) == 0 && len(parts) == 1 {
return parts[0]
}
total := len(dst)
for _, part := range parts {
total += len(part)
}
if cap(dst) < total {
grown := make([]byte, len(dst), total)
copy(grown, dst)
dst = grown
}
for _, part := range parts {
dst = append(dst, part...)
}
return dst
}
func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
if opts.minSize < 32 {
opts.minSize = 32
@@ -565,16 +644,21 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
opts.minPipeline = opts.maxPipeline
}
profile := getPathProfile(serverAddr, token, opts)
profile := pathProfile{
upload: opts.minSize,
download: opts.minSize,
persistent: false,
at: time.Now(),
}
if !opts.skipPathProbe {
profile = getPathProfile(serverAddr, token, opts)
}
reconnect := opts.reconnectEvery
// Compatibility-friendly reconnect modes:
// 0 = persistent (CLI explicit)
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
// 1 = auto: start persistent, then learn one request/connection only if
// reuse fails during real traffic
// N>=2 = force connection rotation after N logical requests
// 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 {
@@ -583,7 +667,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
// 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)
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile)
payload, err := encodeOpen(token, targetHost, targetPort)
if err != nil {
uploadLane.Close()
@@ -624,7 +708,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
opts: opts,
serverMax: serverMax,
uploadLane: uploadLane,
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile),
// 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
@@ -659,8 +743,8 @@ func (c *chunkConn) fillReadBuffer() error {
}
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
c.readBuf = appendChunkParts(c.readBuf, data)
for _, part := range data {
c.readBuf = append(c.readBuf, part...)
c.downloadOffset += uint64(len(part))
}
if len(data) > 0 {
@@ -678,10 +762,10 @@ func (c *chunkConn) fillReadBuffer() error {
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)
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure: %v\n", old, c.pipeline, err)
}
} else {
old, next := c.downSizer.Failure(chunk)
old, next := c.downSizer.FailureReason(chunk, err)
if old == next && next == c.opts.minSize {
minFailures++
if minFailures >= 8 {
@@ -745,7 +829,7 @@ func (c *chunkConn) Write(p []byte) (int, error) {
n := minInt(size, len(p))
status, body, err := c.uploadLane.single(wire.ModeUpload, c.sid, c.upOffset, p[:n])
if err != nil {
old, next := c.upSizer.Failure(size)
old, next := c.upSizer.FailureReason(size, err)
if old == next && next == c.opts.minSize {
minFailures++
if minFailures >= 8 {
+72 -2
View File
@@ -1,6 +1,13 @@
package main
import "testing"
import (
"net"
"testing"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/wire"
)
func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
opts := chunkClientOptions{
@@ -23,9 +30,72 @@ func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
}
}
func TestReconnectAutoLearnsFromRealReuseFailure(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
serverErr := make(chan error, 1)
go func() {
first, err := ln.Accept()
if err != nil {
serverErr <- err
return
}
if _, err := wire.ReadRequest(first); err != nil {
serverErr <- err
return
}
if err := wire.WriteResponse(first, wire.StatusOK, nil); err != nil {
serverErr <- err
return
}
_ = first.Close() // Force the next logical request to reconnect.
second, err := ln.Accept()
if err != nil {
serverErr <- err
return
}
defer second.Close()
if _, err := wire.ReadRequest(second); err != nil {
serverErr <- err
return
}
serverErr <- wire.WriteResponse(second, wire.StatusOK, nil)
}()
lane := newRequestLane(ln.Addr().String(), 0, 1, time.Second, 0, cover.Profile{})
defer lane.Close()
if !lane.autoReconnect || lane.reconnectEvery != 0 {
t.Fatalf("auto lane started auto=%t reconnectEvery=%d", lane.autoReconnect, lane.reconnectEvery)
}
var sid wire.SessionID
if status, _, err := lane.single(wire.ModeProbe, sid, 1, nil); err != nil || status != wire.StatusOK {
t.Fatalf("first request status=%d err=%v", status, err)
}
if status, _, err := lane.single(wire.ModeProbe, sid, 2, nil); err != nil || status != wire.StatusOK {
t.Fatalf("retried request status=%d err=%v", status, err)
}
if lane.reconnectEvery != 1 || lane.pc != nil {
t.Fatalf("auto lane did not learn single-request mode: reconnectEvery=%d pc=%v", lane.reconnectEvery, lane.pc)
}
if err := <-serverErr; err != nil {
t.Fatal(err)
}
}
func TestReconnectZeroMeansPersistent(t *testing.T) {
lane := newRequestLane("127.0.0.1:1", 0, 0, 0)
lane := newRequestLane("127.0.0.1:1", 0, 0, 0, 0, cover.Profile{})
if lane.reconnectEvery != 0 {
t.Fatalf("reconnectEvery=%d, want 0", lane.reconnectEvery)
}
}
func TestBPAutoStartsPersistent(t *testing.T) {
lane := newBPLane("127.0.0.1:1", 0, 1, time.Second, cover.Profile{})
if !lane.autoReconnect || lane.reconnectEvery != 0 {
t.Fatalf("BP auto lane started auto=%t reconnectEvery=%d", lane.autoReconnect, lane.reconnectEvery)
}
}
+22 -14
View File
@@ -377,10 +377,12 @@ func main() {
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")
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", 2*time.Second, "per-record transaction timeout before adaptive shrink")
wireMode = flag.String("wire", "auto", "wire mode: b, x, or auto (probe and pick)")
chunkTimeout = flag.Duration("chunk-timeout", 5*time.Second, "per-record transaction timeout before adaptive shrink")
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)")
wireProbeThreads = flag.Int("wire-probe-threads", 1, "maximum concurrent wire profile probes (1-16)")
)
flag.Parse()
@@ -430,19 +432,29 @@ func main() {
}
*wireMode = strings.ToLower(strings.TrimSpace(*wireMode))
switch *wireMode {
case WireBinary, WireXOR, WireAuto:
case WireBinary, WireBP, WireXOR, WireAuto:
case "binary":
*wireMode = WireBinary
case "bh", "h":
*wireMode = WireBP
case "xor":
*wireMode = WireXOR
default:
fmt.Fprintln(os.Stderr, "--wire must be b, x or auto")
fmt.Fprintln(os.Stderr, "--wire must be b, bp, x or auto")
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")
os.Exit(2)
}
if *wireProbeThreads < 1 || *wireProbeThreads > 16 {
fmt.Fprintln(os.Stderr, "--wire-probe-threads must be between 1 and 16")
os.Exit(2)
}
chunkOpts := chunkClientOptions{
startSize: *chunkStart,
minSize: *chunkMin,
@@ -498,15 +510,11 @@ 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)
}
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()
slots := make(chan struct{}, *maxConnections)
+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/")
}
@@ -0,0 +1,171 @@
package main
import (
"sync/atomic"
"testing"
"time"
)
func TestProfileCandidatesCoverBothWireFamilies(t *testing.T) {
selector := &wireSelector{configured: WireAuto}
candidates := selector.profileCandidates()
binaryCount, bpCount, xorCount := 0, 0, 0
seen := make(map[wireChoice]bool, len(candidates))
firstBytes := make(map[byte]bool, 256)
coveredMasks := map[string]map[byte]bool{WireBinary: {}, WireBP: {}, WireXOR: {}}
coveredPadding := map[string]map[uint16]bool{WireBinary: {}, WireBP: {}, WireXOR: {}}
for _, candidate := range candidates {
if seen[candidate] {
t.Fatalf("duplicate candidate: %s", candidate)
}
seen[candidate] = true
if candidate.cover.Enabled {
coveredMasks[candidate.mode][candidate.mask] = true
coveredPadding[candidate.mode][candidate.cover.Padding] = true
}
switch candidate.mode {
case WireBinary:
binaryCount++
if !candidate.cover.Enabled && candidate.mask&7 != 0 {
t.Fatalf("ambiguous binary mask: %02x", candidate.mask)
}
if candidate.cover.Enabled {
firstBytes[byte(candidate.cover.ID>>8)] = true
} else {
for mode := byte(0); mode <= 4; mode++ {
firstBytes[mode^candidate.mask] = true
}
}
case WireXOR:
xorCount++
if !candidate.cover.Enabled && ('U'^candidate.mask)&7 < 5 {
t.Fatalf("ambiguous XOR mask: %02x", candidate.mask)
}
if candidate.cover.Enabled {
firstBytes[byte(candidate.cover.ID>>8)] = true
} else {
firstBytes['U'^candidate.mask] = true
}
case WireBP:
bpCount++
if candidate.cover.Enabled && !candidate.cover.Clear {
t.Fatalf("covered BP profile must use clear payloads: %s", candidate)
}
if !candidate.cover.Enabled && candidate.mask != 0 {
t.Fatalf("direct BP profile must keep a clear header: %s", candidate)
}
default:
t.Fatalf("unknown candidate: %s", candidate)
}
}
if binaryCount != 544 || bpCount != 257 || xorCount != 352 {
t.Fatalf("profiles B=%d BP=%d X=%d, want B=544 BP=257 X=352", binaryCount, bpCount, xorCount)
}
if len(firstBytes) != 256 {
t.Fatalf("profiles cover %d first-byte values, want 256", len(firstBytes))
}
for _, mode := range []string{WireBinary, WireBP, WireXOR} {
if len(coveredMasks[mode]) != 256 {
t.Fatalf("mode %s covers %d masks, want 256", mode, len(coveredMasks[mode]))
}
if len(coveredPadding[mode]) != 16 {
t.Fatalf("mode %s covers %d padding lengths, want 16", mode, len(coveredPadding[mode]))
}
}
}
func TestManualWireStillDiscoversAllProfilesForThatFamily(t *testing.T) {
for _, tc := range []struct {
mode string
want int
}{{WireBinary, 544}, {WireBP, 257}, {WireXOR, 352}} {
selector := &wireSelector{configured: tc.mode}
candidates := selector.profileCandidates()
if len(candidates) != tc.want {
t.Fatalf("mode %s profiles=%d, want %d", tc.mode, len(candidates), tc.want)
}
for _, candidate := range candidates {
if candidate.mode != tc.mode {
t.Fatalf("mode %s included %s", tc.mode, candidate)
}
}
}
}
func TestClearProfilesAreTriedBeforeLegacyFallbacks(t *testing.T) {
for _, mode := range []string{WireBinary, WireBP} {
candidates := (&wireSelector{configured: mode}).profileCandidates()
if len(candidates) < 2 || !candidates[0].cover.Clear {
t.Fatalf("mode %s does not prefer a clear profile", mode)
}
if candidates[1].cover.Enabled {
t.Fatalf("mode %s does not fall back immediately to a legacy direct profile", mode)
}
}
}
func measureDiscoveryConcurrency(t *testing.T, threads int) int32 {
t.Helper()
candidates := make([]wireChoice, 24)
for i := range candidates {
candidates[i] = wireChoice{mode: WireBinary, mask: byte(i * 8)}
}
var active atomic.Int32
var maximum atomic.Int32
var calls atomic.Int32
selector := &wireSelector{
configured: WireAuto,
candidateOverride: candidates,
probeThreads: threads,
probeDelay: time.Nanosecond,
probeOverride: func(wireChoice) bool {
current := active.Add(1)
for {
old := maximum.Load()
if current <= old || maximum.CompareAndSwap(old, current) {
break
}
}
calls.Add(1)
// Keep attempts alive long enough for the globally spaced scheduler
// to fill every configured worker reliably on slower CI runners.
time.Sleep(20 * time.Millisecond)
active.Add(-1)
return false
},
}
if _, ok := selector.detectLocked(); ok {
t.Fatal("unexpected working profile")
}
if calls.Load() != int32(len(candidates)) {
t.Fatalf("screened=%d, want %d", calls.Load(), len(candidates))
}
return maximum.Load()
}
func TestDiscoveryDefaultsToOneWorker(t *testing.T) {
if maximum := measureDiscoveryConcurrency(t, 0); maximum != 1 {
t.Fatalf("maximum concurrent probes=%d, want 1", maximum)
}
}
func TestDiscoveryHonorsConfiguredWorkers(t *testing.T) {
if maximum := measureDiscoveryConcurrency(t, 4); maximum != 4 {
t.Fatalf("maximum concurrent probes=%d, want 4", maximum)
}
}
func TestFailedManualDiscoveryKeepsPinnedWire(t *testing.T) {
for _, mode := range []string{WireBinary, WireBP, WireXOR} {
selector := &wireSelector{
configured: mode,
candidateOverride: []wireChoice{{mode: mode}},
probeDelay: time.Nanosecond,
probeOverride: func(wireChoice) bool { return false },
}
if choice := selector.mode(); choice.mode != mode {
t.Fatalf("configured=%s fallback=%s", mode, choice.mode)
}
}
}
+653
View File
@@ -0,0 +1,653 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"dragontcp/internal/wire"
)
const (
bhttpModeProbe byte = 0
bhttpModeUpload byte = 1
bhttpModeDownload byte = 2
bhttpModeBatchDownload byte = 3
bhttpModeACK byte = 4
bhttpProbeVersion byte = 1
bhttpRequestHeaderSize = 29
)
var bhttpProbeMagic = [4]byte{'B', 'H', 'P', '1'}
var bhttpOpenMagic = [4]byte{'D', 'O', 'P', '1'}
var bpCloseMagic = [4]byte{'D', 'C', 'L', '1'}
// bhttpSession intentionally models only the transport/session behavior that
// is observable in bhttp_remote_test.py. The supplied client test contains no
// destination-selection handshake, so uploads are acknowledged and counted but
// are not forwarded to an invented target.
type bhttpSession struct {
mu sync.Mutex
lastSeen time.Time
uploaded uint64
acked uint64
stream *streamSession
}
func (s *bhttpSession) touch() {
s.mu.Lock()
s.lastSeen = time.Now()
s.mu.Unlock()
}
type bhttpSessionManager struct {
mu sync.RWMutex
sessions map[string]*bhttpSession
timeout time.Duration
max int
}
func newBHTTPSessionManager(timeout time.Duration, max int) *bhttpSessionManager {
if timeout <= 0 {
timeout = 2 * time.Minute
}
if max < 1 {
max = 1
}
m := &bhttpSessionManager{
sessions: make(map[string]*bhttpSession),
timeout: timeout,
max: max,
}
go m.cleanupLoop()
return m
}
func (m *bhttpSessionManager) get(sid wire.SessionID) *bhttpSession {
m.mu.RLock()
s := m.sessions[sidKey(sid)]
m.mu.RUnlock()
if s != nil {
s.touch()
}
return s
}
func (m *bhttpSessionManager) register(sid wire.SessionID) bool {
key := sidKey(sid)
m.mu.Lock()
if old := m.sessions[key]; old != nil {
m.mu.Unlock()
old.touch()
return true
}
if len(m.sessions) >= m.max {
m.mu.Unlock()
return false
}
m.sessions[key] = &bhttpSession{lastSeen: time.Now()}
m.mu.Unlock()
return true
}
func (m *bhttpSessionManager) remove(sid wire.SessionID) bool {
key := sidKey(sid)
m.mu.Lock()
session := m.sessions[key]
delete(m.sessions, key)
m.mu.Unlock()
if session == nil {
return false
}
session.mu.Lock()
stream := session.stream
session.stream = nil
session.mu.Unlock()
if stream != nil {
stream.close()
}
return true
}
func (m *bhttpSessionManager) cleanupLoop() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for now := range ticker.C {
cutoff := now.Add(-m.timeout)
var closing []*streamSession
m.mu.Lock()
for key, session := range m.sessions {
session.mu.Lock()
stale := session.lastSeen.Before(cutoff)
stream := session.stream
session.mu.Unlock()
if stale {
delete(m.sessions, key)
if stream != nil {
closing = append(closing, stream)
}
}
}
m.mu.Unlock()
for _, stream := range closing {
stream.close()
}
}
}
type bhttpRequest struct {
mode byte
session wire.SessionID
seq uint64
value uint32
payload []byte
headerMask byte
clear bool
}
type binaryHeader struct {
mode byte
session wire.SessionID
seq uint64
length uint32
}
func peekBinaryHeader(r *bufio.Reader, headerMask byte) (binaryHeader, error) {
var out binaryHeader
header, err := r.Peek(bhttpRequestHeaderSize)
if err != nil {
return out, err
}
out.mode = header[0] ^ headerMask
copy(out.session[:], header[1:17])
out.seq = binary.BigEndian.Uint64(header[17:25])
out.length = binary.BigEndian.Uint32(header[25:29])
return out, nil
}
func readBHTTPRequest(r *bufio.Reader, headerMask byte, clear bool) (bhttpRequest, error) {
var req bhttpRequest
var header [bhttpRequestHeaderSize]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return req, err
}
req.mode = header[0] ^ headerMask
req.headerMask = headerMask
req.clear = clear
if req.mode > bhttpModeACK {
return req, fmt.Errorf("unknown BP mode")
}
copy(req.session[:], header[1:17])
req.seq = binary.BigEndian.Uint64(header[17:25])
req.value = binary.BigEndian.Uint32(header[25:29])
// BHTTP mode 2 overloads the normal body-length field as a download-size
// hint and sends no payload bytes after the 29-byte header.
if req.mode == bhttpModeDownload {
return req, nil
}
if req.value > wire.MaxPayload {
return req, fmt.Errorf("BP payload too large")
}
if req.value > 0 {
req.payload = make([]byte, int(req.value))
if _, err := io.ReadFull(r, req.payload); err != nil {
return req, err
}
if !clear {
wire.MaskInPlace(req.payload, req.session, req.mode, req.seq, false)
}
}
return req, nil
}
func parseBHTTPProbe(payload []byte) (byte, int, error) {
if len(payload) < 10 || !bytes.Equal(payload[:4], bhttpProbeMagic[:]) || payload[4] != bhttpProbeVersion {
return 0, 0, fmt.Errorf("bad BP probe")
}
submode := payload[5]
if submode > bhttpModeACK {
return 0, 0, fmt.Errorf("unknown BP probe submode")
}
param := int(binary.BigEndian.Uint32(payload[6:10]))
want := 10
if submode == bhttpModeUpload && param >= 10 {
want = param
}
if len(payload) != want {
return 0, 0, fmt.Errorf("bad BP probe length")
}
for i := 10; i < len(payload); i++ {
if payload[i] != byte(i*31) {
return 0, 0, fmt.Errorf("bad BP probe pattern")
}
}
return submode, param, nil
}
func makeBHTTPProbe(submode byte, param int) []byte {
total := 10
if submode == bhttpModeDownload && param > total {
total = param
}
out := make([]byte, total)
copy(out[:4], bhttpProbeMagic[:])
out[4] = bhttpProbeVersion
out[5] = submode
binary.BigEndian.PutUint32(out[6:10], uint32(param))
for i := 10; i < len(out); i++ {
out[i] = byte(i * 31)
}
return out
}
func writeBHTTPError(conn net.Conn, message string) error {
return wire.WriteResponse(conn, wire.StatusError, []byte(message))
}
func writeBHTTPMasked(conn net.Conn, status byte, body []byte, req bhttpRequest) error {
return wire.WriteMaskedResponseProfileEncoding(conn, status, body, req.session, req.mode, req.seq, req.headerMask, req.clear)
}
func writeBHTTPData(conn net.Conn, req bhttpRequest, data []byte) error {
// Build and mask the complete response once. The generic two-step path
// first built a BP body and then copied it into another framed packet,
// temporarily allocating roughly twice the download size.
if req.clear {
var header [wire.ResponseHeaderSize]byte
header[0] = wire.StatusData ^ req.headerMask
binary.BigEndian.PutUint32(header[1:5], uint32(4+len(data)))
var length [4]byte
binary.BigEndian.PutUint32(length[:], uint32(len(data)))
buffers := net.Buffers{header[:], length[:], data}
_, err := buffers.WriteTo(conn)
return err
}
packet := make([]byte, wire.ResponseHeaderSize+4+len(data))
packet[0] = wire.StatusData ^ req.headerMask
binary.BigEndian.PutUint32(packet[1:5], uint32(4+len(data)))
binary.BigEndian.PutUint32(packet[5:9], uint32(len(data)))
copy(packet[9:], data)
wire.MaskInPlace(packet[5:], req.session, req.mode, req.seq, true)
for len(packet) > 0 {
n, err := conn.Write(packet)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
packet = packet[n:]
}
return nil
}
type bhttpServerContext struct {
sessions *bhttpSessionManager
token string
allowPrivate bool
cache *dnsCache
tcpBuffer int
maxChunk int
maxBuffer int
pollWait time.Duration
debug *serverDebug
}
func processBHTTPRequest(conn net.Conn, req bhttpRequest, ctx *bhttpServerContext) error {
sessions := ctx.sessions
maxChunk := ctx.maxChunk
switch req.mode {
case bhttpModeProbe:
submode, param, err := parseBHTTPProbe(req.payload)
if err != nil {
return writeBHTTPError(conn, err.Error())
}
if submode == bhttpModeUpload && len(req.payload) > maxChunk {
return writeBHTTPError(conn, "probe too large")
}
if submode == bhttpModeDownload && (param < 0 || param > maxChunk) {
return writeBHTTPError(conn, "probe too large")
}
count := 1
if submode == bhttpModeACK {
count = param
if count < 1 {
count = 1
}
if count > 256 {
count = 256
}
}
body := makeBHTTPProbe(submode, param)
for i := 0; i < count; i++ {
// The reference client decrypts every batch echo with the original
// request sequence, rather than incrementing it per response.
if err := writeBHTTPMasked(conn, wire.StatusOK, body, req); err != nil {
return err
}
}
return nil
case bhttpModeUpload:
if req.seq == 0 && len(req.payload) == 0 {
if !sessions.register(req.session) {
return writeBHTTPError(conn, "session limit reached")
}
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
session := sessions.get(req.session)
if session == nil {
return writeBHTTPError(conn, "unknown session")
}
if len(req.payload) > maxChunk {
return writeBHTTPError(conn, "upload too large")
}
if req.seq == 1 && len(req.payload) >= len(bhttpOpenMagic) && bytes.Equal(req.payload[:len(bhttpOpenMagic)], bhttpOpenMagic[:]) {
supplied, host, port, err := parseOpen(req.payload[len(bhttpOpenMagic):])
if err != nil {
return writeBHTTPError(conn, err.Error())
}
if !tokenEqual(supplied, ctx.token) {
return writeBHTTPError(conn, "authentication failed")
}
session.mu.Lock()
alreadyOpen := session.stream != nil
session.mu.Unlock()
if alreadyOpen {
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
dialCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
target, err := dialTarget(dialCtx, host, port, ctx.allowPrivate, ctx.cache, ctx.tcpBuffer)
cancel()
if err != nil {
return writeBHTTPError(conn, err.Error())
}
stream := newStreamSession(req.session, target, fmt.Sprintf("%s:%d", host, port), maxChunk, ctx.maxBuffer, ctx.debug)
session.mu.Lock()
if session.stream == nil {
session.stream = stream
session.lastSeen = time.Now()
stream = nil
}
session.mu.Unlock()
if stream != nil {
stream.close()
}
if ctx.debug != nil && ctx.debug.enabled {
ctx.debug.logf("BP OPEN sid=%x target=%s:%d", req.session[:4], host, port)
}
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
session.mu.Lock()
stream := session.stream
session.mu.Unlock()
if stream != nil {
if req.seq < 2 {
return writeBHTTPError(conn, "bad upload sequence")
}
if err := stream.upload(req.seq-2, req.payload); err != nil {
return writeBHTTPError(conn, err.Error())
}
}
session.mu.Lock()
session.uploaded += uint64(len(req.payload))
session.lastSeen = time.Now()
session.mu.Unlock()
return wire.WriteResponse(conn, wire.StatusOK, nil)
case bhttpModeDownload:
session := sessions.get(req.session)
if session == nil {
return writeBHTTPError(conn, "unknown session")
}
session.mu.Lock()
stream := session.stream
session.mu.Unlock()
if stream == nil {
// The reference transport has no observable downstream producer.
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
limit := int(req.value)
if limit < 1 {
limit = 1
}
if limit > maxChunk {
limit = maxChunk
}
data, status, err := stream.readAt(req.seq, limit, ctx.pollWait)
if err != nil {
return writeBHTTPError(conn, err.Error())
}
switch status {
case wire.StatusData:
return writeBHTTPData(conn, req, data)
case wire.StatusEOF:
return wire.WriteResponse(conn, wire.StatusEOF, nil)
default:
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
case bhttpModeBatchDownload:
session := sessions.get(req.session)
if session == nil {
return writeBHTTPError(conn, "unknown session")
}
if len(req.payload) != 6 {
return writeBHTTPError(conn, "bad batch download request")
}
count := int(binary.BigEndian.Uint16(req.payload[4:6]))
limit := int(binary.BigEndian.Uint32(req.payload[:4]))
if limit < 1 {
limit = 1
}
if limit > maxChunk {
limit = maxChunk
}
if count < 1 {
count = 1
}
if count > 256 {
count = 256
}
session.mu.Lock()
stream := session.stream
session.mu.Unlock()
offset := req.seq
for i := 0; i < count; i++ {
if stream == nil {
if err := wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
return err
}
continue
}
wait := time.Duration(0)
if i == 0 {
wait = ctx.pollWait
}
data, status, err := stream.readAt(offset, limit, wait)
if err != nil {
return writeBHTTPError(conn, err.Error())
}
switch status {
case wire.StatusData:
if err := writeBHTTPData(conn, req, data); err != nil {
return err
}
offset += uint64(len(data))
case wire.StatusEOF:
if err := wire.WriteResponse(conn, wire.StatusEOF, nil); err != nil {
return err
}
default:
if err := wire.WriteResponse(conn, wire.StatusOK, nil); err != nil {
return err
}
}
}
return nil
case bhttpModeACK:
session := sessions.get(req.session)
if session == nil {
return writeBHTTPError(conn, "unknown session")
}
// Dragon's BP extension sends an explicit close marker. Reference BP
// clients continue to use an empty ACK, while Dragon clients release the
// target socket and buffered download data immediately instead of waiting
// for the idle-session reaper.
if bytes.Equal(req.payload, bpCloseMagic[:]) {
sessions.remove(req.session)
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
session.mu.Lock()
if req.seq > session.acked {
session.acked = req.seq
}
session.lastSeen = time.Now()
stream := session.stream
session.mu.Unlock()
if stream != nil {
stream.ack(req.seq)
}
return wire.WriteResponse(conn, wire.StatusOK, nil)
}
return writeBHTTPError(conn, "unknown mode")
}
type binaryFlavor byte
const (
binaryFlavorUnknown binaryFlavor = iota
binaryFlavorDragon
binaryFlavorBHTTP
)
func isBHTTPProbe(payload []byte) bool {
return len(payload) >= 4 && bytes.Equal(payload[:4], bhttpProbeMagic[:])
}
// handleBinary auto-detects the two protocols without changing the native B
// header space. BHTTP is clear-header only; Dragon profiles and cover-prefaced
// connections continue through the existing handler unchanged.
func handleBinary(
conn net.Conn,
headerMask byte,
clearPayload bool,
token string,
allowPrivate bool,
cache *dnsCache,
tcpBuffer int,
manager *streamManager,
bhttp *bhttpSessionManager,
chunkMax int,
bufferBytes int,
pollWait time.Duration,
debug *serverDebug,
) {
reader := bufio.NewReader(conn)
bhttpContext := &bhttpServerContext{
sessions: bhttp,
token: token,
allowPrivate: allowPrivate,
cache: cache,
tcpBuffer: tcpBuffer,
maxChunk: chunkMax,
maxBuffer: bufferBytes,
pollWait: pollWait,
debug: debug,
}
flavor := binaryFlavorUnknown
deadline := newIdleDeadline(conn, 30*time.Second)
for {
if deadline.refresh() != nil {
return
}
header, err := peekBinaryHeader(reader, headerMask)
if err != nil {
return
}
if flavor == binaryFlavorUnknown {
switch header.mode {
case bhttpModeProbe:
// Probe framing is shared, so consume it once and use its magic
// to select BHP1 or DTP2 without losing any bytes.
req, err := wire.ReadRequestProfileEncoding(reader, headerMask, clearPayload)
if err != nil {
return
}
if isBHTTPProbe(req.Payload) {
flavor = binaryFlavorBHTTP
breq := bhttpRequest{mode: req.Mode, session: req.Session, seq: req.Seq, value: uint32(len(req.Payload)), payload: req.Payload, headerMask: headerMask, clear: clearPayload}
if processBHTTPRequest(conn, breq, bhttpContext) != nil {
return
}
continue
}
flavor = binaryFlavorDragon
if processWireRequest(conn, req, token, allowPrivate, cache, tcpBuffer, manager, chunkMax, bufferBytes, pollWait, debug) != nil {
return
}
continue
case bhttpModeUpload:
if bhttp.get(header.session) != nil || (header.seq == 0 && header.length == 0) {
flavor = binaryFlavorBHTTP
} else {
flavor = binaryFlavorDragon
}
case bhttpModeDownload:
if bhttp.get(header.session) != nil {
flavor = binaryFlavorBHTTP
} else if manager.get(header.session) != nil {
flavor = binaryFlavorDragon
} else {
// The BHTTP unknown-session test sends only a header whose
// length field is a hint. Consume no nonexistent body.
if _, err := readBHTTPRequest(reader, headerMask, clearPayload); err == nil {
_ = writeBHTTPError(conn, "unknown session")
}
return
}
case bhttpModeBatchDownload:
if bhttp.get(header.session) != nil || header.length == 6 {
flavor = binaryFlavorBHTTP
} else {
flavor = binaryFlavorDragon
}
case bhttpModeACK:
if bhttp.get(header.session) != nil {
flavor = binaryFlavorBHTTP
} else {
flavor = binaryFlavorDragon
}
default:
return
}
}
if flavor == binaryFlavorBHTTP {
req, err := readBHTTPRequest(reader, headerMask, clearPayload)
if err != nil || processBHTTPRequest(conn, req, bhttpContext) != nil {
return
}
continue
}
req, err := wire.ReadRequestProfileEncoding(reader, headerMask, clearPayload)
if err != nil {
return
}
if processWireRequest(conn, req, token, allowPrivate, cache, tcpBuffer, manager, chunkMax, bufferBytes, pollWait, debug) != nil {
return
}
}
}
+301
View File
@@ -0,0 +1,301 @@
package main
import (
"bytes"
"encoding/binary"
"io"
"net"
"testing"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/wire"
)
func writeBHTTPTestRequest(w io.Writer, mode byte, sid wire.SessionID, seq uint64, payload []byte, downloadHint uint32) error {
n := uint32(len(payload))
if mode == bhttpModeDownload {
n = downloadHint
payload = nil
}
packet := make([]byte, bhttpRequestHeaderSize+len(payload))
packet[0] = mode
copy(packet[1:17], sid[:])
binary.BigEndian.PutUint64(packet[17:25], seq)
binary.BigEndian.PutUint32(packet[25:29], n)
copy(packet[29:], payload)
wire.MaskInPlace(packet[29:], sid, mode, seq, false)
_, err := w.Write(packet)
return err
}
func readBHTTPTestResponse(r io.Reader, sid wire.SessionID, mode byte, seq uint64) (byte, []byte, error) {
status, body, err := wire.ReadResponse(r)
if err == nil && status != wire.StatusError {
wire.MaskInPlace(body, sid, mode, seq, true)
}
return status, body, err
}
func startBHTTPTestServer(t *testing.T, sessions *bhttpSessionManager) (net.Conn, <-chan struct{}) {
t.Helper()
server, client := net.Pipe()
done := make(chan struct{})
go func() {
defer close(done)
defer server.Close()
handleBinary(
server,
0,
false,
"",
false,
newDNSCache(time.Minute, 16),
0,
newStreamManager(time.Minute, nil),
sessions,
1024*1024,
1024*1024,
10*time.Millisecond,
nil,
)
}()
return client, done
}
func TestBHTTPReferenceSessionStack(t *testing.T) {
sessions := newBHTTPSessionManager(time.Minute, 32)
client, done := startBHTTPTestServer(t, sessions)
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
var sid wire.SessionID
for i := range sid {
sid[i] = byte(i + 1)
}
if err := writeBHTTPTestRequest(client, bhttpModeUpload, sid, 0, nil, 0); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeUpload, 0); err != nil || status != wire.StatusOK {
t.Fatalf("registration status=%d err=%v", status, err)
}
if err := writeBHTTPTestRequest(client, bhttpModeUpload, sid, 1, []byte("Hello BHTTP"), 0); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeUpload, 1); err != nil || status != wire.StatusOK {
t.Fatalf("upload status=%d err=%v", status, err)
}
// The size is in the header but no 1,350-byte body follows. This is the
// framing difference that made the native Dragon parser wait forever.
if err := writeBHTTPTestRequest(client, bhttpModeDownload, sid, 0, nil, 1350); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeDownload, 0); err != nil || status != wire.StatusOK {
t.Fatalf("download status=%d err=%v", status, err)
}
batch := make([]byte, 6)
binary.BigEndian.PutUint32(batch[:4], 1350)
binary.BigEndian.PutUint16(batch[4:], 2)
if err := writeBHTTPTestRequest(client, bhttpModeBatchDownload, sid, 0, batch, 0); err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeBatchDownload, 0); err != nil || status != wire.StatusOK {
t.Fatalf("batch response %d status=%d err=%v", i, status, err)
}
}
if err := writeBHTTPTestRequest(client, bhttpModeACK, sid, 5, nil, 0); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeACK, 5); err != nil || status != wire.StatusOK {
t.Fatalf("ack status=%d err=%v", status, err)
}
}
func TestBPExplicitCloseRemovesSession(t *testing.T) {
sessions := newBHTTPSessionManager(time.Minute, 32)
client, done := startBHTTPTestServer(t, sessions)
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
var sid wire.SessionID
copy(sid[:], []byte("close-session-01"))
if err := writeBHTTPTestRequest(client, bhttpModeUpload, sid, 0, nil, 0); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeUpload, 0); err != nil || status != wire.StatusOK {
t.Fatalf("registration status=%d err=%v", status, err)
}
if sessions.get(sid) == nil {
t.Fatal("registered session is missing")
}
if err := writeBHTTPTestRequest(client, bhttpModeACK, sid, 0, bpCloseMagic[:], 0); err != nil {
t.Fatal(err)
}
if status, _, err := readBHTTPTestResponse(client, sid, bhttpModeACK, 0); err != nil || status != wire.StatusOK {
t.Fatalf("close status=%d err=%v", status, err)
}
if sessions.get(sid) != nil {
t.Fatal("explicit close retained the session")
}
}
func TestBHTTPReferenceProbeAndBatchEcho(t *testing.T) {
sessions := newBHTTPSessionManager(time.Minute, 32)
client, done := startBHTTPTestServer(t, sessions)
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
var sid wire.SessionID
copy(sid[:], []byte("probe-session-01"))
payload := make([]byte, 10)
copy(payload[:4], []byte("BHP1"))
payload[4] = 1
payload[5] = bhttpModeDownload
binary.BigEndian.PutUint32(payload[6:], 512)
if err := writeBHTTPTestRequest(client, bhttpModeProbe, sid, 0, payload, 0); err != nil {
t.Fatal(err)
}
status, body, err := readBHTTPTestResponse(client, sid, bhttpModeProbe, 0)
if err != nil || status != wire.StatusOK || len(body) != 512 || !bytes.Equal(body[:10], payload) {
t.Fatalf("download probe status=%d len=%d err=%v", status, len(body), err)
}
for i := 10; i < len(body); i++ {
if body[i] != byte(i*31) {
t.Fatalf("probe pattern byte %d=%02x", i, body[i])
}
}
payload[5] = bhttpModeACK
binary.BigEndian.PutUint32(payload[6:], 3)
if err := writeBHTTPTestRequest(client, bhttpModeProbe, sid, 0, payload, 0); err != nil {
t.Fatal(err)
}
for i := 0; i < 3; i++ {
status, body, err := readBHTTPTestResponse(client, sid, bhttpModeProbe, 0)
if err != nil || status != wire.StatusOK || !bytes.Equal(body, payload) {
t.Fatalf("batch probe %d status=%d body=%x err=%v", i, status, body, err)
}
}
}
func TestBHTTPUnknownSessionDownloadHasNoBody(t *testing.T) {
sessions := newBHTTPSessionManager(time.Minute, 32)
client, done := startBHTTPTestServer(t, sessions)
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
var sid wire.SessionID
copy(sid[:], []byte("unknown-session!"))
if err := writeBHTTPTestRequest(client, bhttpModeDownload, sid, 0, nil, 1350); err != nil {
t.Fatal(err)
}
status, _, err := wire.ReadResponse(client)
if err != nil || status == wire.StatusOK || status == wire.StatusData {
t.Fatalf("unknown session status=%d err=%v", status, err)
}
}
func TestBinaryAutoDetectionKeepsNativeDragonProbe(t *testing.T) {
sessions := newBHTTPSessionManager(time.Minute, 32)
client, done := startBHTTPTestServer(t, sessions)
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
var sid wire.SessionID
payload := make([]byte, 11)
copy(payload[:4], wire.ProbeMagic[:])
payload[4] = wire.ProbeKeepalive
if err := wire.WriteRequest(client, wire.ModeProbe, sid, 1, payload); err != nil {
t.Fatal(err)
}
status, _, err := wire.ReadResponse(client)
if err != nil || status != wire.StatusOK {
t.Fatalf("native probe status=%d err=%v", status, err)
}
}
func TestClearCoveredBinaryAndBPProfiles(t *testing.T) {
for _, bp := range []bool{false, true} {
t.Run(map[bool]string{false: "B", true: "BP"}[bp], func(t *testing.T) {
server, client := net.Pipe()
profile := cover.Profile{Enabled: true, ID: 0x8173, Padding: 32, HeaderMask: 0x9b, Clear: true}
done := make(chan struct{})
go func() {
defer close(done)
defer server.Close()
profiled, isXOR, mask, err := sniffWire(server)
if err != nil || isXOR {
return
}
handleBinary(
profiled, mask, true, "", false,
newDNSCache(time.Minute, 16), 0,
newStreamManager(time.Minute, nil),
newBHTTPSessionManager(time.Minute, 32),
1024*1024, 1024*1024, 10*time.Millisecond, nil,
)
}()
defer func() {
client.Close()
<-done
}()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
if err := cover.WritePreface(client, profile); err != nil {
t.Fatal(err)
}
var sid wire.SessionID
copy(sid[:], []byte("clear-profile-01"))
if bp {
payload := makeBHTTPProbe(bhttpModeDownload, 256)[:10]
packet := make([]byte, bhttpRequestHeaderSize+len(payload))
packet[0] = bhttpModeProbe ^ profile.HeaderMask
copy(packet[1:17], sid[:])
binary.BigEndian.PutUint32(packet[25:29], uint32(len(payload)))
copy(packet[29:], payload)
if _, err := client.Write(packet); err != nil {
t.Fatal(err)
}
status, body, err := wire.ReadResponseProfile(client, profile.HeaderMask)
if err != nil || status != wire.StatusOK || !bytes.Equal(body, makeBHTTPProbe(bhttpModeDownload, 256)) {
t.Fatalf("clear BP status=%d len=%d err=%v", status, len(body), err)
}
return
}
payload := make([]byte, 11)
copy(payload[:4], wire.ProbeMagic[:])
payload[4] = wire.ProbeDownload
binary.BigEndian.PutUint32(payload[7:11], 256)
if err := wire.WriteRequestProfileEncoding(client, wire.ModeProbe, sid, 7, payload, profile.HeaderMask, true); err != nil {
t.Fatal(err)
}
status, body, err := wire.ReadResponseProfile(client, profile.HeaderMask)
if err != nil || status != wire.StatusData || !bytes.Equal(body, probePattern(256)) {
t.Fatalf("clear B status=%d len=%d err=%v", status, len(body), err)
}
})
}
}
+5 -2
View File
@@ -9,6 +9,7 @@ import (
"sync"
"time"
"dragontcp/internal/protocol"
"dragontcp/internal/wire"
)
@@ -55,11 +56,13 @@ func (s *streamSession) signalLocked() {
func (s *streamSession) touchLocked() { s.lastSeen = time.Now() }
func (s *streamSession) readTarget() {
tmp := make([]byte, 64*1024)
ptr := protocol.BufferPool.Get().(*[]byte)
tmp := *ptr
defer protocol.BufferPool.Put(ptr)
for {
n, err := s.target.Read(tmp)
if n > 0 {
data := append([]byte(nil), tmp[:n]...)
data := tmp[:n]
for len(data) > 0 {
s.mu.Lock()
for !s.closed && len(s.buf) >= s.maxBuffer {
+181
View File
@@ -1,8 +1,17 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"testing"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/protocol"
"dragontcp/internal/wire"
)
func TestParseOpenAllowsEmptyToken(t *testing.T) {
@@ -20,3 +29,175 @@ func TestParseOpenAllowsEmptyToken(t *testing.T) {
t.Fatalf("got token=%q host=%q port=%d", token, gotHost, port)
}
}
func TestBinaryProfileProbeEndToEnd(t *testing.T) {
for n := 0; n < 256; n += 8 {
mask := byte(n)
server, client := net.Pipe()
clientResult := make(chan error, 1)
go func() {
defer client.Close()
var sid wire.SessionID
payload := make([]byte, 11)
copy(payload[:4], wire.ProbeMagic[:])
payload[4] = wire.ProbeKeepalive
if err := wire.WriteRequestProfile(client, wire.ModeProbe, sid, 1, payload, mask); err != nil {
clientResult <- err
return
}
status, _, err := wire.ReadResponseProfile(client, mask)
if err == nil && status != wire.StatusOK {
err = fmt.Errorf("status=%d", status)
}
clientResult <- err
}()
profiled, isXOR, gotMask, err := sniffWire(server)
if err != nil || isXOR || gotMask != mask {
t.Fatalf("mask %02x sniff: xor=%t gotMask=%02x err=%v", mask, isXOR, gotMask, err)
}
req, err := wire.ReadRequestProfile(profiled, gotMask)
if err == nil {
err = processWireRequest(profiled, req, "", false, nil, 0, nil, 1024, 0, 0, nil)
}
if err != nil {
t.Fatalf("mask %02x server: %v", mask, err)
}
if err := <-clientResult; err != nil {
t.Fatalf("mask %02x client: %v", mask, err)
}
_ = server.Close()
}
}
func TestXORProfileProbeEndToEnd(t *testing.T) {
for n := 0; n < 256; n++ {
mask := byte(n)
if ('U'^mask)&7 < 5 {
continue
}
server, client := net.Pipe()
clientResult := make(chan error, 1)
go func() {
defer client.Close()
if err := protocol.WriteRequestFrameProfile(client, 7, []byte("CPROBE -"), mask); err != nil {
clientResult <- err
return
}
id, payload, err := protocol.ReadResponseFrameProfile(client, mask)
if err == nil && (id != 7 || string(payload) != "PROBEOK") {
err = fmt.Errorf("id=%d payload=%q", id, payload)
}
clientResult <- err
}()
profiled, isXOR, gotMask, err := sniffWire(server)
if err != nil || !isXOR || gotMask != mask {
t.Fatalf("mask %02x sniff: xor=%t gotMask=%02x err=%v", mask, isXOR, gotMask, err)
}
handleXOR(profiled, gotMask, "", false, nil, 0, nil, 1024, 8, time.Millisecond, nil)
if err := <-clientResult; err != nil {
t.Fatalf("mask %02x client: %v", mask, err)
}
_ = server.Close()
}
}
func TestCoveredProfilesProbeEndToEnd(t *testing.T) {
for _, padding := range []uint16{0, 64, cover.MaxPadding} {
for _, xor := range []bool{false, true} {
profile := cover.Profile{Enabled: true, ID: 0x91e7, Padding: padding, HeaderMask: 0x6b, XOR: xor}
server, client := net.Pipe()
clientResult := make(chan error, 1)
go func() {
defer client.Close()
if err := cover.WritePreface(client, profile); err != nil {
clientResult <- err
return
}
if xor {
if err := protocol.WriteRequestFrameProfile(client, 11, []byte("CPROBE -"), profile.HeaderMask); err != nil {
clientResult <- err
return
}
id, payload, err := protocol.ReadResponseFrameProfile(client, profile.HeaderMask)
if err == nil && (id != 11 || string(payload) != "PROBEOK") {
err = fmt.Errorf("id=%d payload=%q", id, payload)
}
clientResult <- err
return
}
var sid wire.SessionID
payload := make([]byte, 11)
copy(payload[:4], wire.ProbeMagic[:])
payload[4] = wire.ProbeKeepalive
if err := wire.WriteRequestProfile(client, wire.ModeProbe, sid, 3, payload, profile.HeaderMask); err != nil {
clientResult <- err
return
}
status, _, err := wire.ReadResponseProfile(client, profile.HeaderMask)
if err == nil && status != wire.StatusOK {
err = fmt.Errorf("status=%d", status)
}
clientResult <- err
}()
profiled, gotXOR, gotMask, err := sniffWire(server)
if err != nil || gotXOR != xor || gotMask != profile.HeaderMask {
t.Fatalf("padding=%d xor=%t sniff got xor=%t mask=%02x err=%v", padding, xor, gotXOR, gotMask, err)
}
if xor {
handleXOR(profiled, gotMask, "", false, nil, 0, nil, 1024, 8, time.Millisecond, nil)
} else {
req, readErr := wire.ReadRequestProfile(profiled, gotMask)
if readErr == nil {
readErr = processWireRequest(profiled, req, "", false, nil, 0, nil, 1024, 0, 0, nil)
}
if readErr != nil {
t.Fatalf("padding=%d binary server: %v", padding, readErr)
}
}
if err := <-clientResult; err != nil {
t.Fatalf("padding=%d xor=%t client: %v", padding, xor, err)
}
_ = server.Close()
}
}
}
func TestSniffWireRecognizesAllHeaderProfiles(t *testing.T) {
test := func(firstTwo []byte, wantXOR bool, wantMask byte) {
server, client := net.Pipe()
defer server.Close()
go func() {
initial := make([]byte, 12)
copy(initial, firstTwo)
_, _ = client.Write(initial)
_ = client.Close()
}()
profiled, gotXOR, gotMask, err := sniffWire(server)
if err != nil {
t.Fatalf("header=%x: %v", firstTwo, err)
}
if gotXOR != wantXOR || gotMask != wantMask {
t.Fatalf("header=%x got xor=%t mask=%02x, want xor=%t mask=%02x", firstTwo, gotXOR, gotMask, wantXOR, wantMask)
}
replayed := make([]byte, 2)
if _, err := io.ReadFull(profiled, replayed); err != nil || !bytes.Equal(replayed, firstTwo) {
t.Fatalf("header=%x replay=%x err=%v", firstTwo, replayed, err)
}
}
for n := 0; n < 256; n += 8 {
mask := byte(n)
test([]byte{mask, 0xa7}, false, mask)
}
for n := 0; n < 256; n++ {
mask := byte(n)
if ('U'^mask)&7 >= 5 {
test([]byte{'U' ^ mask, 'P' ^ mask}, true, mask)
}
}
}
+99 -64
View File
@@ -15,11 +15,32 @@ import (
"time"
"dragontcp/internal/protocol"
"dragontcp/internal/wire"
)
var active int64
// idleDeadline avoids a SetDeadline system call for every small protocol
// record. It refreshes halfway through the idle window, preserving idle-client
// cleanup while making persistent high-throughput lanes substantially cheaper.
type idleDeadline struct {
conn net.Conn
timeout time.Duration
next time.Time
}
func newIdleDeadline(conn net.Conn, timeout time.Duration) *idleDeadline {
return &idleDeadline{conn: conn, timeout: timeout}
}
func (d *idleDeadline) refresh() error {
now := time.Now()
if !d.next.IsZero() && now.Before(d.next.Add(-d.timeout/2)) {
return nil
}
d.next = now.Add(d.timeout)
return d.conn.SetDeadline(d.next)
}
type dnsEntry struct {
ips []netip.Addr
expires time.Time
@@ -160,10 +181,11 @@ func handle(
tcpBuffer int,
slots chan struct{},
manager *streamManager,
bhttpManager *bhttpSessionManager,
xorManager *chunkManager,
chunkMax int,
bufferBytes int,
chunkBuffered int,
xorBufferBytes int,
chunkPollWait time.Duration,
debug *serverDebug,
) {
@@ -176,46 +198,71 @@ 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.
// One listener serves both wires and every startup-selected header profile.
// sniffWire partitions the full first-byte space so B and X remain
// unambiguous even when their legacy mode/UP bytes are masked.
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
conn, isXOR, err := sniffWire(conn)
conn, isXOR, headerMask, err := sniffWire(conn)
if err != nil {
return
}
if isXOR {
if debug != nil && debug.enabled {
debug.logf("WIRE peer=%v mode=xor", conn.RemoteAddr())
debug.logf("WIRE peer=%v mode=xor header_mask=%02x", conn.RemoteAddr(), headerMask)
}
handleXOR(conn, token, allowPrivate, cache, tcpBuffer, xorManager,
chunkMax, chunkBuffered, chunkPollWait, debug)
handleXOR(conn, headerMask, token, allowPrivate, cache, tcpBuffer, xorManager,
chunkMax, xorBufferBytes, chunkPollWait, debug)
return
}
clearPayload := false
if profiled, ok := conn.(interface{ ClearPayload() bool }); ok {
clearPayload = profiled.ClearPayload()
}
if debug != nil && debug.enabled {
debug.logf("WIRE peer=%v mode=binary", conn.RemoteAddr())
debug.logf("WIRE peer=%v mode=binary header_mask=%02x clear_payload=%t", conn.RemoteAddr(), headerMask, clearPayload)
}
handleBinary(conn, headerMask, clearPayload, token, allowPrivate, cache, tcpBuffer, manager,
bhttpManager, chunkMax, bufferBytes, chunkPollWait, debug)
}
func acceptLoop(
ln net.Listener,
token string,
allowPrivate bool,
cache *dnsCache,
tcpBuffer int,
slots chan struct{},
manager *streamManager,
bhttpManager *bhttpSessionManager,
xorManager *chunkManager,
chunkMax int,
bufferBytes int,
xorBufferBytes int,
chunkPollWait time.Duration,
debug *serverDebug,
) {
for {
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
req, err := wire.ReadRequest(conn)
conn, err := ln.Accept()
if err != nil {
return
fmt.Fprintln(os.Stderr, "accept:", err)
continue
}
if err := processWireRequest(
conn,
req,
token,
allowPrivate,
cache,
tcpBuffer,
manager,
chunkMax,
bufferBytes,
chunkPollWait,
debug,
); err != nil {
return
select {
case slots <- struct{}{}:
atomic.AddInt64(&active, 1)
if debug.enabled {
debug.logf("ACCEPT local=%v peer=%v active_connections=%d", conn.LocalAddr(), conn.RemoteAddr(), atomic.LoadInt64(&active))
}
go handle(conn, token, allowPrivate, cache, tcpBuffer, slots, manager,
bhttpManager, xorManager, chunkMax, bufferBytes, xorBufferBytes,
chunkPollWait, debug)
default:
if debug.enabled {
debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
}
_ = conn.Close()
}
}
}
@@ -224,6 +271,7 @@ func main() {
var (
host = flag.String("host", "0.0.0.0", "listen host")
port = flag.Int("port", 53, "listen port")
portAlt = flag.Int("port-alt", 80, "second simultaneous listen port; 0 disables")
token = flag.String("token", "", "optional shared token")
maxConnections = flag.Int("max-connections", 20000, "max simultaneous tunnels")
allowPrivate = flag.Bool("allow-private", false, "allow private/loopback targets")
@@ -231,7 +279,7 @@ func main() {
dnsCacheSize = flag.Int("dns-cache-size", 4096, "maximum cached DNS hostnames")
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (32 bytes to 1 MiB)")
chunkBuffered = flag.Int("chunk-buffered", 256, "compatibility buffer units; 256 = about 16 MiB per active session")
chunkBuffered = flag.Int("chunk-buffered", 32, "per-session download buffer in 64 KiB units; 32 = about 2 MiB")
chunkPollWait = flag.Duration("chunk-poll-wait", 200*time.Millisecond, "server long-poll wait for chunk data")
sessionTimeout = flag.Duration("chunk-session-timeout", 2*time.Minute, "idle chunk session timeout")
debugEnabled = flag.Bool("debug", false, "log session/connect/errors and periodic statistics")
@@ -256,8 +304,23 @@ func main() {
os.Exit(1)
}
defer ln.Close()
listeners := []net.Listener{ln}
fmt.Printf("DragonTCP Go server listening on %s\n", listenAddr)
if *portAlt < 0 || *portAlt > 65535 {
fmt.Fprintln(os.Stderr, "--port-alt must be between 0 and 65535")
os.Exit(2)
}
if *portAlt != 0 && *portAlt != *port {
altAddr := net.JoinHostPort(*host, strconv.Itoa(*portAlt))
alt, altErr := net.Listen("tcp", altAddr)
if altErr != nil {
fmt.Fprintf(os.Stderr, "warning: secondary listener %s unavailable: %v\n", altAddr, altErr)
} else {
defer alt.Close()
listeners = append(listeners, alt)
fmt.Printf("DragonTCP Go server listening on %s\n", altAddr)
}
}
fmt.Printf("max_connections=%d tcp_buffer=%d\n", *maxConnections, *tcpBuffer)
slots := make(chan struct{}, *maxConnections)
@@ -271,45 +334,17 @@ func main() {
bufferBytes = 64 * 1024 * 1024
}
manager := newStreamManager(*sessionTimeout, debug)
bhttpManager := newBHTTPSessionManager(*sessionTimeout, *maxConnections)
xorManager := newChunkManager(*sessionTimeout, debug)
fmt.Printf("binary_transport=true chunk_max=%d buffer_bytes=%d poll_wait=%s\n", *chunkMax, bufferBytes, chunkPollWait.String())
fmt.Printf("binary_transport=true bp_compat=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)
}
for {
conn, err := ln.Accept()
if err != nil {
fmt.Fprintln(os.Stderr, "accept:", err)
continue
}
select {
case slots <- struct{}{}:
atomic.AddInt64(&active, 1)
if debug.enabled {
debug.logf("ACCEPT peer=%v active_connections=%d", conn.RemoteAddr(), atomic.LoadInt64(&active))
}
go handle(
conn,
*token,
*allowPrivate,
cache,
*tcpBuffer,
slots,
manager,
xorManager,
*chunkMax,
bufferBytes,
*chunkBuffered,
*chunkPollWait,
debug,
)
default:
if debug.enabled {
debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
}
_ = conn.Close()
}
for _, listener := range listeners {
go acceptLoop(listener, *token, *allowPrivate, cache, *tcpBuffer, slots,
manager, bhttpManager, xorManager, *chunkMax, bufferBytes,
bufferBytes, *chunkPollWait, debug)
}
select {}
}
+96 -23
View File
@@ -17,6 +17,7 @@ import (
"sync"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/protocol"
)
@@ -24,11 +25,12 @@ type chunkSession struct {
id string
target net.Conn
maxChunk int
maxChunks int
maxBuffer int
mu sync.Mutex
notify chan struct{}
chunks map[uint64][]byte
buffered int
nextDown uint64
eof bool
closed bool
@@ -42,14 +44,28 @@ type chunkSession struct {
haveLastUp bool
}
func newChunkSession(id string, target net.Conn, maxChunk, maxChunks int, debug *serverDebug) *chunkSession {
func newChunkSession(id string, target net.Conn, maxChunk, maxBuffer int, debug *serverDebug) *chunkSession {
if maxBuffer < maxChunk {
maxBuffer = maxChunk
}
readSize := maxChunk
if readSize > 64*1024 {
readSize = 64 * 1024
}
mapCapacity := maxBuffer / readSize
if mapCapacity < 1 {
mapCapacity = 1
}
if mapCapacity > 256 {
mapCapacity = 256
}
s := &chunkSession{
id: id,
target: target,
maxChunk: maxChunk,
maxChunks: maxChunks,
maxBuffer: maxBuffer,
notify: make(chan struct{}),
chunks: make(map[uint64][]byte, maxChunks),
chunks: make(map[uint64][]byte, mapCapacity),
lastSeen: time.Now(),
debug: debug,
}
@@ -73,7 +89,12 @@ func (s *chunkSession) touch() {
}
func (s *chunkSession) readTarget() {
buf := make([]byte, s.maxChunk)
ptr := protocol.BufferPool.Get().(*[]byte)
buf := *ptr
defer protocol.BufferPool.Put(ptr)
if s.maxChunk < len(buf) {
buf = buf[:s.maxChunk]
}
for {
n, err := s.target.Read(buf)
@@ -89,10 +110,11 @@ func (s *chunkSession) readTarget() {
s.mu.Unlock()
return
}
if len(s.chunks) < s.maxChunks {
if s.buffered+len(data) <= s.maxBuffer {
seq := s.nextDown
s.nextDown++
s.chunks[seq] = data
s.buffered += len(data)
s.touchLocked()
s.signalLocked()
s.mu.Unlock()
@@ -181,6 +203,7 @@ func (s *chunkSession) pull(want uint64, ack int64, offset, limit int, wait time
removed := false
for seq := range s.chunks {
if seq <= uint64(ack) {
s.buffered -= len(s.chunks[seq])
delete(s.chunks, seq)
removed = true
}
@@ -333,7 +356,8 @@ func decodeWireToken(token string) string {
}
func isChunkCommand(payload []byte) bool {
return bytes.HasPrefix(payload, []byte("COPEN ")) ||
return bytes.HasPrefix(payload, []byte("CPROBE ")) ||
bytes.HasPrefix(payload, []byte("COPEN ")) ||
bytes.HasPrefix(payload, []byte("CPUSH ")) ||
bytes.HasPrefix(payload, []byte("CPULL ")) ||
bytes.HasPrefix(payload, []byte("CCLOSE "))
@@ -349,10 +373,21 @@ func processChunkCommand(
tcpBuffer int,
manager *chunkManager,
maxChunk int,
maxBufferedChunks int,
maxBufferedBytes int,
pollWait time.Duration,
debug *serverDebug,
) error {
if bytes.HasPrefix(payload, []byte("CPROBE ")) {
parts := strings.Fields(string(payload))
if len(parts) != 2 {
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPROBE"))
}
if !tokenEqual(decodeWireToken(parts[1]), token) {
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
}
return protocol.WriteResponseFrame(conn, requestID, []byte("PROBEOK"))
}
if bytes.HasPrefix(payload, []byte("COPEN ")) {
parts := strings.Fields(string(payload))
if len(parts) != 5 {
@@ -378,7 +413,7 @@ func processChunkCommand(
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
}
session := newChunkSession(sid, target, maxChunk, maxBufferedChunks, debug)
session := newChunkSession(sid, target, maxChunk, maxBufferedBytes, debug)
if err := manager.add(sid, session); err != nil {
session.close()
if debug != nil && debug.enabled {
@@ -521,40 +556,78 @@ func processChunkCommand(
// beyond the magic would be lost.
type prefixedConn struct {
net.Conn
r io.Reader
r io.Reader
headerMask byte
cover cover.Profile
}
func (p *prefixedConn) Read(b []byte) (int, error) { return p.r.Read(b) }
func (p *prefixedConn) Read(b []byte) (int, error) { return p.r.Read(b) }
func (p *prefixedConn) HeaderMask() byte { return p.headerMask }
func (p *prefixedConn) CoverProfile() cover.Profile { return p.cover }
func (p *prefixedConn) ClearPayload() bool { return p.cover.Clear }
// 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
// sniffWire first checks for the optional self-describing cover preface. If it
// is absent, the bytes are replayed and the legacy/direct B/X classifier is
// used unchanged.
func sniffWire(conn net.Conn) (net.Conn, bool, byte, error) {
var initial [cover.PrefaceSize]byte
if _, err := io.ReadFull(conn, initial[:]); err != nil {
return conn, false, 0, err
}
replayed := &prefixedConn{Conn: conn, r: io.MultiReader(bytes.NewReader(magic[:]), conn)}
return replayed, magic[0] == 'U' && magic[1] == 'P', nil
if profile, ok := cover.DecodePreface(initial); ok {
if profile.Padding > 0 {
padding := make([]byte, int(profile.Padding))
if _, err := io.ReadFull(conn, padding); err != nil {
return conn, false, 0, err
}
}
profiled := &prefixedConn{Conn: conn, r: conn, headerMask: profile.HeaderMask, cover: profile}
return profiled, profile.XOR, profile.HeaderMask, nil
}
magic := initial[:2]
replay := io.MultiReader(bytes.NewReader(initial[:]), conn)
if magic[0]&7 >= 5 {
mask := magic[0] ^ 'U'
if magic[1]^mask != 'P' {
return conn, false, 0, fmt.Errorf("unknown wire header")
}
replayed := &prefixedConn{Conn: conn, r: replay, headerMask: mask}
return replayed, true, mask, nil
}
mask := magic[0] & 0xf8
mode := magic[0] ^ mask
if mode > 4 {
return conn, false, 0, fmt.Errorf("unknown binary mode")
}
replayed := &prefixedConn{Conn: conn, r: replay, headerMask: mask}
return replayed, false, mask, 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,
headerMask byte,
token string,
allowPrivate bool,
cache *dnsCache,
tcpBuffer int,
manager *chunkManager,
chunkMax int,
chunkBuffered int,
bufferBytes int,
chunkPollWait time.Duration,
debug *serverDebug,
) {
deadline := newIdleDeadline(conn, 20*time.Second)
for {
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
if deadline.refresh() != nil {
return
}
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
requestID, _, payload, err := protocol.ReadRequestFrameProfile(conn, headerMask)
if err != nil {
if debug != nil && debug.enabled && err != io.EOF {
debug.errorf("peer=%v read XOR request: %v", conn.RemoteAddr(), err)
@@ -565,7 +638,7 @@ func handleXOR(
if isChunkCommand(payload) {
if err := processChunkCommand(
conn, requestID, payload, token, allowPrivate, cache, tcpBuffer,
manager, chunkMax, chunkBuffered, chunkPollWait, debug,
manager, chunkMax, bufferBytes, chunkPollWait, debug,
); err != nil {
return
}
+141
View File
@@ -0,0 +1,141 @@
// Package cover implements the optional connection preface used by startup
// profile discovery. Legacy connections have no preface and remain supported.
package cover
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"fmt"
"io"
)
const (
PrefaceSize = 12
MaxPadding = 4096
)
// Profile is selected once during startup and then reused unchanged. Padding
// bytes are freshly random on each physical connection, but their length and
// all header fields remain fixed.
type Profile struct {
Enabled bool
ID uint16
Padding uint16
HeaderMask byte
XOR bool
Clear bool
}
func (p Profile) String() string {
if !p.Enabled {
return "direct"
}
encoding := "masked"
if p.Clear {
encoding = "clear"
}
return fmt.Sprintf("cover-%04x/pad-%d/%s", p.ID, p.Padding, encoding)
}
func key(id uint16) [32]byte {
var seed [16]byte
copy(seed[:12], []byte("DragonTCP-C3"))
binary.BigEndian.PutUint16(seed[12:14], id)
seed[14], seed[15] = byte(id)^0x6d, byte(id>>8)^0xb2
return sha256.Sum256(seed[:])
}
// EncodePreface returns the fixed-size, self-describing portion. The first two
// bytes are the mutable profile ID; all metadata after them is masked.
func EncodePreface(p Profile) ([PrefaceSize]byte, error) {
var out [PrefaceSize]byte
if !p.Enabled {
return out, fmt.Errorf("cover profile is disabled")
}
if p.Padding > MaxPadding {
return out, fmt.Errorf("cover padding too large: %d", p.Padding)
}
binary.BigEndian.PutUint16(out[0:2], p.ID)
var plain [10]byte
copy(plain[0:4], []byte("DTC3"))
if p.XOR {
plain[4] |= 1
}
if p.Clear {
plain[4] |= 2
}
plain[5] = p.HeaderMask
binary.BigEndian.PutUint16(plain[6:8], p.Padding)
plain[8] = plain[4] ^ plain[5] ^ 0xa5
plain[9] = plain[6] ^ plain[7] ^ 0x5a
k := key(p.ID)
for i := range plain {
out[2+i] = plain[i] ^ k[i]
}
return out, nil
}
// DecodePreface recognizes an encoded cover profile. ok=false means the bytes
// belong to a legacy/direct connection and must be replayed unchanged.
func DecodePreface(in [PrefaceSize]byte) (p Profile, ok bool) {
id := binary.BigEndian.Uint16(in[0:2])
k := key(id)
var plain [10]byte
for i := range plain {
plain[i] = in[2+i] ^ k[i]
}
if string(plain[0:4]) != "DTC3" || plain[4]&^byte(3) != 0 {
return Profile{}, false
}
if plain[8] != plain[4]^plain[5]^0xa5 || plain[9] != plain[6]^plain[7]^0x5a {
return Profile{}, false
}
padding := binary.BigEndian.Uint16(plain[6:8])
if padding > MaxPadding {
return Profile{}, false
}
return Profile{
Enabled: true,
ID: id,
Padding: padding,
HeaderMask: plain[5],
XOR: plain[4]&1 != 0,
Clear: plain[4]&2 != 0,
}, true
}
// WritePreface sends the encoded profile followed by its fixed amount of
// random padding.
func WritePreface(w io.Writer, p Profile) error {
if !p.Enabled {
return nil
}
preface, err := EncodePreface(p)
if err != nil {
return err
}
packet := make([]byte, PrefaceSize+int(p.Padding))
copy(packet, preface[:])
if p.Padding > 0 {
if _, err := rand.Read(packet[PrefaceSize:]); err != nil {
return err
}
}
return writeAll(w, packet)
}
func writeAll(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
b = b[n:]
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package cover
import (
"bytes"
"testing"
)
func TestProfileRoundTripAcrossRange(t *testing.T) {
for id := 0; id < 65536; id += 257 {
for _, xor := range []bool{false, true} {
for _, clear := range []bool{false, true} {
want := Profile{Enabled: true, ID: uint16(id), Padding: uint16(id % (MaxPadding + 1)), HeaderMask: byte(id), XOR: xor, Clear: clear}
encoded, err := EncodePreface(want)
if err != nil {
t.Fatal(err)
}
got, ok := DecodePreface(encoded)
if !ok || got != want {
t.Fatalf("id=%04x xor=%t clear=%t got=%+v ok=%t", id, xor, clear, got, ok)
}
}
}
}
}
func TestWritePrefaceIncludesFixedPadding(t *testing.T) {
p := Profile{Enabled: true, ID: 0x1234, Padding: 64, HeaderMask: 0x9a, XOR: true}
var out bytes.Buffer
if err := WritePreface(&out, p); err != nil {
t.Fatal(err)
}
if out.Len() != PrefaceSize+64 {
t.Fatalf("length=%d", out.Len())
}
var encoded [PrefaceSize]byte
copy(encoded[:], out.Bytes())
got, ok := DecodePreface(encoded)
if !ok || got != p {
t.Fatalf("got=%+v ok=%t", got, ok)
}
}
+31 -4
View File
@@ -31,12 +31,17 @@ var BufferPool = sync.Pool{
}
func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
return ReadRequestFrameProfile(r, 0)
}
// ReadRequestFrameProfile decodes the UP magic after applying headerMask.
func ReadRequestFrameProfile(r io.Reader, headerMask byte) (uint32, uint32, []byte, error) {
var header [14]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return 0, 0, nil, err
}
if header[0] != 'U' || header[1] != 'P' {
if header[0]^headerMask != 'U' || header[1]^headerMask != 'P' {
return 0, 0, nil, errors.New("bad request magic")
}
@@ -58,11 +63,16 @@ func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
}
func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
return WriteRequestFrameProfile(w, requestID, payload, 0)
}
// WriteRequestFrameProfile masks the two-byte UP magic with headerMask.
func WriteRequestFrameProfile(w io.Writer, requestID uint32, payload []byte, headerMask byte) error {
if len(payload) > MaxHandshake {
return errors.New("request frame payload too large")
}
packet := make([]byte, 14+len(payload))
packet[0], packet[1] = 'U', 'P'
packet[0], packet[1] = 'U'^headerMask, 'P'^headerMask
binary.BigEndian.PutUint32(packet[2:6], requestID)
binary.BigEndian.PutUint32(packet[6:10], 0)
binary.BigEndian.PutUint32(packet[10:14], uint32(len(payload)))
@@ -72,12 +82,17 @@ func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
}
func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
return ReadResponseFrameProfile(r, 0)
}
// ReadResponseFrameProfile decodes the OK magic after applying headerMask.
func ReadResponseFrameProfile(r io.Reader, headerMask byte) (uint32, []byte, error) {
var header [10]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return 0, nil, err
}
if header[0] != 'O' || header[1] != 'K' {
if header[0]^headerMask != 'O' || header[1]^headerMask != 'K' {
return 0, nil, fmt.Errorf("bad response magic: %q", header[:2])
}
@@ -98,11 +113,16 @@ func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
}
func WriteResponseFrame(w io.Writer, requestID uint32, payload []byte) error {
return WriteResponseFrameProfile(w, requestID, payload, writerHeaderMask(w))
}
// WriteResponseFrameProfile masks the two-byte OK magic with headerMask.
func WriteResponseFrameProfile(w io.Writer, requestID uint32, payload []byte, headerMask byte) error {
if len(payload) > MaxHandshake {
return errors.New("response frame payload too large")
}
packet := make([]byte, 10+len(payload))
packet[0], packet[1] = 'O', 'K'
packet[0], packet[1] = 'O'^headerMask, 'K'^headerMask
binary.BigEndian.PutUint32(packet[2:6], requestID)
binary.BigEndian.PutUint32(packet[6:10], uint32(len(payload)))
copy(packet[10:], payload)
@@ -121,6 +141,13 @@ func writeAll(w io.Writer, b []byte) error {
return nil
}
func writerHeaderMask(w io.Writer) byte {
if profiled, ok := w.(interface{ HeaderMask() byte }); ok {
return profiled.HeaderMask()
}
return 0
}
func CopyXOR(dst net.Conn, src net.Conn) error {
ptr := BufferPool.Get().(*[]byte)
buf := *ptr
+50
View File
@@ -0,0 +1,50 @@
package protocol
import (
"bytes"
"testing"
)
func TestXORHeaderProfilesRoundTrip(t *testing.T) {
for n := 0; n < 256; n++ {
mask := byte(n)
if ('U'^mask)&7 < 5 {
continue
}
var request bytes.Buffer
if err := WriteRequestFrameProfile(&request, 7, []byte("CPROBE -"), mask); err != nil {
t.Fatal(err)
}
requestID, _, payload, err := ReadRequestFrameProfile(&request, mask)
if err != nil || requestID != 7 || !bytes.Equal(payload, []byte("CPROBE -")) {
t.Fatalf("mask %02x request did not round-trip: id=%d payload=%q err=%v", mask, requestID, payload, err)
}
var response bytes.Buffer
if err := WriteResponseFrameProfile(&response, 7, []byte("PROBEOK"), mask); err != nil {
t.Fatal(err)
}
responseID, payload, err := ReadResponseFrameProfile(&response, mask)
if err != nil || responseID != 7 || !bytes.Equal(payload, []byte("PROBEOK")) {
t.Fatalf("mask %02x response did not round-trip: id=%d payload=%q err=%v", mask, responseID, payload, err)
}
}
}
type profiledBuffer struct {
bytes.Buffer
mask byte
}
func (b *profiledBuffer) HeaderMask() byte { return b.mask }
func TestServerResponseUsesConnectionProfile(t *testing.T) {
profiled := &profiledBuffer{mask: 0x3a}
if err := WriteResponseFrame(profiled, 9, []byte("ok")); err != nil {
t.Fatal(err)
}
if got := profiled.Bytes()[0]; got != 'O'^profiled.mask {
t.Fatalf("first byte=%02x, want %02x", got, byte('O')^profiled.mask)
}
}
+93 -9
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"net"
)
const (
@@ -72,12 +73,36 @@ func MaskInPlace(data []byte, sid SessionID, mode byte, seq uint64, response boo
}
func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte) error {
return WriteRequestProfile(w, mode, sid, seq, plaintext, 0)
}
// WriteRequestProfile writes a binary request whose first byte is XORed with
// headerMask. The remaining framing and payload encoding stay unchanged.
// Masks are selected once at client startup and then remain fixed.
func WriteRequestProfile(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte, headerMask byte) error {
return WriteRequestProfileEncoding(w, mode, sid, seq, plaintext, headerMask, false)
}
// WriteRequestProfileEncoding optionally leaves the payload clear. Clear mode
// is signalled by the connection cover preface, so legacy peers continue to use
// the SHA-256 compatibility mask unchanged.
func WriteRequestProfileEncoding(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte, headerMask byte, clear bool) error {
if len(plaintext) > MaxPayload {
return fmt.Errorf("request payload too large: %d", len(plaintext))
}
if clear {
var header [RequestHeaderSize]byte
header[0] = mode ^ headerMask
copy(header[1:17], sid[:])
binary.BigEndian.PutUint64(header[17:25], seq)
binary.BigEndian.PutUint32(header[25:29], uint32(len(plaintext)))
buffers := net.Buffers{header[:], plaintext}
_, err := buffers.WriteTo(w)
return err
}
packet := make([]byte, RequestHeaderSize+len(plaintext))
packet[0] = mode
packet[0] = mode ^ headerMask
copy(packet[1:17], sid[:])
binary.BigEndian.PutUint64(packet[17:25], seq)
binary.BigEndian.PutUint32(packet[25:29], uint32(len(plaintext)))
@@ -87,13 +112,25 @@ func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext [
}
func ReadRequest(r io.Reader) (Request, error) {
return ReadRequestProfile(r, 0)
}
// ReadRequestProfile decodes a request written with WriteRequestProfile.
func ReadRequestProfile(r io.Reader, headerMask byte) (Request, error) {
return ReadRequestProfileEncoding(r, headerMask, false)
}
func ReadRequestProfileEncoding(r io.Reader, headerMask byte, clear bool) (Request, error) {
var req Request
var header [RequestHeaderSize]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return req, err
}
req.Mode = header[0]
req.Mode = header[0] ^ headerMask
if req.Mode > ModeClose {
return req, errors.New("unknown request mode")
}
copy(req.Session[:], header[1:17])
req.Seq = binary.BigEndian.Uint64(header[17:25])
n := binary.BigEndian.Uint32(header[25:29])
@@ -106,28 +143,53 @@ func ReadRequest(r io.Reader) (Request, error) {
if _, err := io.ReadFull(r, req.Payload); err != nil {
return req, err
}
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
if !clear {
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
}
}
return req, nil
}
func WriteResponse(w io.Writer, status byte, body []byte) error {
return WriteResponseProfile(w, status, body, writerHeaderMask(w))
}
// WriteResponseProfile writes a response using the selected first-byte mask.
func WriteResponseProfile(w io.Writer, status byte, body []byte, headerMask byte) error {
if len(body) > MaxPayload {
return fmt.Errorf("response body too large: %d", len(body))
}
packet := make([]byte, ResponseHeaderSize+len(body))
packet[0] = status
packet[0] = status ^ headerMask
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
copy(packet[5:], body)
return writeAll(w, packet)
}
func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64) error {
return WriteMaskedResponseProfileEncoding(w, status, body, sid, mode, seq, writerHeaderMask(w), writerClearPayload(w))
}
// WriteMaskedResponseProfile combines the normal payload mask with the
// selected first-byte header mask.
func WriteMaskedResponseProfile(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64, headerMask byte) error {
return WriteMaskedResponseProfileEncoding(w, status, body, sid, mode, seq, headerMask, false)
}
func WriteMaskedResponseProfileEncoding(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64, headerMask byte, clear bool) error {
if len(body) > MaxPayload {
return fmt.Errorf("response body too large: %d", len(body))
}
if clear {
var header [ResponseHeaderSize]byte
header[0] = status ^ headerMask
binary.BigEndian.PutUint32(header[1:5], uint32(len(body)))
buffers := net.Buffers{header[:], body}
_, err := buffers.WriteTo(w)
return err
}
packet := make([]byte, ResponseHeaderSize+len(body))
packet[0] = status
packet[0] = status ^ headerMask
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
copy(packet[5:], body)
MaskInPlace(packet[5:], sid, mode, seq, true)
@@ -135,6 +197,11 @@ func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, m
}
func ReadResponse(r io.Reader) (byte, []byte, error) {
return ReadResponseProfile(r, 0)
}
// ReadResponseProfile decodes a response written with a header profile.
func ReadResponseProfile(r io.Reader, headerMask byte) (byte, []byte, error) {
var header [ResponseHeaderSize]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return 0, nil, err
@@ -150,16 +217,19 @@ func ReadResponse(r io.Reader) (byte, []byte, error) {
return 0, nil, err
}
}
return header[0], body, nil
status := header[0] ^ headerMask
if status > StatusEOF {
return 0, nil, errors.New("unknown response status")
}
return status, body, nil
}
func DecodeMaskedResponse(status byte, body []byte, sid SessionID, mode byte, seq uint64) []byte {
if len(body) == 0 || status == StatusError {
return body
}
out := append([]byte(nil), body...)
MaskInPlace(out, sid, mode, seq, true)
return out
MaskInPlace(body, sid, mode, seq, true)
return body
}
func writeAll(w io.Writer, b []byte) error {
@@ -175,3 +245,17 @@ func writeAll(w io.Writer, b []byte) error {
}
return nil
}
func writerHeaderMask(w io.Writer) byte {
if profiled, ok := w.(interface{ HeaderMask() byte }); ok {
return profiled.HeaderMask()
}
return 0
}
func writerClearPayload(w io.Writer) bool {
if profiled, ok := w.(interface{ ClearPayload() bool }); ok {
return profiled.ClearPayload()
}
return false
}
+95 -19
View File
@@ -1,29 +1,105 @@
package wire
import (
"bytes"
"testing"
"bytes"
"io"
"testing"
)
func TestMaskChangesWithSequenceAndRoundTrips(t *testing.T) {
var sid SessionID
for i := range sid { sid[i] = byte(i+1) }
plain := bytes.Repeat([]byte("DragonTCP"), 100)
a := append([]byte(nil), plain...)
b := append([]byte(nil), plain...)
MaskInPlace(a, sid, ModeUpload, 1, false)
MaskInPlace(b, sid, ModeUpload, 2, false)
if bytes.Equal(a, b) { t.Fatal("different sequences produced identical wire bytes") }
MaskInPlace(a, sid, ModeUpload, 1, false)
if !bytes.Equal(a, plain) { t.Fatal("mask did not round-trip") }
var sid SessionID
for i := range sid {
sid[i] = byte(i + 1)
}
plain := bytes.Repeat([]byte("DragonTCP"), 100)
a := append([]byte(nil), plain...)
b := append([]byte(nil), plain...)
MaskInPlace(a, sid, ModeUpload, 1, false)
MaskInPlace(b, sid, ModeUpload, 2, false)
if bytes.Equal(a, b) {
t.Fatal("different sequences produced identical wire bytes")
}
MaskInPlace(a, sid, ModeUpload, 1, false)
if !bytes.Equal(a, plain) {
t.Fatal("mask did not round-trip")
}
}
func TestBinaryHeaderProfilesRoundTrip(t *testing.T) {
var sid SessionID
for i := range sid {
sid[i] = byte(i + 1)
}
for n := 0; n < 256; n += 8 {
mask := byte(n)
var request bytes.Buffer
if err := WriteRequestProfile(&request, ModeUpload, sid, 42, []byte("payload"), mask); err != nil {
t.Fatal(err)
}
if got := request.Bytes()[0]; got != ModeUpload^mask {
t.Fatalf("mask %02x first byte=%02x", mask, got)
}
req, err := ReadRequestProfile(&request, mask)
if err != nil {
t.Fatalf("mask %02x: %v", mask, err)
}
if req.Mode != ModeUpload || req.Seq != 42 || !bytes.Equal(req.Payload, []byte("payload")) {
t.Fatalf("mask %02x request did not round-trip", mask)
}
var response bytes.Buffer
if err := WriteResponseProfile(&response, StatusOK, []byte("ok"), mask); err != nil {
t.Fatal(err)
}
status, body, err := ReadResponseProfile(&response, mask)
if err != nil || status != StatusOK || !bytes.Equal(body, []byte("ok")) {
t.Fatalf("mask %02x response did not round-trip: status=%d body=%q err=%v", mask, status, body, err)
}
}
}
type profiledBuffer struct {
bytes.Buffer
mask byte
}
func (b *profiledBuffer) HeaderMask() byte { return b.mask }
func TestServerResponseUsesConnectionProfile(t *testing.T) {
profiled := &profiledBuffer{mask: 0xa0}
if err := WriteResponse(profiled, StatusOK, []byte("ok")); err != nil {
t.Fatal(err)
}
if got := profiled.Bytes()[0]; got != StatusOK^profiled.mask {
t.Fatalf("first byte=%02x, want %02x", got, StatusOK^profiled.mask)
}
}
func BenchmarkMask1MiB(b *testing.B) {
var sid SessionID
data := make([]byte, 1024*1024)
b.SetBytes(int64(len(data)))
b.ResetTimer()
for i:=0;i<b.N;i++ {
MaskInPlace(data,sid,ModeUpload,uint64(i),false)
}
var sid SessionID
data := make([]byte, 1024*1024)
b.SetBytes(int64(len(data)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
MaskInPlace(data, sid, ModeUpload, uint64(i), false)
}
}
func BenchmarkWriteRequest1MiB(b *testing.B) {
var sid SessionID
data := make([]byte, 1024*1024)
for _, tc := range []struct {
name string
clear bool
}{{"sha256-compat", false}, {"clear", true}} {
b.Run(tc.name, func(b *testing.B) {
b.SetBytes(int64(len(data)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := WriteRequestProfileEncoding(io.Discard, ModeUpload, sid, uint64(i), data, 0, tc.clear); err != nil {
b.Fatal(err)
}
}
})
}
}
+55 -6
View File
@@ -19,6 +19,7 @@ import (
"sync/atomic"
"time"
"dragontcp/internal/cover"
"dragontcp/internal/protocol"
)
@@ -57,6 +58,24 @@ type Options struct {
pollDelay time.Duration
txnTimeout time.Duration
tcpBuffer int
headerMask byte
coverProfile cover.Profile
}
// WithHeaderMask returns a copy using one fixed frame-magic profile. The mask
// is selected during startup discovery and remains unchanged for normal data.
func (o Options) WithHeaderMask(mask byte) Options {
o.headerMask = mask
o.coverProfile = cover.Profile{}
return o
}
// WithCoverProfile returns a copy using a fixed startup-selected preface,
// padding length, and frame mask.
func (o Options) WithCoverProfile(profile cover.Profile) Options {
o.coverProfile = profile
o.headerMask = profile.HeaderMask
return o
}
func wireToken(token string) string {
@@ -219,17 +238,21 @@ type txnLane struct {
tcpBuffer int
reconnectEvery int
timeout time.Duration
headerMask byte
coverProfile cover.Profile
conn net.Conn
count int
closed bool
}
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *txnLane {
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *txnLane {
return &txnLane{
serverAddr: serverAddr,
tcpBuffer: tcpBuffer,
reconnectEvery: reconnectEvery,
timeout: timeout,
headerMask: headerMask,
coverProfile: coverProfile,
}
}
@@ -262,6 +285,10 @@ func (l *txnLane) ensureConn() error {
if err != nil {
return err
}
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
_ = conn.Close()
return err
}
protocol.TuneTCP(conn)
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
l.conn = conn
@@ -285,12 +312,12 @@ func (l *txnLane) Do(payload []byte) ([]byte, error) {
_ = l.conn.SetDeadline(time.Now().Add(timeout))
requestID := requestCounter.Add(1)
if err := protocol.WriteRequestFrame(l.conn, requestID, payload); err != nil {
if err := protocol.WriteRequestFrameProfile(l.conn, requestID, payload, l.headerMask); err != nil {
l.closeLocked()
return nil, err
}
responseID, response, err := protocol.ReadResponseFrame(l.conn)
responseID, response, err := protocol.ReadResponseFrameProfile(l.conn, l.headerMask)
if err != nil {
l.closeLocked()
return nil, err
@@ -324,6 +351,28 @@ func doControl(lane *txnLane, payload []byte) ([]byte, error) {
return nil, lastErr
}
// ProbeProfile performs one small authenticated transaction using the selected
// frame-magic mask. It does not create a target session.
func ProbeProfile(serverAddr, token string, opts Options) bool {
timeout := opts.txnTimeout
if timeout <= 0 || timeout > 2*time.Second {
timeout = 2 * time.Second
}
lane := newTxnLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
defer lane.Close()
resp, err := lane.Do([]byte("CPROBE " + wireToken(token)))
if err != nil {
return false
}
if string(resp) == "PROBEOK" {
return true
}
// Servers predating profile discovery do not know CPROBE, but receiving a
// correctly framed error still proves that the legacy mask-zero header
// survived. The subsequent end-to-end probe remains authoritative.
return opts.headerMask == 0 && strings.HasPrefix(string(resp), "ERR expected TUNNEL")
}
type chunkResult struct {
seq uint64
data []byte
@@ -425,7 +474,7 @@ func Open(serverAddr, token, targetHost string, targetPort int, opts Options) (n
c.upSizer = newAdaptiveSizer("upload", opts)
c.downSizer = newAdaptiveSizer("download", opts)
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout, opts.headerMask, opts.coverProfile)
openPayload := []byte(fmt.Sprintf(
"COPEN %s %s %s %d",
@@ -464,7 +513,7 @@ func Open(serverAddr, token, targetHost string, targetPort int, opts Options) (n
c.pullLanes = make([]*txnLane, opts.pollers)
for i := 0; i < opts.pollers; i++ {
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout, opts.headerMask, opts.coverProfile)
c.pullLanes[i] = lane
c.workers.Add(1)
go c.pullWorker(lane)
@@ -762,7 +811,7 @@ func (c *chunkConn) Close() error {
c.once.Do(func() {
c.cancel()
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout, c.opts.headerMask, c.opts.coverProfile)
_, _ = doControl(lane, []byte(fmt.Sprintf("CCLOSE %s %s", wireToken(c.token), c.sid)))
lane.Close()