feat(singbox-aura,tools): Go port of Aura UDP client + KAT bridge to Rust
Lays the foundation for sing-box mobile clients (Option B from
docs/sing-box.md): an independent Go module that speaks the AuraVPN wire
protocol byte-for-byte. Proof of equivalence is in KAT tests cross-loaded
from a Rust-side deterministic vector exporter.
- tools/export-kat (new Rust bin in workspace): captures a handshake +
derived keys + a sealed datagram record + a knock token using seeded
RNGs (rand::rngs::StdRng + ml-kem's *_deterministic public API), emits
JSON. Reproducible byte-for-byte.
- singbox-aura/ (new Go module, ~3000 LOC, 22 files):
- aura/frame: 5-byte protocol header + Frame{Data,Ping,Pong,Close,
Control} + magic envelope (0xAA,0xAA,0xC0,0x01) — encode/decode
matching aura-proto::frame.
- aura/crypto: hybrid X25519 + ML-KEM-768 (stdlib crypto/ecdh +
crypto/mlkem on Go 1.24+; falls back to circl on older Go via a
documented swap), HKDF-SHA256 derive_session_keys, ChaCha20-Poly1305
with the **LE(u64 counter) || [0;4]** nonce scheme that matches
aura-crypto::AeadKey/AeadSession.
- aura/handshake: client_handshake state machine reproducing protocol.md
§6.2 exactly (CH→SH→ServerAuth→ClientAuth→Finished×2; transcript hash;
ECDSA-P256 transcript signature; HMAC-SHA256 Finished).
- aura/session: DatagramSender/Receiver + 64-wide sliding replay window.
- aura/transport: reliable HS-adapter (DTLS-flight retransmit) + UDP
datagram data path + 16-byte HMAC port-knock with ±1-minute window.
- aura/outbound: sing-box-shaped shim (interface signatures only — sing-
box upstream registration is one more step, documented in README).
- cmd/aura-client: standalone Go binary; reads client.toml via
pelletier/go-toml/v2 and connects to a real aura server. Validates
end-to-end interop with the Rust side.
- KAT: 6 comparisons against Rust vectors — session_keys (HKDF), hybrid
KEM ek/encaps roundtrip, c2s + s2c Finished HMAC, sealed datagram
record at seq=2 (incl. 16-byte Poly1305 tag), knock token. All byte-
for-byte.
Go: 29 tests across 5 packages, all green. Only deps: golang.org/x/crypto
and pelletier/go-toml/v2. Rust: 293 tests still green; tools/export-kat
added to workspace members.
v1 limits documented in singbox-aura/README.md: UDP-only (no TCP/QUIC
fallback yet), no cell padding / cover traffic, no relay/exit role, no
multi-hop, sing-box upstream-registration sketch (vendor sagernet/sing-box +
init() RegisterOutbound) for follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package frame
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ControlEnvelopeMagic is the 4-byte prefix marking a v2 control message multiplexed through the
|
||||
// PacketConnection's send_packet path. An IPv4 packet's first byte is 0x4X and an IPv6 packet's
|
||||
// first byte is 0x6X, so this magic (starting with 0xAA) never collides with a real IP packet.
|
||||
var ControlEnvelopeMagic = [4]byte{0xAA, 0xAA, 0xC0, 0x01}
|
||||
|
||||
// ControlKind is the on-wire byte selector inside a control envelope.
|
||||
type ControlKind byte
|
||||
|
||||
// Known control kinds (must match crates/aura-proto/src/frame.rs ControlKind).
|
||||
const (
|
||||
ControlCrlPush ControlKind = 0x01
|
||||
ControlCrlAck ControlKind = 0x02
|
||||
ControlExtendBridge ControlKind = 0x03
|
||||
ControlCircuitReady ControlKind = 0x04
|
||||
ControlCircuitFailed ControlKind = 0x05
|
||||
)
|
||||
|
||||
// EncodeControlEnvelope wraps (kind, payload) as
|
||||
//
|
||||
// MAGIC(4) || kind(u8) || u32_be(payload_len) || payload
|
||||
//
|
||||
// suitable for shipping through PacketConnection.SendPacket.
|
||||
func EncodeControlEnvelope(kind ControlKind, payload []byte) []byte {
|
||||
out := make([]byte, 0, len(ControlEnvelopeMagic)+1+4+len(payload))
|
||||
out = append(out, ControlEnvelopeMagic[:]...)
|
||||
out = append(out, byte(kind))
|
||||
var lb [4]byte
|
||||
binary.BigEndian.PutUint32(lb[:], uint32(len(payload)))
|
||||
out = append(out, lb[:]...)
|
||||
out = append(out, payload...)
|
||||
return out
|
||||
}
|
||||
|
||||
// DecodeControlEnvelope returns (kind, payload, true, nil) if buf starts with the magic and
|
||||
// parses cleanly. If buf does NOT start with the magic (i.e. it is a normal IP packet) the third
|
||||
// return is false and the error is nil. A malformed envelope (truncated) returns an error.
|
||||
func DecodeControlEnvelope(buf []byte) (ControlKind, []byte, bool, error) {
|
||||
if len(buf) < len(ControlEnvelopeMagic) {
|
||||
return 0, nil, false, nil
|
||||
}
|
||||
for i, b := range ControlEnvelopeMagic {
|
||||
if buf[i] != b {
|
||||
return 0, nil, false, nil
|
||||
}
|
||||
}
|
||||
rest := buf[len(ControlEnvelopeMagic):]
|
||||
if len(rest) < 1 {
|
||||
return 0, nil, true, fmt.Errorf("%w: control envelope: missing kind", ErrMalformedFrame)
|
||||
}
|
||||
kind := ControlKind(rest[0])
|
||||
if len(rest) < 5 {
|
||||
return 0, nil, true, fmt.Errorf("%w: control envelope: missing payload length", ErrMalformedFrame)
|
||||
}
|
||||
plen := int(binary.BigEndian.Uint32(rest[1:5]))
|
||||
if len(rest) < 5+plen {
|
||||
return 0, nil, true, fmt.Errorf("%w: control envelope: truncated payload", ErrMalformedFrame)
|
||||
}
|
||||
payload := make([]byte, plen)
|
||||
copy(payload, rest[5:5+plen])
|
||||
return kind, payload, true, nil
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Package frame implements Aura's wire framing: the 5-byte protocol header and the
|
||||
// application-level Frame{Data,Ping,Pong,Close}.
|
||||
//
|
||||
// This is a byte-for-byte port of crates/aura-proto/src/frame.rs. The Rust unit tests in that
|
||||
// file are the wire spec; matching them here keeps the Go port interoperable with the Rust
|
||||
// server.
|
||||
//
|
||||
// Wire layout (from docs/protocol.md §6.1):
|
||||
//
|
||||
// byte 0 : msg_type (u8)
|
||||
// bytes 1..4 : length (u24, big-endian) — payload length in bytes
|
||||
// byte 4 : version = 0x01
|
||||
// bytes 5.. : payload (length bytes)
|
||||
package frame
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// HeaderLen is the size of the protocol header in bytes.
|
||||
const HeaderLen = 5
|
||||
|
||||
// ProtocolVersion is the constant carried in byte 4 of every header.
|
||||
const ProtocolVersion byte = 0x01
|
||||
|
||||
// MaxPayloadLen is the largest payload expressible by the u24 length field.
|
||||
const MaxPayloadLen = 0x00FF_FFFF
|
||||
|
||||
// MsgType is the on-wire message-type discriminant carried in byte 0 of the header.
|
||||
type MsgType byte
|
||||
|
||||
// Message-type bytes (must match the Rust MsgType repr in aura-proto/frame.rs).
|
||||
const (
|
||||
MsgClientHello MsgType = 0x01
|
||||
MsgServerHello MsgType = 0x02
|
||||
MsgClientAuth MsgType = 0x03
|
||||
MsgServerAuth MsgType = 0x04
|
||||
MsgFinished MsgType = 0x05
|
||||
MsgData MsgType = 0x06
|
||||
MsgAlert MsgType = 0xFF
|
||||
)
|
||||
|
||||
// String returns the short name of the message type, for logs.
|
||||
func (m MsgType) String() string {
|
||||
switch m {
|
||||
case MsgClientHello:
|
||||
return "ClientHello"
|
||||
case MsgServerHello:
|
||||
return "ServerHello"
|
||||
case MsgClientAuth:
|
||||
return "ClientAuth"
|
||||
case MsgServerAuth:
|
||||
return "ServerAuth"
|
||||
case MsgFinished:
|
||||
return "Finished"
|
||||
case MsgData:
|
||||
return "Data"
|
||||
case MsgAlert:
|
||||
return "Alert"
|
||||
default:
|
||||
return fmt.Sprintf("MsgType(0x%02X)", byte(m))
|
||||
}
|
||||
}
|
||||
|
||||
// Errors returned by the codec. They mirror the ProtoError variants the Rust side returns so
|
||||
// callers can map them onto identical wire alerts.
|
||||
var (
|
||||
ErrFrameTooLarge = errors.New("aura/frame: payload exceeds 16 MiB u24 length field")
|
||||
ErrBadVersion = errors.New("aura/frame: header byte 4 is not protocol version 0x01")
|
||||
ErrUnknownMsgType = errors.New("aura/frame: unknown message-type byte")
|
||||
ErrMalformedFrame = errors.New("aura/frame: malformed application frame")
|
||||
)
|
||||
|
||||
// EncodeHeader builds a 5-byte header for msgType carrying a payload of payloadLen bytes.
|
||||
func EncodeHeader(msgType MsgType, payloadLen int) ([HeaderLen]byte, error) {
|
||||
var h [HeaderLen]byte
|
||||
if payloadLen < 0 || payloadLen > MaxPayloadLen {
|
||||
return h, fmt.Errorf("%w: len=%d", ErrFrameTooLarge, payloadLen)
|
||||
}
|
||||
h[0] = byte(msgType)
|
||||
// u24 big-endian.
|
||||
h[1] = byte((payloadLen >> 16) & 0xFF)
|
||||
h[2] = byte((payloadLen >> 8) & 0xFF)
|
||||
h[3] = byte(payloadLen & 0xFF)
|
||||
h[4] = ProtocolVersion
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// DecodeHeader parses a 5-byte header into (msgType, payloadLen).
|
||||
func DecodeHeader(h [HeaderLen]byte) (MsgType, int, error) {
|
||||
if h[4] != ProtocolVersion {
|
||||
return 0, 0, fmt.Errorf("%w: got 0x%02X", ErrBadVersion, h[4])
|
||||
}
|
||||
mt := MsgType(h[0])
|
||||
switch mt {
|
||||
case MsgClientHello, MsgServerHello, MsgClientAuth, MsgServerAuth, MsgFinished, MsgData, MsgAlert:
|
||||
// recognized
|
||||
default:
|
||||
return 0, 0, fmt.Errorf("%w: got 0x%02X", ErrUnknownMsgType, h[0])
|
||||
}
|
||||
plen := int(h[1])<<16 | int(h[2])<<8 | int(h[3])
|
||||
return mt, plen, nil
|
||||
}
|
||||
|
||||
// RawFrame is a frame as it was on the wire: type, header bytes (useful for AEAD AAD and the
|
||||
// handshake transcript hash), and payload bytes.
|
||||
type RawFrame struct {
|
||||
MsgType MsgType
|
||||
Header [HeaderLen]byte
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// WireBytes returns header || payload in a fresh slice — used to feed the transcript hash, which
|
||||
// hashes the bytes exactly as transmitted.
|
||||
func (rf *RawFrame) WireBytes() []byte {
|
||||
out := make([]byte, 0, HeaderLen+len(rf.Payload))
|
||||
out = append(out, rf.Header[:]...)
|
||||
out = append(out, rf.Payload...)
|
||||
return out
|
||||
}
|
||||
|
||||
// WriteFrame serializes header || payload and writes it to w. Single Write, so on a streaming
|
||||
// transport a single TCP segment is preferred.
|
||||
func WriteFrame(w io.Writer, msgType MsgType, payload []byte) error {
|
||||
h, err := EncodeHeader(msgType, len(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, 0, HeaderLen+len(payload))
|
||||
buf = append(buf, h[:]...)
|
||||
buf = append(buf, payload...)
|
||||
_, err = w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadFrame reads one full frame (header || payload) from r.
|
||||
func ReadFrame(r io.Reader) (*RawFrame, error) {
|
||||
var h [HeaderLen]byte
|
||||
if _, err := io.ReadFull(r, h[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mt, plen, err := DecodeHeader(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := make([]byte, plen)
|
||||
if plen > 0 {
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &RawFrame{MsgType: mt, Header: h, Payload: payload}, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Application frames (§6.3) — Data, Ping, Pong, Close, Control.
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
|
||||
// FrameKind identifies the Application-frame variant.
|
||||
type FrameKind byte
|
||||
|
||||
// On-wire frame tags (must match crates/aura-proto/src/frame.rs frame_tag::*).
|
||||
const (
|
||||
FrameData FrameKind = 0x01
|
||||
FramePing FrameKind = 0x02
|
||||
FramePong FrameKind = 0x03
|
||||
FrameClose FrameKind = 0x04
|
||||
)
|
||||
|
||||
// Frame is the post-handshake application payload carried inside an AEAD-sealed MsgData record.
|
||||
// One Frame is mapped to one of the four variants by Kind.
|
||||
type Frame struct {
|
||||
Kind FrameKind
|
||||
StreamID uint32 // Data only
|
||||
Payload []byte // Data only
|
||||
Seq uint32 // Ping / Pong only
|
||||
Code byte // Close only
|
||||
Reason string // Close only
|
||||
}
|
||||
|
||||
// EncodeFrame serializes f into its compact byte encoding (all multi-byte ints big-endian):
|
||||
//
|
||||
// Data : 0x01 || stream_id(u32) || payload
|
||||
// Ping : 0x02 || seq(u32)
|
||||
// Pong : 0x03 || seq(u32)
|
||||
// Close : 0x04 || code(u8) || reason_len(u32) || reason_utf8
|
||||
func EncodeFrame(f *Frame) []byte {
|
||||
switch f.Kind {
|
||||
case FrameData:
|
||||
out := make([]byte, 1+4+len(f.Payload))
|
||||
out[0] = byte(FrameData)
|
||||
binary.BigEndian.PutUint32(out[1:5], f.StreamID)
|
||||
copy(out[5:], f.Payload)
|
||||
return out
|
||||
case FramePing:
|
||||
out := make([]byte, 1+4)
|
||||
out[0] = byte(FramePing)
|
||||
binary.BigEndian.PutUint32(out[1:5], f.Seq)
|
||||
return out
|
||||
case FramePong:
|
||||
out := make([]byte, 1+4)
|
||||
out[0] = byte(FramePong)
|
||||
binary.BigEndian.PutUint32(out[1:5], f.Seq)
|
||||
return out
|
||||
case FrameClose:
|
||||
reason := []byte(f.Reason)
|
||||
out := make([]byte, 1+1+4+len(reason))
|
||||
out[0] = byte(FrameClose)
|
||||
out[1] = f.Code
|
||||
binary.BigEndian.PutUint32(out[2:6], uint32(len(reason)))
|
||||
copy(out[6:], reason)
|
||||
return out
|
||||
default:
|
||||
// Programmer error — encode nothing rather than panic so call sites can defensively
|
||||
// inspect the returned length.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeFrame parses one byte-encoded Frame (the inverse of EncodeFrame).
|
||||
func DecodeFrame(b []byte) (*Frame, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, fmt.Errorf("%w: empty frame", ErrMalformedFrame)
|
||||
}
|
||||
tag := FrameKind(b[0])
|
||||
rest := b[1:]
|
||||
switch tag {
|
||||
case FrameData:
|
||||
if len(rest) < 4 {
|
||||
return nil, fmt.Errorf("%w: Data: missing stream_id", ErrMalformedFrame)
|
||||
}
|
||||
sid := binary.BigEndian.Uint32(rest[:4])
|
||||
// Payload is everything after the 4-byte stream_id.
|
||||
payload := make([]byte, len(rest)-4)
|
||||
copy(payload, rest[4:])
|
||||
return &Frame{Kind: FrameData, StreamID: sid, Payload: payload}, nil
|
||||
case FramePing:
|
||||
if len(rest) < 4 {
|
||||
return nil, fmt.Errorf("%w: Ping: truncated seq", ErrMalformedFrame)
|
||||
}
|
||||
return &Frame{Kind: FramePing, Seq: binary.BigEndian.Uint32(rest[:4])}, nil
|
||||
case FramePong:
|
||||
if len(rest) < 4 {
|
||||
return nil, fmt.Errorf("%w: Pong: truncated seq", ErrMalformedFrame)
|
||||
}
|
||||
return &Frame{Kind: FramePong, Seq: binary.BigEndian.Uint32(rest[:4])}, nil
|
||||
case FrameClose:
|
||||
if len(rest) < 1 {
|
||||
return nil, fmt.Errorf("%w: Close: missing code", ErrMalformedFrame)
|
||||
}
|
||||
code := rest[0]
|
||||
if len(rest) < 5 {
|
||||
return nil, fmt.Errorf("%w: Close: missing reason_len", ErrMalformedFrame)
|
||||
}
|
||||
rlen := int(binary.BigEndian.Uint32(rest[1:5]))
|
||||
if len(rest) < 5+rlen {
|
||||
return nil, fmt.Errorf("%w: Close: truncated reason", ErrMalformedFrame)
|
||||
}
|
||||
// We do not enforce strict UTF-8 here (Go strings can hold any bytes); the Rust side
|
||||
// rejects non-UTF-8 in this slot, so peers that follow the spec only ever send valid
|
||||
// strings.
|
||||
return &Frame{Kind: FrameClose, Code: code, Reason: string(rest[5 : 5+rlen])}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unknown frame tag 0x%02X", ErrMalformedFrame, byte(tag))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package frame
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeaderRoundtripAllTypes(t *testing.T) {
|
||||
cases := []struct {
|
||||
ty MsgType
|
||||
b byte
|
||||
}{
|
||||
{MsgClientHello, 0x01},
|
||||
{MsgServerHello, 0x02},
|
||||
{MsgClientAuth, 0x03},
|
||||
{MsgServerAuth, 0x04},
|
||||
{MsgFinished, 0x05},
|
||||
{MsgData, 0x06},
|
||||
{MsgAlert, 0xFF},
|
||||
}
|
||||
for _, c := range cases {
|
||||
h, err := EncodeHeader(c.ty, 0x00123456)
|
||||
if err != nil {
|
||||
t.Fatalf("encode %s: %v", c.ty, err)
|
||||
}
|
||||
if h[0] != c.b {
|
||||
t.Fatalf("type byte for %s: got 0x%02X want 0x%02X", c.ty, h[0], c.b)
|
||||
}
|
||||
if h[4] != ProtocolVersion {
|
||||
t.Fatalf("version byte: got 0x%02X want 0x01", h[4])
|
||||
}
|
||||
mt, plen, err := DecodeHeader(h)
|
||||
if err != nil {
|
||||
t.Fatalf("decode %s: %v", c.ty, err)
|
||||
}
|
||||
if mt != c.ty || plen != 0x00123456 {
|
||||
t.Fatalf("roundtrip mismatch: got (%s, %d)", mt, plen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRejectsOversizeAndBadVersion(t *testing.T) {
|
||||
if _, err := EncodeHeader(MsgData, MaxPayloadLen+1); !errors.Is(err, ErrFrameTooLarge) {
|
||||
t.Fatalf("oversize: want ErrFrameTooLarge, got %v", err)
|
||||
}
|
||||
h, err := EncodeHeader(MsgData, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h[4] = 0x02
|
||||
if _, _, err := DecodeHeader(h); !errors.Is(err, ErrBadVersion) {
|
||||
t.Fatalf("bad version: want ErrBadVersion, got %v", err)
|
||||
}
|
||||
// Reset the version so the unknown-type check actually exercises the type branch.
|
||||
h[4] = ProtocolVersion
|
||||
h[0] = 0x77
|
||||
if _, _, err := DecodeHeader(h); !errors.Is(err, ErrUnknownMsgType) {
|
||||
t.Fatalf("unknown type: want ErrUnknownMsgType, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameRoundtrip(t *testing.T) {
|
||||
frames := []*Frame{
|
||||
{Kind: FrameData, StreamID: 0xDEADBEEF, Payload: []byte("hello world")},
|
||||
{Kind: FrameData, StreamID: 0, Payload: nil},
|
||||
{Kind: FramePing, Seq: 42},
|
||||
{Kind: FramePong, Seq: 0xFFFFFFFF},
|
||||
{Kind: FrameClose, Code: 7, Reason: "going away \U0001F44B"},
|
||||
{Kind: FrameClose, Code: 0, Reason: ""},
|
||||
}
|
||||
for _, f := range frames {
|
||||
enc := EncodeFrame(f)
|
||||
got, err := DecodeFrame(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("decode %v: %v", f.Kind, err)
|
||||
}
|
||||
if got.Kind != f.Kind || got.StreamID != f.StreamID || got.Seq != f.Seq ||
|
||||
got.Code != f.Code || got.Reason != f.Reason || !bytes.Equal(got.Payload, f.Payload) {
|
||||
t.Fatalf("roundtrip mismatch: %+v vs %+v", f, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameDecodeRejectsGarbage(t *testing.T) {
|
||||
if _, err := DecodeFrame(nil); err == nil {
|
||||
t.Fatal("nil: want error")
|
||||
}
|
||||
if _, err := DecodeFrame([]byte{0x99}); err == nil {
|
||||
t.Fatal("unknown tag: want error")
|
||||
}
|
||||
if _, err := DecodeFrame([]byte{byte(FramePing), 0x00}); err == nil {
|
||||
t.Fatal("truncated ping: want error")
|
||||
}
|
||||
if _, err := DecodeFrame([]byte{byte(FrameClose)}); err == nil {
|
||||
t.Fatal("missing close code: want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlEnvelopeRoundtrip(t *testing.T) {
|
||||
env := EncodeControlEnvelope(ControlCrlPush, []byte("hello"))
|
||||
if !bytes.Equal(env[:4], ControlEnvelopeMagic[:]) {
|
||||
t.Fatalf("magic mismatch: %x", env[:4])
|
||||
}
|
||||
kind, payload, ok, err := DecodeControlEnvelope(env)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("decode: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if kind != ControlCrlPush || string(payload) != "hello" {
|
||||
t.Fatalf("decode mismatch: kind=%v payload=%q", kind, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlEnvelopeSkipsNormalIPPackets(t *testing.T) {
|
||||
cases := [][]byte{
|
||||
{0x45, 0x00, 0x00, 0x14}, // IPv4 packet
|
||||
{0x60, 0x00, 0x00, 0x00}, // IPv6 packet
|
||||
{0xAA, 0xAA, 0xC0, 0x02}, // wrong magic last byte
|
||||
{0xAA, 0xAA}, // shorter than magic
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, _, ok, err := DecodeControlEnvelope(c)
|
||||
if ok || err != nil {
|
||||
t.Fatalf("expected pass-through (ok=false, err=nil): got ok=%v err=%v on %x", ok, err, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlEnvelopeRejectsTruncatedPayload(t *testing.T) {
|
||||
env := EncodeControlEnvelope(ControlCrlPush, []byte("payload-bytes"))
|
||||
env = env[:len(env)-3]
|
||||
if _, _, _, err := DecodeControlEnvelope(env); err == nil {
|
||||
t.Fatal("want truncated payload error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAndReadFrameRoundtrip(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
payload := []byte{1, 2, 3, 4, 5}
|
||||
if err := WriteFrame(&buf, MsgData, payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := ReadFrame(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if raw.MsgType != MsgData || !bytes.Equal(raw.Payload, payload) {
|
||||
t.Fatalf("roundtrip mismatch: %+v", raw)
|
||||
}
|
||||
if got := raw.WireBytes(); len(got) != HeaderLen+len(payload) {
|
||||
t.Fatalf("wire bytes wrong length: %d", len(got))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user