Files
RemLink/internal/protocol/control.go
T
qsc20001102 142e5dc7d6
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled
初版功能完成
2026-08-29 13:12:17 +08:00

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"`
}