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
+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
}