初版功能完成
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user