New
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Package cover implements the optional connection preface used by startup
|
||||
// profile discovery. Legacy connections have no preface and remain supported.
|
||||
package cover
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
PrefaceSize = 12
|
||||
MaxPadding = 4096
|
||||
)
|
||||
|
||||
// Profile is selected once during startup and then reused unchanged. Padding
|
||||
// bytes are freshly random on each physical connection, but their length and
|
||||
// all header fields remain fixed.
|
||||
type Profile struct {
|
||||
Enabled bool
|
||||
ID uint16
|
||||
Padding uint16
|
||||
HeaderMask byte
|
||||
XOR bool
|
||||
Clear bool
|
||||
}
|
||||
|
||||
func (p Profile) String() string {
|
||||
if !p.Enabled {
|
||||
return "direct"
|
||||
}
|
||||
encoding := "masked"
|
||||
if p.Clear {
|
||||
encoding = "clear"
|
||||
}
|
||||
return fmt.Sprintf("cover-%04x/pad-%d/%s", p.ID, p.Padding, encoding)
|
||||
}
|
||||
|
||||
func key(id uint16) [32]byte {
|
||||
var seed [16]byte
|
||||
copy(seed[:12], []byte("DragonTCP-C3"))
|
||||
binary.BigEndian.PutUint16(seed[12:14], id)
|
||||
seed[14], seed[15] = byte(id)^0x6d, byte(id>>8)^0xb2
|
||||
return sha256.Sum256(seed[:])
|
||||
}
|
||||
|
||||
// EncodePreface returns the fixed-size, self-describing portion. The first two
|
||||
// bytes are the mutable profile ID; all metadata after them is masked.
|
||||
func EncodePreface(p Profile) ([PrefaceSize]byte, error) {
|
||||
var out [PrefaceSize]byte
|
||||
if !p.Enabled {
|
||||
return out, fmt.Errorf("cover profile is disabled")
|
||||
}
|
||||
if p.Padding > MaxPadding {
|
||||
return out, fmt.Errorf("cover padding too large: %d", p.Padding)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(out[0:2], p.ID)
|
||||
var plain [10]byte
|
||||
copy(plain[0:4], []byte("DTC3"))
|
||||
if p.XOR {
|
||||
plain[4] |= 1
|
||||
}
|
||||
if p.Clear {
|
||||
plain[4] |= 2
|
||||
}
|
||||
plain[5] = p.HeaderMask
|
||||
binary.BigEndian.PutUint16(plain[6:8], p.Padding)
|
||||
plain[8] = plain[4] ^ plain[5] ^ 0xa5
|
||||
plain[9] = plain[6] ^ plain[7] ^ 0x5a
|
||||
k := key(p.ID)
|
||||
for i := range plain {
|
||||
out[2+i] = plain[i] ^ k[i]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DecodePreface recognizes an encoded cover profile. ok=false means the bytes
|
||||
// belong to a legacy/direct connection and must be replayed unchanged.
|
||||
func DecodePreface(in [PrefaceSize]byte) (p Profile, ok bool) {
|
||||
id := binary.BigEndian.Uint16(in[0:2])
|
||||
k := key(id)
|
||||
var plain [10]byte
|
||||
for i := range plain {
|
||||
plain[i] = in[2+i] ^ k[i]
|
||||
}
|
||||
if string(plain[0:4]) != "DTC3" || plain[4]&^byte(3) != 0 {
|
||||
return Profile{}, false
|
||||
}
|
||||
if plain[8] != plain[4]^plain[5]^0xa5 || plain[9] != plain[6]^plain[7]^0x5a {
|
||||
return Profile{}, false
|
||||
}
|
||||
padding := binary.BigEndian.Uint16(plain[6:8])
|
||||
if padding > MaxPadding {
|
||||
return Profile{}, false
|
||||
}
|
||||
return Profile{
|
||||
Enabled: true,
|
||||
ID: id,
|
||||
Padding: padding,
|
||||
HeaderMask: plain[5],
|
||||
XOR: plain[4]&1 != 0,
|
||||
Clear: plain[4]&2 != 0,
|
||||
}, true
|
||||
}
|
||||
|
||||
// WritePreface sends the encoded profile followed by its fixed amount of
|
||||
// random padding.
|
||||
func WritePreface(w io.Writer, p Profile) error {
|
||||
if !p.Enabled {
|
||||
return nil
|
||||
}
|
||||
preface, err := EncodePreface(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
packet := make([]byte, PrefaceSize+int(p.Padding))
|
||||
copy(packet, preface[:])
|
||||
if p.Padding > 0 {
|
||||
if _, err := rand.Read(packet[PrefaceSize:]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n <= 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfileRoundTripAcrossRange(t *testing.T) {
|
||||
for id := 0; id < 65536; id += 257 {
|
||||
for _, xor := range []bool{false, true} {
|
||||
for _, clear := range []bool{false, true} {
|
||||
want := Profile{Enabled: true, ID: uint16(id), Padding: uint16(id % (MaxPadding + 1)), HeaderMask: byte(id), XOR: xor, Clear: clear}
|
||||
encoded, err := EncodePreface(want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := DecodePreface(encoded)
|
||||
if !ok || got != want {
|
||||
t.Fatalf("id=%04x xor=%t clear=%t got=%+v ok=%t", id, xor, clear, got, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePrefaceIncludesFixedPadding(t *testing.T) {
|
||||
p := Profile{Enabled: true, ID: 0x1234, Padding: 64, HeaderMask: 0x9a, XOR: true}
|
||||
var out bytes.Buffer
|
||||
if err := WritePreface(&out, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.Len() != PrefaceSize+64 {
|
||||
t.Fatalf("length=%d", out.Len())
|
||||
}
|
||||
var encoded [PrefaceSize]byte
|
||||
copy(encoded[:], out.Bytes())
|
||||
got, ok := DecodePreface(encoded)
|
||||
if !ok || got != p {
|
||||
t.Fatalf("got=%+v ok=%t", got, ok)
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,17 @@ var BufferPool = sync.Pool{
|
||||
}
|
||||
|
||||
func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
|
||||
return ReadRequestFrameProfile(r, 0)
|
||||
}
|
||||
|
||||
// ReadRequestFrameProfile decodes the UP magic after applying headerMask.
|
||||
func ReadRequestFrameProfile(r io.Reader, headerMask byte) (uint32, uint32, []byte, error) {
|
||||
var header [14]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if header[0] != 'U' || header[1] != 'P' {
|
||||
if header[0]^headerMask != 'U' || header[1]^headerMask != 'P' {
|
||||
return 0, 0, nil, errors.New("bad request magic")
|
||||
}
|
||||
|
||||
@@ -58,11 +63,16 @@ func ReadRequestFrame(r io.Reader) (uint32, uint32, []byte, error) {
|
||||
}
|
||||
|
||||
func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
|
||||
return WriteRequestFrameProfile(w, requestID, payload, 0)
|
||||
}
|
||||
|
||||
// WriteRequestFrameProfile masks the two-byte UP magic with headerMask.
|
||||
func WriteRequestFrameProfile(w io.Writer, requestID uint32, payload []byte, headerMask byte) error {
|
||||
if len(payload) > MaxHandshake {
|
||||
return errors.New("request frame payload too large")
|
||||
}
|
||||
packet := make([]byte, 14+len(payload))
|
||||
packet[0], packet[1] = 'U', 'P'
|
||||
packet[0], packet[1] = 'U'^headerMask, 'P'^headerMask
|
||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
||||
binary.BigEndian.PutUint32(packet[6:10], 0)
|
||||
binary.BigEndian.PutUint32(packet[10:14], uint32(len(payload)))
|
||||
@@ -72,12 +82,17 @@ func WriteRequestFrame(w io.Writer, requestID uint32, payload []byte) error {
|
||||
}
|
||||
|
||||
func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
|
||||
return ReadResponseFrameProfile(r, 0)
|
||||
}
|
||||
|
||||
// ReadResponseFrameProfile decodes the OK magic after applying headerMask.
|
||||
func ReadResponseFrameProfile(r io.Reader, headerMask byte) (uint32, []byte, error) {
|
||||
var header [10]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if header[0] != 'O' || header[1] != 'K' {
|
||||
if header[0]^headerMask != 'O' || header[1]^headerMask != 'K' {
|
||||
return 0, nil, fmt.Errorf("bad response magic: %q", header[:2])
|
||||
}
|
||||
|
||||
@@ -98,11 +113,16 @@ func ReadResponseFrame(r io.Reader) (uint32, []byte, error) {
|
||||
}
|
||||
|
||||
func WriteResponseFrame(w io.Writer, requestID uint32, payload []byte) error {
|
||||
return WriteResponseFrameProfile(w, requestID, payload, writerHeaderMask(w))
|
||||
}
|
||||
|
||||
// WriteResponseFrameProfile masks the two-byte OK magic with headerMask.
|
||||
func WriteResponseFrameProfile(w io.Writer, requestID uint32, payload []byte, headerMask byte) error {
|
||||
if len(payload) > MaxHandshake {
|
||||
return errors.New("response frame payload too large")
|
||||
}
|
||||
packet := make([]byte, 10+len(payload))
|
||||
packet[0], packet[1] = 'O', 'K'
|
||||
packet[0], packet[1] = 'O'^headerMask, 'K'^headerMask
|
||||
binary.BigEndian.PutUint32(packet[2:6], requestID)
|
||||
binary.BigEndian.PutUint32(packet[6:10], uint32(len(payload)))
|
||||
copy(packet[10:], payload)
|
||||
@@ -121,6 +141,13 @@ func writeAll(w io.Writer, b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func writerHeaderMask(w io.Writer) byte {
|
||||
if profiled, ok := w.(interface{ HeaderMask() byte }); ok {
|
||||
return profiled.HeaderMask()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func CopyXOR(dst net.Conn, src net.Conn) error {
|
||||
ptr := BufferPool.Get().(*[]byte)
|
||||
buf := *ptr
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXORHeaderProfilesRoundTrip(t *testing.T) {
|
||||
for n := 0; n < 256; n++ {
|
||||
mask := byte(n)
|
||||
if ('U'^mask)&7 < 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
var request bytes.Buffer
|
||||
if err := WriteRequestFrameProfile(&request, 7, []byte("CPROBE -"), mask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requestID, _, payload, err := ReadRequestFrameProfile(&request, mask)
|
||||
if err != nil || requestID != 7 || !bytes.Equal(payload, []byte("CPROBE -")) {
|
||||
t.Fatalf("mask %02x request did not round-trip: id=%d payload=%q err=%v", mask, requestID, payload, err)
|
||||
}
|
||||
|
||||
var response bytes.Buffer
|
||||
if err := WriteResponseFrameProfile(&response, 7, []byte("PROBEOK"), mask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
responseID, payload, err := ReadResponseFrameProfile(&response, mask)
|
||||
if err != nil || responseID != 7 || !bytes.Equal(payload, []byte("PROBEOK")) {
|
||||
t.Fatalf("mask %02x response did not round-trip: id=%d payload=%q err=%v", mask, responseID, payload, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type profiledBuffer struct {
|
||||
bytes.Buffer
|
||||
mask byte
|
||||
}
|
||||
|
||||
func (b *profiledBuffer) HeaderMask() byte { return b.mask }
|
||||
|
||||
func TestServerResponseUsesConnectionProfile(t *testing.T) {
|
||||
profiled := &profiledBuffer{mask: 0x3a}
|
||||
if err := WriteResponseFrame(profiled, 9, []byte("ok")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := profiled.Bytes()[0]; got != 'O'^profiled.mask {
|
||||
t.Fatalf("first byte=%02x, want %02x", got, byte('O')^profiled.mask)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -72,12 +73,36 @@ func MaskInPlace(data []byte, sid SessionID, mode byte, seq uint64, response boo
|
||||
}
|
||||
|
||||
func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte) error {
|
||||
return WriteRequestProfile(w, mode, sid, seq, plaintext, 0)
|
||||
}
|
||||
|
||||
// WriteRequestProfile writes a binary request whose first byte is XORed with
|
||||
// headerMask. The remaining framing and payload encoding stay unchanged.
|
||||
// Masks are selected once at client startup and then remain fixed.
|
||||
func WriteRequestProfile(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte, headerMask byte) error {
|
||||
return WriteRequestProfileEncoding(w, mode, sid, seq, plaintext, headerMask, false)
|
||||
}
|
||||
|
||||
// WriteRequestProfileEncoding optionally leaves the payload clear. Clear mode
|
||||
// is signalled by the connection cover preface, so legacy peers continue to use
|
||||
// the SHA-256 compatibility mask unchanged.
|
||||
func WriteRequestProfileEncoding(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte, headerMask byte, clear bool) error {
|
||||
if len(plaintext) > MaxPayload {
|
||||
return fmt.Errorf("request payload too large: %d", len(plaintext))
|
||||
}
|
||||
if clear {
|
||||
var header [RequestHeaderSize]byte
|
||||
header[0] = mode ^ headerMask
|
||||
copy(header[1:17], sid[:])
|
||||
binary.BigEndian.PutUint64(header[17:25], seq)
|
||||
binary.BigEndian.PutUint32(header[25:29], uint32(len(plaintext)))
|
||||
buffers := net.Buffers{header[:], plaintext}
|
||||
_, err := buffers.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
packet := make([]byte, RequestHeaderSize+len(plaintext))
|
||||
packet[0] = mode
|
||||
packet[0] = mode ^ headerMask
|
||||
copy(packet[1:17], sid[:])
|
||||
binary.BigEndian.PutUint64(packet[17:25], seq)
|
||||
binary.BigEndian.PutUint32(packet[25:29], uint32(len(plaintext)))
|
||||
@@ -87,13 +112,25 @@ func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext [
|
||||
}
|
||||
|
||||
func ReadRequest(r io.Reader) (Request, error) {
|
||||
return ReadRequestProfile(r, 0)
|
||||
}
|
||||
|
||||
// ReadRequestProfile decodes a request written with WriteRequestProfile.
|
||||
func ReadRequestProfile(r io.Reader, headerMask byte) (Request, error) {
|
||||
return ReadRequestProfileEncoding(r, headerMask, false)
|
||||
}
|
||||
|
||||
func ReadRequestProfileEncoding(r io.Reader, headerMask byte, clear bool) (Request, error) {
|
||||
var req Request
|
||||
var header [RequestHeaderSize]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Mode = header[0]
|
||||
req.Mode = header[0] ^ headerMask
|
||||
if req.Mode > ModeClose {
|
||||
return req, errors.New("unknown request mode")
|
||||
}
|
||||
copy(req.Session[:], header[1:17])
|
||||
req.Seq = binary.BigEndian.Uint64(header[17:25])
|
||||
n := binary.BigEndian.Uint32(header[25:29])
|
||||
@@ -106,28 +143,53 @@ func ReadRequest(r io.Reader) (Request, error) {
|
||||
if _, err := io.ReadFull(r, req.Payload); err != nil {
|
||||
return req, err
|
||||
}
|
||||
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
|
||||
if !clear {
|
||||
MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func WriteResponse(w io.Writer, status byte, body []byte) error {
|
||||
return WriteResponseProfile(w, status, body, writerHeaderMask(w))
|
||||
}
|
||||
|
||||
// WriteResponseProfile writes a response using the selected first-byte mask.
|
||||
func WriteResponseProfile(w io.Writer, status byte, body []byte, headerMask byte) error {
|
||||
if len(body) > MaxPayload {
|
||||
return fmt.Errorf("response body too large: %d", len(body))
|
||||
}
|
||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
||||
packet[0] = status
|
||||
packet[0] = status ^ headerMask
|
||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
||||
copy(packet[5:], body)
|
||||
return writeAll(w, packet)
|
||||
}
|
||||
|
||||
func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64) error {
|
||||
return WriteMaskedResponseProfileEncoding(w, status, body, sid, mode, seq, writerHeaderMask(w), writerClearPayload(w))
|
||||
}
|
||||
|
||||
// WriteMaskedResponseProfile combines the normal payload mask with the
|
||||
// selected first-byte header mask.
|
||||
func WriteMaskedResponseProfile(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64, headerMask byte) error {
|
||||
return WriteMaskedResponseProfileEncoding(w, status, body, sid, mode, seq, headerMask, false)
|
||||
}
|
||||
|
||||
func WriteMaskedResponseProfileEncoding(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64, headerMask byte, clear bool) error {
|
||||
if len(body) > MaxPayload {
|
||||
return fmt.Errorf("response body too large: %d", len(body))
|
||||
}
|
||||
if clear {
|
||||
var header [ResponseHeaderSize]byte
|
||||
header[0] = status ^ headerMask
|
||||
binary.BigEndian.PutUint32(header[1:5], uint32(len(body)))
|
||||
buffers := net.Buffers{header[:], body}
|
||||
_, err := buffers.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
packet := make([]byte, ResponseHeaderSize+len(body))
|
||||
packet[0] = status
|
||||
packet[0] = status ^ headerMask
|
||||
binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
|
||||
copy(packet[5:], body)
|
||||
MaskInPlace(packet[5:], sid, mode, seq, true)
|
||||
@@ -135,6 +197,11 @@ func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, m
|
||||
}
|
||||
|
||||
func ReadResponse(r io.Reader) (byte, []byte, error) {
|
||||
return ReadResponseProfile(r, 0)
|
||||
}
|
||||
|
||||
// ReadResponseProfile decodes a response written with a header profile.
|
||||
func ReadResponseProfile(r io.Reader, headerMask byte) (byte, []byte, error) {
|
||||
var header [ResponseHeaderSize]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return 0, nil, err
|
||||
@@ -150,16 +217,19 @@ func ReadResponse(r io.Reader) (byte, []byte, error) {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
return header[0], body, nil
|
||||
status := header[0] ^ headerMask
|
||||
if status > StatusEOF {
|
||||
return 0, nil, errors.New("unknown response status")
|
||||
}
|
||||
return status, body, nil
|
||||
}
|
||||
|
||||
func DecodeMaskedResponse(status byte, body []byte, sid SessionID, mode byte, seq uint64) []byte {
|
||||
if len(body) == 0 || status == StatusError {
|
||||
return body
|
||||
}
|
||||
out := append([]byte(nil), body...)
|
||||
MaskInPlace(out, sid, mode, seq, true)
|
||||
return out
|
||||
MaskInPlace(body, sid, mode, seq, true)
|
||||
return body
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, b []byte) error {
|
||||
@@ -175,3 +245,17 @@ func writeAll(w io.Writer, b []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writerHeaderMask(w io.Writer) byte {
|
||||
if profiled, ok := w.(interface{ HeaderMask() byte }); ok {
|
||||
return profiled.HeaderMask()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func writerClearPayload(w io.Writer) bool {
|
||||
if profiled, ok := w.(interface{ ClearPayload() bool }); ok {
|
||||
return profiled.ClearPayload()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,29 +1,105 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaskChangesWithSequenceAndRoundTrips(t *testing.T) {
|
||||
var sid SessionID
|
||||
for i := range sid { sid[i] = byte(i+1) }
|
||||
plain := bytes.Repeat([]byte("DragonTCP"), 100)
|
||||
a := append([]byte(nil), plain...)
|
||||
b := append([]byte(nil), plain...)
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
MaskInPlace(b, sid, ModeUpload, 2, false)
|
||||
if bytes.Equal(a, b) { t.Fatal("different sequences produced identical wire bytes") }
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
if !bytes.Equal(a, plain) { t.Fatal("mask did not round-trip") }
|
||||
var sid SessionID
|
||||
for i := range sid {
|
||||
sid[i] = byte(i + 1)
|
||||
}
|
||||
plain := bytes.Repeat([]byte("DragonTCP"), 100)
|
||||
a := append([]byte(nil), plain...)
|
||||
b := append([]byte(nil), plain...)
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
MaskInPlace(b, sid, ModeUpload, 2, false)
|
||||
if bytes.Equal(a, b) {
|
||||
t.Fatal("different sequences produced identical wire bytes")
|
||||
}
|
||||
MaskInPlace(a, sid, ModeUpload, 1, false)
|
||||
if !bytes.Equal(a, plain) {
|
||||
t.Fatal("mask did not round-trip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinaryHeaderProfilesRoundTrip(t *testing.T) {
|
||||
var sid SessionID
|
||||
for i := range sid {
|
||||
sid[i] = byte(i + 1)
|
||||
}
|
||||
for n := 0; n < 256; n += 8 {
|
||||
mask := byte(n)
|
||||
var request bytes.Buffer
|
||||
if err := WriteRequestProfile(&request, ModeUpload, sid, 42, []byte("payload"), mask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := request.Bytes()[0]; got != ModeUpload^mask {
|
||||
t.Fatalf("mask %02x first byte=%02x", mask, got)
|
||||
}
|
||||
req, err := ReadRequestProfile(&request, mask)
|
||||
if err != nil {
|
||||
t.Fatalf("mask %02x: %v", mask, err)
|
||||
}
|
||||
if req.Mode != ModeUpload || req.Seq != 42 || !bytes.Equal(req.Payload, []byte("payload")) {
|
||||
t.Fatalf("mask %02x request did not round-trip", mask)
|
||||
}
|
||||
|
||||
var response bytes.Buffer
|
||||
if err := WriteResponseProfile(&response, StatusOK, []byte("ok"), mask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, body, err := ReadResponseProfile(&response, mask)
|
||||
if err != nil || status != StatusOK || !bytes.Equal(body, []byte("ok")) {
|
||||
t.Fatalf("mask %02x response did not round-trip: status=%d body=%q err=%v", mask, status, body, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type profiledBuffer struct {
|
||||
bytes.Buffer
|
||||
mask byte
|
||||
}
|
||||
|
||||
func (b *profiledBuffer) HeaderMask() byte { return b.mask }
|
||||
|
||||
func TestServerResponseUsesConnectionProfile(t *testing.T) {
|
||||
profiled := &profiledBuffer{mask: 0xa0}
|
||||
if err := WriteResponse(profiled, StatusOK, []byte("ok")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := profiled.Bytes()[0]; got != StatusOK^profiled.mask {
|
||||
t.Fatalf("first byte=%02x, want %02x", got, StatusOK^profiled.mask)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMask1MiB(b *testing.B) {
|
||||
var sid SessionID
|
||||
data := make([]byte, 1024*1024)
|
||||
b.SetBytes(int64(len(data)))
|
||||
b.ResetTimer()
|
||||
for i:=0;i<b.N;i++ {
|
||||
MaskInPlace(data,sid,ModeUpload,uint64(i),false)
|
||||
}
|
||||
var sid SessionID
|
||||
data := make([]byte, 1024*1024)
|
||||
b.SetBytes(int64(len(data)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MaskInPlace(data, sid, ModeUpload, uint64(i), false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteRequest1MiB(b *testing.B) {
|
||||
var sid SessionID
|
||||
data := make([]byte, 1024*1024)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
clear bool
|
||||
}{{"sha256-compat", false}, {"clear", true}} {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
b.SetBytes(int64(len(data)))
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := WriteRequestProfileEncoding(io.Discard, ModeUpload, sid, uint64(i), data, 0, tc.clear); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,24 @@ type Options struct {
|
||||
pollDelay time.Duration
|
||||
txnTimeout time.Duration
|
||||
tcpBuffer int
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
}
|
||||
|
||||
// WithHeaderMask returns a copy using one fixed frame-magic profile. The mask
|
||||
// is selected during startup discovery and remains unchanged for normal data.
|
||||
func (o Options) WithHeaderMask(mask byte) Options {
|
||||
o.headerMask = mask
|
||||
o.coverProfile = cover.Profile{}
|
||||
return o
|
||||
}
|
||||
|
||||
// WithCoverProfile returns a copy using a fixed startup-selected preface,
|
||||
// padding length, and frame mask.
|
||||
func (o Options) WithCoverProfile(profile cover.Profile) Options {
|
||||
o.coverProfile = profile
|
||||
o.headerMask = profile.HeaderMask
|
||||
return o
|
||||
}
|
||||
|
||||
func wireToken(token string) string {
|
||||
@@ -219,17 +238,21 @@ type txnLane struct {
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
conn net.Conn
|
||||
count int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *txnLane {
|
||||
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *txnLane {
|
||||
return &txnLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
headerMask: headerMask,
|
||||
coverProfile: coverProfile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +285,10 @@ func (l *txnLane) ensureConn() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
||||
l.conn = conn
|
||||
@@ -285,12 +312,12 @@ func (l *txnLane) Do(payload []byte) ([]byte, error) {
|
||||
_ = l.conn.SetDeadline(time.Now().Add(timeout))
|
||||
requestID := requestCounter.Add(1)
|
||||
|
||||
if err := protocol.WriteRequestFrame(l.conn, requestID, payload); err != nil {
|
||||
if err := protocol.WriteRequestFrameProfile(l.conn, requestID, payload, l.headerMask); err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseID, response, err := protocol.ReadResponseFrame(l.conn)
|
||||
responseID, response, err := protocol.ReadResponseFrameProfile(l.conn, l.headerMask)
|
||||
if err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
@@ -324,6 +351,28 @@ func doControl(lane *txnLane, payload []byte) ([]byte, error) {
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// ProbeProfile performs one small authenticated transaction using the selected
|
||||
// frame-magic mask. It does not create a target session.
|
||||
func ProbeProfile(serverAddr, token string, opts Options) bool {
|
||||
timeout := opts.txnTimeout
|
||||
if timeout <= 0 || timeout > 2*time.Second {
|
||||
timeout = 2 * time.Second
|
||||
}
|
||||
lane := newTxnLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
|
||||
defer lane.Close()
|
||||
resp, err := lane.Do([]byte("CPROBE " + wireToken(token)))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if string(resp) == "PROBEOK" {
|
||||
return true
|
||||
}
|
||||
// Servers predating profile discovery do not know CPROBE, but receiving a
|
||||
// correctly framed error still proves that the legacy mask-zero header
|
||||
// survived. The subsequent end-to-end probe remains authoritative.
|
||||
return opts.headerMask == 0 && strings.HasPrefix(string(resp), "ERR expected TUNNEL")
|
||||
}
|
||||
|
||||
type chunkResult struct {
|
||||
seq uint64
|
||||
data []byte
|
||||
@@ -425,7 +474,7 @@ func Open(serverAddr, token, targetHost string, targetPort int, opts Options) (n
|
||||
c.upSizer = newAdaptiveSizer("upload", opts)
|
||||
c.downSizer = newAdaptiveSizer("download", opts)
|
||||
|
||||
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout, opts.headerMask, opts.coverProfile)
|
||||
|
||||
openPayload := []byte(fmt.Sprintf(
|
||||
"COPEN %s %s %s %d",
|
||||
@@ -464,7 +513,7 @@ func Open(serverAddr, token, targetHost string, targetPort int, opts Options) (n
|
||||
|
||||
c.pullLanes = make([]*txnLane, opts.pollers)
|
||||
for i := 0; i < opts.pollers; i++ {
|
||||
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout, opts.headerMask, opts.coverProfile)
|
||||
c.pullLanes[i] = lane
|
||||
c.workers.Add(1)
|
||||
go c.pullWorker(lane)
|
||||
@@ -762,7 +811,7 @@ func (c *chunkConn) Close() error {
|
||||
c.once.Do(func() {
|
||||
c.cancel()
|
||||
|
||||
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
||||
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout, c.opts.headerMask, c.opts.coverProfile)
|
||||
_, _ = doControl(lane, []byte(fmt.Sprintf("CCLOSE %s %s", wireToken(c.token), c.sid)))
|
||||
lane.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user