Files
2026-08-16 19:02:48 -03:00

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
}