142 lines
3.3 KiB
Go
142 lines
3.3 KiB
Go
// 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
|
|
}
|