V13
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
XORKey byte = 0xAD
|
||||
|
||||
// MaxChunkPayload is the hard application-record payload ceiling.
|
||||
// The adaptive chunk protocol may use any size from 32 bytes through 1 MiB.
|
||||
MaxChunkPayload = 1024 * 1024
|
||||
|
||||
// Framed CPUSH/DATA messages include text metadata in addition to chunk
|
||||
// bytes, so keep the frame ceiling comfortably above MaxChunkPayload.
|
||||
MaxHandshake = 2 * 1024 * 1024
|
||||
)
|
||||
|
||||
// 64 KiB balances throughput with memory use at high connection counts.
|
||||
var BufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 64*1024)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
|
||||
var header [14]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if header[0] != 'U' || header[1] != 'P' {
|
||||
return 0, 0, nil, errors.New("bad request magic")
|
||||
}
|
||||
|
||||
requestID := binary.BigEndian.Uint32(header[2:6])
|
||||
reserved := binary.BigEndian.Uint32(header[6:10])
|
||||
length := binary.BigEndian.Uint32(header[10:14])
|
||||
|
||||
if length > MaxHandshake {
|
||||
return 0, 0, nil, errors.New("handshake payload too large")
|
||||
}
|
||||
|
||||
payload := make([]byte, int(length))
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
XorInPlace(payload)
|
||||
|
||||
return requestID, reserved, payload, nil
|
||||
}
|
||||
|
||||
func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
|
||||
if len(payload) > MaxHandshake {
|
||||
return errors.New("request frame payload too large")
|
||||
}
|
||||
packet := make([]byte, 14+len(payload))
|
||||
packet[0], packet[1] = 'U', 'P'
|
||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
||||
binary.BigEndian.PutUint32(packet[6:10], 0)
|
||||
binary.BigEndian.PutUint32(packet[10:14], uint32(len(payload)))
|
||||
copy(packet[14:], payload)
|
||||
XorInPlace(packet[14:])
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
|
||||
var header [10]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if header[0] != 'O' || header[1] != 'K' {
|
||||
return 0, nil, fmt.Errorf("bad response magic: %q", header[:2])
|
||||
}
|
||||
|
||||
requestID := binary.BigEndian.Uint32(header[2:6])
|
||||
length := binary.BigEndian.Uint32(header[6:10])
|
||||
|
||||
if length > MaxHandshake {
|
||||
return 0, nil, errors.New("handshake response too large")
|
||||
}
|
||||
|
||||
payload := make([]byte, int(length))
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
XorInPlace(payload)
|
||||
|
||||
return requestID, payload, nil
|
||||
}
|
||||
|
||||
func WriteResponseFrame(w io.Writer, requestID uint32, payload []byte) error {
|
||||
if len(payload) > MaxHandshake {
|
||||
return errors.New("response frame payload too large")
|
||||
}
|
||||
packet := make([]byte, 10+len(payload))
|
||||
packet[0], packet[1] = 'O', 'K'
|
||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
||||
binary.BigEndian.PutUint32(packet[6:10], uint32(len(payload)))
|
||||
copy(packet[10:], payload)
|
||||
XorInPlace(packet[10:])
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyXOR(dst net.Conn, src net.Conn) error {
|
||||
ptr := BufferPool.Get().(*[]byte)
|
||||
buf := *ptr
|
||||
defer BufferPool.Put(ptr)
|
||||
|
||||
for {
|
||||
n, err := src.Read(buf)
|
||||
if n > 0 {
|
||||
chunk := buf[:n]
|
||||
XorInPlace(chunk)
|
||||
|
||||
if err2 := writeAll(dst, chunk); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
// No restore pass is needed. The next Read overwrites these bytes.
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func relayPair(a, b net.Conn, copier func(net.Conn, net.Conn) error) {
|
||||
done := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
_ = copier(b, a)
|
||||
if cw, ok := b.(interface{ CloseWrite() error }); ok {
|
||||
_ = cw.CloseWrite()
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = copier(a, b)
|
||||
if cw, ok := a.(interface{ CloseWrite() error }); ok {
|
||||
_ = cw.CloseWrite()
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// Preserve normal TCP half-close semantics. The old implementation set a
|
||||
// 2-second deadline on both connections after the first copy direction
|
||||
// ended, which truncated slow or large responses. Wait for the remaining
|
||||
// direction to drain naturally instead.
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
|
||||
func RelayXOR(a, b net.Conn) {
|
||||
relayPair(a, b, CopyXOR)
|
||||
}
|
||||
|
||||
// RelayRaw allows Go/Linux to use the optimized TCP io.Copy path. On Linux,
|
||||
// TCP-to-TCP copies can use splice, eliminating the userspace XOR/copy loop.
|
||||
func RelayRaw(a, b net.Conn) {
|
||||
relayPair(a, b, func(dst, src net.Conn) error {
|
||||
_, err := io.Copy(dst, src)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func TuneTCP(conn net.Conn) {
|
||||
if tcp, ok := conn.(*net.TCPConn); ok {
|
||||
_ = tcp.SetNoDelay(true)
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// TuneTCPBuffer optionally requests larger kernel socket buffers. A value <= 0
|
||||
// leaves Linux/Android autotuning untouched, which is the recommended default
|
||||
// for large connection counts. For a small number of high-BDP mobile links,
|
||||
// values such as 1048576 or 4194304 can improve throughput.
|
||||
func TuneTCPBuffer(conn net.Conn, size int) {
|
||||
if size <= 0 {
|
||||
return
|
||||
}
|
||||
if tcp, ok := conn.(*net.TCPConn); ok {
|
||||
_ = tcp.SetReadBuffer(size)
|
||||
_ = tcp.SetWriteBuffer(size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build arm || 386
|
||||
|
||||
package protocol
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const xorWordMask32 uint32 = 0xADADADAD
|
||||
|
||||
// XorInPlace is the 32-bit optimized path used by ARMv7/386 builds.
|
||||
// It aligns once, then processes 32 bytes per iteration with native uint32
|
||||
// operations instead of a byte-at-a-time loop.
|
||||
func XorInPlace(b []byte) {
|
||||
n := len(b)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
i := 0
|
||||
for i < n && (uintptr(unsafe.Pointer(&b[i]))&3) != 0 {
|
||||
b[i] ^= XORKey
|
||||
i++
|
||||
}
|
||||
|
||||
for ; i+32 <= n; i += 32 {
|
||||
p := unsafe.Pointer(&b[i])
|
||||
*(*uint32)(unsafe.Add(p, 0)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 4)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 8)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 12)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 16)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 20)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 24)) ^= xorWordMask32
|
||||
*(*uint32)(unsafe.Add(p, 28)) ^= xorWordMask32
|
||||
}
|
||||
|
||||
for ; i+4 <= n; i += 4 {
|
||||
p := (*uint32)(unsafe.Pointer(&b[i]))
|
||||
*p ^= xorWordMask32
|
||||
}
|
||||
|
||||
for ; i < n; i++ {
|
||||
b[i] ^= XORKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package protocol
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const xorWordMask uint64 = 0xADADADADADADADAD
|
||||
|
||||
// XorInPlace is optimized for 64-bit targets (amd64/arm64).
|
||||
//
|
||||
// It aligns the input once, then XORs 64 bytes per loop iteration using
|
||||
// eight native 64-bit operations. This removes the encoding/binary call
|
||||
// overhead from the hot relay path and lets the compiler generate a tight
|
||||
// load/xor/store loop.
|
||||
func XorInPlace(b []byte) {
|
||||
n := len(b)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
// Align the pointer for native uint64 accesses. This is normally already
|
||||
// aligned for pooled relay buffers, but also makes this safe for subslices.
|
||||
for i < n && (uintptr(unsafe.Pointer(&b[i]))&7) != 0 {
|
||||
b[i] ^= XORKey
|
||||
i++
|
||||
}
|
||||
|
||||
for ; i+64 <= n; i += 64 {
|
||||
p := unsafe.Pointer(&b[i])
|
||||
*(*uint64)(unsafe.Add(p, 0)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 8)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 16)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 24)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 32)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 40)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 48)) ^= xorWordMask
|
||||
*(*uint64)(unsafe.Add(p, 56)) ^= xorWordMask
|
||||
}
|
||||
|
||||
for ; i+8 <= n; i += 8 {
|
||||
p := (*uint64)(unsafe.Pointer(&b[i]))
|
||||
*p ^= xorWordMask
|
||||
}
|
||||
|
||||
for ; i < n; i++ {
|
||||
b[i] ^= XORKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build !amd64 && !arm64 && !arm && !386
|
||||
|
||||
package protocol
|
||||
|
||||
// Generic fallback for 32-bit and uncommon architectures.
|
||||
func XorInPlace(b []byte) {
|
||||
for i := range b {
|
||||
b[i] ^= XORKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
RequestHeaderSize = 29
|
||||
ResponseHeaderSize = 5
|
||||
MaxPayload = 2 * 1024 * 1024
|
||||
|
||||
ModeProbe byte = 0
|
||||
ModeOpen byte = 1
|
||||
ModeUpload byte = 2
|
||||
ModeDownload byte = 3
|
||||
ModeClose byte = 4
|
||||
|
||||
StatusOK byte = 0
|
||||
StatusError byte = 1
|
||||
StatusData byte = 2
|
||||
StatusWait byte = 3
|
||||
StatusEOF byte = 4
|
||||
|
||||
ProbeUpload byte = 1
|
||||
ProbeDownload byte = 2
|
||||
ProbeKeepalive byte = 3
|
||||
ProbeBatch byte = 4
|
||||
)
|
||||
|
||||
var ProbeMagic = [4]byte{'D', 'T', 'P', '2'}
|
||||
|
||||
type SessionID [16]byte
|
||||
|
||||
type Request struct {
|
||||
Mode byte
|
||||
Session SessionID
|
||||
Seq uint64
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func MaskInPlace(data []byte, sid SessionID, mode byte, seq uint64, response bool) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var seed [30]byte
|
||||
copy(seed[:16], sid[:])
|
||||
seed[16] = mode
|
||||
binary.BigEndian.PutUint64(seed[17:25], seq)
|
||||
if response {
|
||||
seed[25] = 1
|
||||
}
|
||||
|
||||
var counter uint32
|
||||
for off := 0; off < len(data); {
|
||||
binary.BigEndian.PutUint32(seed[26:30], counter)
|
||||
block := sha256.Sum256(seed[:])
|
||||
n := len(data) - off
|
||||
if n > len(block) {
|
||||
n = len(block)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
data[off+i] ^= block[i]
|
||||
}
|
||||
off += n
|
||||
counter++
|
||||
}
|
||||
}
|
||||
|
||||
func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte) error {
|
||||
if len(plaintext) > MaxPayload {
|
||||
return fmt.Errorf("request payload too large: %d", len(plaintext))
|
||||
}
|
||||
|
||||
packet := make([]byte, RequestHeaderSize+len(plaintext))
|
||||
packet[0] = mode
|
||||
copy(packet[1:17], sid[:])
|
||||
binary.BigEndian.PutUint64(packet[17:25], seq)
|
||||
binary.BigEndian.PutUint32(packet[25:29], uint32(len(plaintext)))
|
||||
copy(packet[29:], plaintext)
|
||||
MaskInPlace(packet[29:], sid, mode, seq, false)
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func ReadRequest(r io.Reader) (Request, error) {
|
||||
var req Request
|
||||
var header [RequestHeaderSize]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Mode = header[0]
|
||||
copy(req.Session[:], header[1:17])
|
||||
req.Seq = binary.BigEndian.Uint64(header[17:25])
|
||||
n := binary.BigEndian.Uint32(header[25:29])
|
||||
if n > MaxPayload {
|
||||
return req, errors.New("request payload too large")
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
req.Payload = make([]byte, int(n))
|
||||
if _, err := io.ReadFull(r, req.Payload); err != nil {
|
||||
return req, err
|
||||
}
|
||||
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func WriteResponse(w io.Writer, status byte, body []byte) error {
|
||||
if len(body) > MaxPayload {
|
||||
return fmt.Errorf("response body too large: %d", len(body))
|
||||
}
|
||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
||||
packet[0] = status
|
||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
||||
copy(packet[5:], body)
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64) error {
|
||||
if len(body) > MaxPayload {
|
||||
return fmt.Errorf("response body too large: %d", len(body))
|
||||
}
|
||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
||||
packet[0] = status
|
||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
||||
copy(packet[5:], body)
|
||||
MaskInPlace(packet[5:], sid, mode, seq, true)
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func ReadResponse(r io.Reader) (byte, []byte, error) {
|
||||
var header [ResponseHeaderSize]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
n := binary.BigEndian.Uint32(header[1:5])
|
||||
if n > MaxPayload {
|
||||
return 0, nil, errors.New("response body too large")
|
||||
}
|
||||
var body []byte
|
||||
if n > 0 {
|
||||
body = make([]byte, int(n))
|
||||
if _, err := io.ReadFull(r, body); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
return header[0], body, nil
|
||||
}
|
||||
|
||||
func DecodeMaskedResponse(status byte, body []byte, sid SessionID, mode byte, seq uint64) []byte {
|
||||
if len(body) == 0 || status == StatusError {
|
||||
return body
|
||||
}
|
||||
out := append([]byte(nil), body...)
|
||||
MaskInPlace(out, sid, mode, seq, true)
|
||||
return out
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n <= 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaskChangesWithSequenceAndRoundTrips(t *testing.T) {
|
||||
var sid SessionID
|
||||
for i := range sid { sid[i] = byte(i+1) }
|
||||
plain := bytes.Repeat([]byte("DragonTCP"), 100)
|
||||
a := append([]byte(nil), plain...)
|
||||
b := append([]byte(nil), plain...)
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
MaskInPlace(b, sid, ModeUpload, 2, false)
|
||||
if bytes.Equal(a, b) { t.Fatal("different sequences produced identical wire bytes") }
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
if !bytes.Equal(a, plain) { t.Fatal("mask did not round-trip") }
|
||||
}
|
||||
|
||||
func BenchmarkMask1MiB(b *testing.B) {
|
||||
var sid SessionID
|
||||
data := make([]byte, 1024*1024)
|
||||
b.SetBytes(int64(len(data)))
|
||||
b.ResetTimer()
|
||||
for i:=0;i<b.N;i++ {
|
||||
MaskInPlace(data,sid,ModeUpload,uint64(i),false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user