mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 06:34:36 +03:00
248 lines
6.1 KiB
Go
248 lines
6.1 KiB
Go
package protocol
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/big"
|
|
)
|
|
|
|
const (
|
|
// MinBlockPayload is the minimum decrypted payload per DNS TXT block.
|
|
MinBlockPayload = 200
|
|
// MaxBlockPayload is the maximum decrypted payload per DNS TXT block.
|
|
// 600 bytes data + 28 GCM overhead + 2 prefix + 32 padding → ~856 base64 chars.
|
|
// Well within the 4096-byte EDNS0 UDP buffer the client advertises.
|
|
MaxBlockPayload = 600
|
|
// DefaultBlockPayload is kept for compatibility; equals MaxBlockPayload.
|
|
DefaultBlockPayload = MaxBlockPayload
|
|
|
|
// DefaultMaxPadding is the default random padding added to responses to vary DNS response size.
|
|
DefaultMaxPadding = 32
|
|
|
|
// PadLengthSize is the 2-byte length prefix added before real data when padding is used.
|
|
PadLengthSize = 2
|
|
|
|
// MetadataChannel is the special channel number for server metadata.
|
|
MetadataChannel = 0
|
|
|
|
// MarkerSize is the random marker in metadata to verify data freshness.
|
|
MarkerSize = 3
|
|
|
|
// Query payload structure sizes.
|
|
QueryPaddingSize = 4
|
|
QueryChannelSize = 2
|
|
QueryBlockSize = 2
|
|
QueryPayloadSize = QueryPaddingSize + QueryChannelSize + QueryBlockSize // 8
|
|
|
|
// Message header sizes (in the serialized message stream).
|
|
MsgIDSize = 4
|
|
MsgTimestampSize = 4
|
|
MsgLengthSize = 2
|
|
MsgHeaderSize = MsgIDSize + MsgTimestampSize + MsgLengthSize // 10
|
|
)
|
|
|
|
// Media placeholder strings for non-text content.
|
|
const (
|
|
MediaImage = "[IMAGE]"
|
|
MediaVideo = "[VIDEO]"
|
|
MediaFile = "[FILE]"
|
|
MediaAudio = "[AUDIO]"
|
|
MediaSticker = "[STICKER]"
|
|
MediaGIF = "[GIF]"
|
|
MediaPoll = "[POLL]"
|
|
MediaContact = "[CONTACT]"
|
|
MediaLocation = "[LOCATION]"
|
|
)
|
|
|
|
// Metadata holds channel 0 data: server info + channel list.
|
|
type Metadata struct {
|
|
Marker [MarkerSize]byte
|
|
Timestamp uint32
|
|
Channels []ChannelInfo
|
|
}
|
|
|
|
// ChannelInfo describes a single feed channel.
|
|
type ChannelInfo struct {
|
|
Name string
|
|
Blocks uint16
|
|
LastMsgID uint32
|
|
}
|
|
|
|
// Message represents a single feed message in a channel.
|
|
type Message struct {
|
|
ID uint32
|
|
Timestamp uint32
|
|
Text string
|
|
}
|
|
|
|
// SerializeMetadata encodes metadata into bytes for channel 0 blocks.
|
|
func SerializeMetadata(m *Metadata) []byte {
|
|
// 3 marker + 4 timestamp + 2 channel count + per-channel data
|
|
size := MarkerSize + 4 + 2
|
|
for _, ch := range m.Channels {
|
|
size += 1 + len(ch.Name) + 2 + 4
|
|
}
|
|
buf := make([]byte, size)
|
|
off := 0
|
|
|
|
copy(buf[off:], m.Marker[:])
|
|
off += MarkerSize
|
|
|
|
binary.BigEndian.PutUint32(buf[off:], m.Timestamp)
|
|
off += 4
|
|
|
|
binary.BigEndian.PutUint16(buf[off:], uint16(len(m.Channels)))
|
|
off += 2
|
|
|
|
for _, ch := range m.Channels {
|
|
nameBytes := []byte(ch.Name)
|
|
if len(nameBytes) > 255 {
|
|
nameBytes = nameBytes[:255]
|
|
}
|
|
buf[off] = byte(len(nameBytes))
|
|
off++
|
|
copy(buf[off:], nameBytes)
|
|
off += len(nameBytes)
|
|
binary.BigEndian.PutUint16(buf[off:], ch.Blocks)
|
|
off += 2
|
|
binary.BigEndian.PutUint32(buf[off:], ch.LastMsgID)
|
|
off += 4
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// ParseMetadata decodes metadata from concatenated channel 0 block data.
|
|
func ParseMetadata(data []byte) (*Metadata, error) {
|
|
if len(data) < MarkerSize+4+2 {
|
|
return nil, fmt.Errorf("metadata too short: %d bytes", len(data))
|
|
}
|
|
m := &Metadata{}
|
|
off := 0
|
|
|
|
copy(m.Marker[:], data[off:off+MarkerSize])
|
|
off += MarkerSize
|
|
|
|
m.Timestamp = binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
|
|
count := binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
|
|
m.Channels = make([]ChannelInfo, 0, count)
|
|
for i := 0; i < int(count); i++ {
|
|
if off >= len(data) {
|
|
return nil, fmt.Errorf("truncated metadata at channel %d", i)
|
|
}
|
|
nameLen := int(data[off])
|
|
off++
|
|
if off+nameLen > len(data) {
|
|
return nil, fmt.Errorf("truncated channel name at %d", i)
|
|
}
|
|
name := string(data[off : off+nameLen])
|
|
off += nameLen
|
|
|
|
if off+6 > len(data) {
|
|
return nil, fmt.Errorf("truncated channel info at %d", i)
|
|
}
|
|
blocks := binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
lastID := binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
|
|
m.Channels = append(m.Channels, ChannelInfo{
|
|
Name: name,
|
|
Blocks: blocks,
|
|
LastMsgID: lastID,
|
|
})
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// SerializeMessages encodes messages into a byte stream for data channel blocks.
|
|
func SerializeMessages(msgs []Message) []byte {
|
|
size := 0
|
|
for _, msg := range msgs {
|
|
size += MsgHeaderSize + len(msg.Text)
|
|
}
|
|
buf := make([]byte, size)
|
|
off := 0
|
|
|
|
for _, msg := range msgs {
|
|
textBytes := []byte(msg.Text)
|
|
binary.BigEndian.PutUint32(buf[off:], msg.ID)
|
|
off += MsgIDSize
|
|
binary.BigEndian.PutUint32(buf[off:], msg.Timestamp)
|
|
off += MsgTimestampSize
|
|
binary.BigEndian.PutUint16(buf[off:], uint16(len(textBytes)))
|
|
off += MsgLengthSize
|
|
copy(buf[off:], textBytes)
|
|
off += len(textBytes)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// ParseMessages decodes messages from concatenated data channel block data.
|
|
func ParseMessages(data []byte) ([]Message, error) {
|
|
var msgs []Message
|
|
off := 0
|
|
|
|
for off < len(data) {
|
|
if off+MsgHeaderSize > len(data) {
|
|
break // incomplete message header, stop
|
|
}
|
|
id := binary.BigEndian.Uint32(data[off:])
|
|
off += MsgIDSize
|
|
ts := binary.BigEndian.Uint32(data[off:])
|
|
off += MsgTimestampSize
|
|
textLen := int(binary.BigEndian.Uint16(data[off:]))
|
|
off += MsgLengthSize
|
|
|
|
if off+textLen > len(data) {
|
|
break // incomplete message text, stop
|
|
}
|
|
text := string(data[off : off+textLen])
|
|
off += textLen
|
|
|
|
msgs = append(msgs, Message{
|
|
ID: id,
|
|
Timestamp: ts,
|
|
Text: text,
|
|
})
|
|
}
|
|
|
|
return msgs, nil
|
|
}
|
|
|
|
// SplitIntoBlocks splits data into blocks of randomly varying size in [MinBlockPayload, MaxBlockPayload].
|
|
// Random sizes make traffic analysis harder; the client just concatenates all blocks to reassemble.
|
|
func SplitIntoBlocks(data []byte) [][]byte {
|
|
if len(data) == 0 {
|
|
return [][]byte{{}} // channel 0 block 0 must always exist
|
|
}
|
|
var blocks [][]byte
|
|
rem := data
|
|
for len(rem) > 0 {
|
|
size := randBlockSize()
|
|
if size > len(rem) {
|
|
size = len(rem)
|
|
}
|
|
block := make([]byte, size)
|
|
copy(block, rem[:size])
|
|
blocks = append(blocks, block)
|
|
rem = rem[size:]
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
func randBlockSize() int {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(MaxBlockPayload-MinBlockPayload+1)))
|
|
if err != nil {
|
|
return (MinBlockPayload + MaxBlockPayload) / 2
|
|
}
|
|
return MinBlockPayload + int(n.Int64())
|
|
}
|