初版功能完成
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestControlMessageTypesMatchV1Spec(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got, want := len(ControlMessageTypes), 13; got != want {
|
||||
t.Fatalf("ControlMessageTypes length = %d, want %d", got, want)
|
||||
}
|
||||
seen := make(map[ControlMessageType]struct{}, len(ControlMessageTypes))
|
||||
for _, messageType := range ControlMessageTypes {
|
||||
if !messageType.Valid() {
|
||||
t.Fatalf("listed Control message type %q is invalid", messageType)
|
||||
}
|
||||
if _, duplicate := seen[messageType]; duplicate {
|
||||
t.Fatalf("duplicate Control message type %q", messageType)
|
||||
}
|
||||
seen[messageType] = struct{}{}
|
||||
}
|
||||
if ControlMessageType("SESSION_RESUME").Valid() {
|
||||
t.Fatal("non-v1 SESSION_RESUME unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlEnvelopeRoundTripAndStrictPayload(t *testing.T) {
|
||||
envelope, err := NewControlEnvelope(ControlHeartbeat, "request-1", HeartbeatPayload{
|
||||
Timestamp: time.Date(2026, 8, 25, 0, 0, 0, 0, time.UTC), Status: "OK",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload HeartbeatPayload
|
||||
if err := envelope.DecodePayload(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.Status != "OK" || envelope.RequestID != "request-1" {
|
||||
t.Fatalf("unexpected envelope: %+v payload=%+v", envelope, payload)
|
||||
}
|
||||
envelope.Payload = []byte(`{"timestamp":"2026-08-25T00:00:00Z","status":"OK","unknown":true}`)
|
||||
if err := envelope.DecodePayload(&payload); err == nil {
|
||||
t.Fatal("unknown Control payload field was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorCodesMatchAuthoritativeV1List(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got, want := len(ErrorCodes), 14; got != want {
|
||||
t.Fatalf("ErrorCodes length = %d, want %d", got, want)
|
||||
}
|
||||
seen := make(map[ErrorCode]struct{}, len(ErrorCodes))
|
||||
for _, code := range ErrorCodes {
|
||||
if !code.Valid() {
|
||||
t.Fatalf("listed error code %q is invalid", code)
|
||||
}
|
||||
if _, duplicate := seen[code]; duplicate {
|
||||
t.Fatalf("duplicate error code %q", code)
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
}
|
||||
if ErrorCode("UNKNOWN").Valid() {
|
||||
t.Fatal("unknown error code unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"remlink/internal/model"
|
||||
)
|
||||
|
||||
// ErrRebootstrapRequired is returned by a Node runtime when all state derived
|
||||
// from the public Bootstrap API must be rebuilt. Callers must tear down the
|
||||
// current adapter, routes, WireGuard device, and Session runtime before retrying.
|
||||
var ErrRebootstrapRequired = errors.New("Node rebootstrap is required")
|
||||
|
||||
// ControlMessageType identifies a Control WebSocket message.
|
||||
type ControlMessageType string
|
||||
|
||||
const (
|
||||
ControlHello ControlMessageType = "HELLO"
|
||||
ControlWelcome ControlMessageType = "WELCOME"
|
||||
ControlNodeList ControlMessageType = "NODE_LIST"
|
||||
ControlCreateSession ControlMessageType = "CREATE_SESSION"
|
||||
ControlPrepareSession ControlMessageType = "PREPARE_SESSION"
|
||||
ControlPrepareResult ControlMessageType = "PREPARE_RESULT"
|
||||
ControlSessionConfig ControlMessageType = "SESSION_CONFIG"
|
||||
ControlRoutesReady ControlMessageType = "ROUTES_READY"
|
||||
ControlSessionActive ControlMessageType = "SESSION_ACTIVE"
|
||||
ControlStopSession ControlMessageType = "STOP_SESSION"
|
||||
ControlSessionStats ControlMessageType = "SESSION_STATS"
|
||||
ControlHeartbeat ControlMessageType = "HEARTBEAT"
|
||||
ControlRebootstrapRequired ControlMessageType = "REBOOTSTRAP_REQUIRED"
|
||||
)
|
||||
|
||||
// ControlMessageTypes is the complete v1 Control message set.
|
||||
var ControlMessageTypes = [...]ControlMessageType{
|
||||
ControlHello,
|
||||
ControlWelcome,
|
||||
ControlNodeList,
|
||||
ControlCreateSession,
|
||||
ControlPrepareSession,
|
||||
ControlPrepareResult,
|
||||
ControlSessionConfig,
|
||||
ControlRoutesReady,
|
||||
ControlSessionActive,
|
||||
ControlStopSession,
|
||||
ControlSessionStats,
|
||||
ControlHeartbeat,
|
||||
ControlRebootstrapRequired,
|
||||
}
|
||||
|
||||
// Valid reports whether the message type is part of the v1 Control protocol.
|
||||
func (t ControlMessageType) Valid() bool {
|
||||
switch t {
|
||||
case ControlHello,
|
||||
ControlWelcome,
|
||||
ControlNodeList,
|
||||
ControlCreateSession,
|
||||
ControlPrepareSession,
|
||||
ControlPrepareResult,
|
||||
ControlSessionConfig,
|
||||
ControlRoutesReady,
|
||||
ControlSessionActive,
|
||||
ControlStopSession,
|
||||
ControlSessionStats,
|
||||
ControlHeartbeat,
|
||||
ControlRebootstrapRequired:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ControlEnvelope is the stable v1 WebSocket framing shared by all messages.
|
||||
type ControlEnvelope struct {
|
||||
Type ControlMessageType `json:"type"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
// NewControlEnvelope serializes a typed payload.
|
||||
func NewControlEnvelope(messageType ControlMessageType, requestID string, payload any) (ControlEnvelope, error) {
|
||||
if !messageType.Valid() {
|
||||
return ControlEnvelope{}, fmt.Errorf("invalid Control message type %q", messageType)
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return ControlEnvelope{}, fmt.Errorf("encode %s payload: %w", messageType, err)
|
||||
}
|
||||
return ControlEnvelope{Type: messageType, RequestID: requestID, Payload: raw}, nil
|
||||
}
|
||||
|
||||
// DecodePayload strictly decodes exactly one JSON object.
|
||||
func (e ControlEnvelope) DecodePayload(destination any) error {
|
||||
if !e.Type.Valid() {
|
||||
return fmt.Errorf("invalid Control message type %q", e.Type)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(e.Payload))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(destination); err != nil {
|
||||
return fmt.Errorf("decode %s payload: %w", e.Type, err)
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("%s payload must contain one JSON value", e.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NodeCapabilities struct {
|
||||
RemoteSubnet bool `json:"remote_subnet"`
|
||||
NetstackStatus string `json:"netstack_status,omitempty"`
|
||||
TCPCapacity int `json:"tcp_capacity,omitempty"`
|
||||
UDPCapacity int `json:"udp_capacity,omitempty"`
|
||||
}
|
||||
|
||||
type HelloPayload struct {
|
||||
NodeID string `json:"node_id"`
|
||||
NodeToken string `json:"node_token"`
|
||||
ConfigVersion uint64 `json:"config_version"`
|
||||
Capabilities NodeCapabilities `json:"capabilities"`
|
||||
OSVersion string `json:"os_version"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type WelcomePayload struct {
|
||||
ServerTime time.Time `json:"server_time"`
|
||||
NetworkConfigVersion uint64 `json:"network_config_version"`
|
||||
}
|
||||
|
||||
type HeartbeatPayload struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type SiteSummary struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
OverlayIP string `json:"overlay_ip"`
|
||||
Online bool `json:"online"`
|
||||
RemoteSubnetCapability bool `json:"remote_subnet_capability"`
|
||||
LastSeen time.Time `json:"last_seen,omitempty"`
|
||||
}
|
||||
|
||||
type NodeListPayload struct {
|
||||
Sites []SiteSummary `json:"sites"`
|
||||
}
|
||||
|
||||
// CreateSessionPayload is accepted only from an authenticated Engineer.
|
||||
type CreateSessionPayload struct {
|
||||
SiteNodeID string `json:"site_node_id"`
|
||||
TargetCIDRs []string `json:"target_cidrs"`
|
||||
}
|
||||
|
||||
type PrepareSessionPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
EngineerOverlayIP string `json:"engineer_overlay_ip"`
|
||||
TargetCIDRs []string `json:"target_cidrs"`
|
||||
}
|
||||
|
||||
type RouteResult struct {
|
||||
CIDR string `json:"cidr"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type PrepareResultPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
OK bool `json:"ok"`
|
||||
RouteResults []RouteResult `json:"route_results"`
|
||||
SubnetGatewayStatus string `json:"subnet_gateway_status"`
|
||||
TCPCapacity int `json:"tcp_capacity"`
|
||||
UDPCapacity int `json:"udp_capacity"`
|
||||
ErrorCode ErrorCode `json:"error_code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type SessionConfigPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
PeerOverlayIP string `json:"peer_overlay_ip"`
|
||||
CIDRs []string `json:"cidrs"`
|
||||
MTU int `json:"mtu"`
|
||||
UDPPort int `json:"udp_port"`
|
||||
}
|
||||
|
||||
type RoutesReadyPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
}
|
||||
|
||||
type SessionActivePayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
}
|
||||
|
||||
type StopSessionPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type SessionStatsPayload struct {
|
||||
SessionID uint64 `json:"session_id"`
|
||||
Counters model.SessionCounters `json:"counters"`
|
||||
}
|
||||
|
||||
type RebootstrapRequiredPayload struct {
|
||||
ConfigVersion uint64 `json:"config_version"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package protocol
|
||||
|
||||
// ErrorCode is a stable machine-readable RemLink failure identifier.
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorServerUnreachable ErrorCode = "SERVER_UNREACHABLE"
|
||||
ErrorJoinTokenInvalid ErrorCode = "JOIN_TOKEN_INVALID"
|
||||
ErrorNodeAuthFailed ErrorCode = "NODE_AUTH_FAILED"
|
||||
ErrorOverlayLocalConflict ErrorCode = "OVERLAY_LOCAL_CONFLICT"
|
||||
ErrorEngineerSessionExists ErrorCode = "ENGINEER_SESSION_EXISTS"
|
||||
ErrorSiteOffline ErrorCode = "SITE_OFFLINE"
|
||||
ErrorCIDRInvalid ErrorCode = "CIDR_INVALID"
|
||||
ErrorCIDRLocalConflict ErrorCode = "CIDR_LOCAL_CONFLICT"
|
||||
ErrorCIDROverlayConflict ErrorCode = "CIDR_OVERLAY_CONFLICT"
|
||||
ErrorSiteNoRoute ErrorCode = "SITE_NO_ROUTE"
|
||||
ErrorNetstackUnavailable ErrorCode = "NETSTACK_UNAVAILABLE"
|
||||
ErrorFlowLimitReached ErrorCode = "FLOW_LIMIT_REACHED"
|
||||
ErrorSessionTimeout ErrorCode = "SESSION_TIMEOUT"
|
||||
ErrorSessionInjectFailed ErrorCode = "SESSION_INJECT_FAILED"
|
||||
)
|
||||
|
||||
// ErrorCodes is the authoritative 14-code v1 baseline.
|
||||
var ErrorCodes = [...]ErrorCode{
|
||||
ErrorServerUnreachable,
|
||||
ErrorJoinTokenInvalid,
|
||||
ErrorNodeAuthFailed,
|
||||
ErrorOverlayLocalConflict,
|
||||
ErrorEngineerSessionExists,
|
||||
ErrorSiteOffline,
|
||||
ErrorCIDRInvalid,
|
||||
ErrorCIDRLocalConflict,
|
||||
ErrorCIDROverlayConflict,
|
||||
ErrorSiteNoRoute,
|
||||
ErrorNetstackUnavailable,
|
||||
ErrorFlowLimitReached,
|
||||
ErrorSessionTimeout,
|
||||
ErrorSessionInjectFailed,
|
||||
}
|
||||
|
||||
// Valid reports whether the code belongs to the v1 baseline.
|
||||
func (c ErrorCode) Valid() bool {
|
||||
switch c {
|
||||
case ErrorServerUnreachable,
|
||||
ErrorJoinTokenInvalid,
|
||||
ErrorNodeAuthFailed,
|
||||
ErrorOverlayLocalConflict,
|
||||
ErrorEngineerSessionExists,
|
||||
ErrorSiteOffline,
|
||||
ErrorCIDRInvalid,
|
||||
ErrorCIDRLocalConflict,
|
||||
ErrorCIDROverlayConflict,
|
||||
ErrorSiteNoRoute,
|
||||
ErrorNetstackUnavailable,
|
||||
ErrorFlowLimitReached,
|
||||
ErrorSessionTimeout,
|
||||
ErrorSessionInjectFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Package protocol defines RemLink wire-level constants and codecs.
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// SessionHeaderSize is the fixed v1 Session header length in bytes.
|
||||
SessionHeaderSize = 20
|
||||
// SessionVersion is the only Session protocol version supported by v1.
|
||||
SessionVersion uint8 = 1
|
||||
// SessionTypeIPv4 marks a payload as one complete raw IPv4 packet.
|
||||
SessionTypeIPv4 uint8 = 0x01
|
||||
)
|
||||
|
||||
var (
|
||||
sessionMagic = [4]byte{'R', 'M', 'L', 'K'}
|
||||
|
||||
ErrSessionHeaderTooShort = errors.New("session packet is shorter than the 20-byte header")
|
||||
ErrSessionMagic = errors.New("invalid session magic")
|
||||
ErrSessionVersion = errors.New("unsupported session version")
|
||||
ErrSessionType = errors.New("unsupported session payload type")
|
||||
ErrSessionFlags = errors.New("session flags must be zero in v1")
|
||||
ErrSessionReserved = errors.New("session reserved field must be zero in v1")
|
||||
ErrSessionPayloadLength = errors.New("session payload length mismatch")
|
||||
ErrSessionPayloadTooLong = errors.New("session payload exceeds uint16 length")
|
||||
)
|
||||
|
||||
// SessionHeader is the fixed 20-byte prefix of every Remote Subnet datagram.
|
||||
// Multi-byte fields are encoded in network byte order (big-endian).
|
||||
type SessionHeader struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Flags uint16
|
||||
SessionID uint64
|
||||
PayloadLen uint16
|
||||
Reserved uint16
|
||||
}
|
||||
|
||||
// NewIPv4SessionHeader constructs the canonical v1 header for a raw IPv4 payload.
|
||||
func NewIPv4SessionHeader(sessionID uint64, payloadLen uint16) SessionHeader {
|
||||
return SessionHeader{
|
||||
Version: SessionVersion,
|
||||
Type: SessionTypeIPv4,
|
||||
SessionID: sessionID,
|
||||
PayloadLen: payloadLen,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks all v1 constants and reserved fields.
|
||||
func (h SessionHeader) Validate() error {
|
||||
switch {
|
||||
case h.Version != SessionVersion:
|
||||
return fmt.Errorf("%w: got %d", ErrSessionVersion, h.Version)
|
||||
case h.Type != SessionTypeIPv4:
|
||||
return fmt.Errorf("%w: got 0x%02x", ErrSessionType, h.Type)
|
||||
case h.Flags != 0:
|
||||
return fmt.Errorf("%w: got 0x%04x", ErrSessionFlags, h.Flags)
|
||||
case h.Reserved != 0:
|
||||
return fmt.Errorf("%w: got 0x%04x", ErrSessionReserved, h.Reserved)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalBinary serializes only the fixed header.
|
||||
func (h SessionHeader) MarshalBinary() ([]byte, error) {
|
||||
if err := h.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]byte, SessionHeaderSize)
|
||||
copy(out[0:4], sessionMagic[:])
|
||||
out[4] = h.Version
|
||||
out[5] = h.Type
|
||||
binary.BigEndian.PutUint16(out[6:8], h.Flags)
|
||||
binary.BigEndian.PutUint64(out[8:16], h.SessionID)
|
||||
binary.BigEndian.PutUint16(out[16:18], h.PayloadLen)
|
||||
binary.BigEndian.PutUint16(out[18:20], h.Reserved)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ParseSessionHeader parses and validates a fixed header from the start of data.
|
||||
func ParseSessionHeader(data []byte) (SessionHeader, error) {
|
||||
if len(data) < SessionHeaderSize {
|
||||
return SessionHeader{}, ErrSessionHeaderTooShort
|
||||
}
|
||||
if !bytes.Equal(data[0:4], sessionMagic[:]) {
|
||||
return SessionHeader{}, ErrSessionMagic
|
||||
}
|
||||
|
||||
header := SessionHeader{
|
||||
Version: data[4],
|
||||
Type: data[5],
|
||||
Flags: binary.BigEndian.Uint16(data[6:8]),
|
||||
SessionID: binary.BigEndian.Uint64(data[8:16]),
|
||||
PayloadLen: binary.BigEndian.Uint16(data[16:18]),
|
||||
Reserved: binary.BigEndian.Uint16(data[18:20]),
|
||||
}
|
||||
if err := header.Validate(); err != nil {
|
||||
return SessionHeader{}, err
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// EncodeIPv4Session serializes a header and one complete raw IPv4 packet.
|
||||
func EncodeIPv4Session(sessionID uint64, packet []byte) ([]byte, error) {
|
||||
if len(packet) > int(^uint16(0)) {
|
||||
return nil, ErrSessionPayloadTooLong
|
||||
}
|
||||
header := NewIPv4SessionHeader(sessionID, uint16(len(packet)))
|
||||
headerBytes, err := header.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]byte, SessionHeaderSize+len(packet))
|
||||
copy(out, headerBytes)
|
||||
copy(out[SessionHeaderSize:], packet)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DecodeIPv4Session validates framing and returns the header and raw IPv4 payload.
|
||||
// The returned payload aliases data and must be copied before data is reused.
|
||||
func DecodeIPv4Session(data []byte) (SessionHeader, []byte, error) {
|
||||
header, err := ParseSessionHeader(data)
|
||||
if err != nil {
|
||||
return SessionHeader{}, nil, err
|
||||
}
|
||||
payload := data[SessionHeaderSize:]
|
||||
if len(payload) != int(header.PayloadLen) {
|
||||
return SessionHeader{}, nil, fmt.Errorf(
|
||||
"%w: header=%d actual=%d",
|
||||
ErrSessionPayloadLength,
|
||||
header.PayloadLen,
|
||||
len(payload),
|
||||
)
|
||||
}
|
||||
return header, payload, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSessionHeaderGoldenBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
header := NewIPv4SessionHeader(0x0102030405060708, 0x0014)
|
||||
got, err := header.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalBinary() error = %v", err)
|
||||
}
|
||||
want := []byte{
|
||||
'R', 'M', 'L', 'K',
|
||||
0x01, 0x01,
|
||||
0x00, 0x00,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x00, 0x14,
|
||||
0x00, 0x00,
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("MarshalBinary() = %x, want %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPv4SessionRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
packet := []byte{0x45, 0x00, 0x00, 0x04}
|
||||
encoded, err := EncodeIPv4Session(42, packet)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeIPv4Session() error = %v", err)
|
||||
}
|
||||
header, decoded, err := DecodeIPv4Session(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeIPv4Session() error = %v", err)
|
||||
}
|
||||
if header.SessionID != 42 {
|
||||
t.Fatalf("SessionID = %d, want 42", header.SessionID)
|
||||
}
|
||||
if !bytes.Equal(decoded, packet) {
|
||||
t.Fatalf("decoded payload = %x, want %x", decoded, packet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeIPv4SessionRejectsInvalidFraming(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid, err := EncodeIPv4Session(7, []byte{0x45})
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeIPv4Session() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func([]byte) []byte
|
||||
want error
|
||||
}{
|
||||
{name: "short", mutate: func(data []byte) []byte { return data[:19] }, want: ErrSessionHeaderTooShort},
|
||||
{name: "magic", mutate: func(data []byte) []byte { data[0] = 'X'; return data }, want: ErrSessionMagic},
|
||||
{name: "version", mutate: func(data []byte) []byte { data[4] = 2; return data }, want: ErrSessionVersion},
|
||||
{name: "type", mutate: func(data []byte) []byte { data[5] = 2; return data }, want: ErrSessionType},
|
||||
{name: "flags", mutate: func(data []byte) []byte { data[7] = 1; return data }, want: ErrSessionFlags},
|
||||
{name: "reserved", mutate: func(data []byte) []byte { data[19] = 1; return data }, want: ErrSessionReserved},
|
||||
{name: "payload length", mutate: func(data []byte) []byte { data[17] = 2; return data }, want: ErrSessionPayloadLength},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := append([]byte(nil), valid...)
|
||||
_, _, gotErr := DecodeIPv4Session(test.mutate(input))
|
||||
if !errors.Is(gotErr, test.want) {
|
||||
t.Fatalf("DecodeIPv4Session() error = %v, want %v", gotErr, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeIPv4SessionRejectsOversizedPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := EncodeIPv4Session(1, make([]byte, 1<<16))
|
||||
if !errors.Is(err, ErrSessionPayloadTooLong) {
|
||||
t.Fatalf("EncodeIPv4Session() error = %v, want %v", err, ErrSessionPayloadTooLong)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user