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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user