初版功能完成
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled

This commit is contained in:
qsc
2026-08-29 13:12:17 +08:00
commit 142e5dc7d6
217 changed files with 21313 additions and 0 deletions
+66
View File
@@ -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")
}
}
+208
View File
@@ -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"`
}
+62
View File
@@ -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
}
}
+144
View File
@@ -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
}
+88
View File
@@ -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)
}
}