New
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user