209 lines
6.5 KiB
Go
209 lines
6.5 KiB
Go
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"`
|
|
}
|