Files
inp2p/pkg/protocol/protocol.go
openclaw 9f6e065f3a feat: web console with real management operations
Backend APIs added:
- POST /api/v1/nodes/kick - disconnect a node
- POST /api/v1/connect - trigger P2P tunnel between nodes
- GET /api/v1/stats - detailed server statistics

Frontend features:
- Dashboard: real stats from /api/v1/stats (cone/symm/relay counts)
- Node management: table view, kick node, configure tunnels
- SDWAN: enable/disable, CIDR config, IP allocation, online status
- P2P Connect: create tunnel between two nodes from UI
- Event log: tracks all operations
2026-03-03 00:42:01 +08:00

336 lines
9.4 KiB
Go

// Package protocol defines the INP2P wire protocol.
//
// Message format: [Header 8B] + [JSON payload]
//
// Header: DataLen(uint32 LE) + MainType(uint16 LE) + SubType(uint16 LE)
// DataLen = len(header) + len(payload) = 8 + len(json)
package protocol
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
)
// HeaderSize is the fixed 8-byte message header.
const HeaderSize = 8
// ─── Main message types ───
const (
MsgLogin uint16 = 1
MsgHeartbeat uint16 = 2
MsgNAT uint16 = 3
MsgPush uint16 = 4 // signaling push (punch/relay coordination)
MsgRelay uint16 = 5
MsgReport uint16 = 6
MsgTunnel uint16 = 7 // in-tunnel control messages
)
// ─── Sub types: MsgLogin ───
const (
SubLoginReq uint16 = iota
SubLoginRsp
)
// ─── Sub types: MsgHeartbeat ───
const (
SubHeartbeatPing uint16 = iota
SubHeartbeatPong
)
// ─── Sub types: MsgNAT ───
const (
SubNATDetectReq uint16 = iota
SubNATDetectRsp
)
// ─── Sub types: MsgPush ───
const (
SubPushConnectReq uint16 = iota // "please connect to peer X"
SubPushConnectRsp // peer's punch parameters
SubPushPunchStart // coordinate simultaneous punch
SubPushPunchResult // report punch outcome
SubPushRelayOffer // relay node offers to relay
SubPushNodeOnline // notify: destination came online
SubPushEditApp // add/edit tunnel app
SubPushDeleteApp // delete tunnel app
SubPushReportApps // request app list
SubPushSDWANConfig // push sdwan config to client
SubPushSDWANPeer // push sdwan peer online/update
SubPushSDWANDel // push sdwan peer offline/delete
SubPushConfig // generic remote config push
)
// Sub types: MsgTunnel
const (
SubTunnelSDWANData uint16 = iota
SubTunnelSDWANRaw
)
// ─── Sub types: MsgRelay ───
const (
SubRelayNodeReq uint16 = iota
SubRelayNodeRsp
SubRelayDataReq // establish data channel through relay
SubRelayDataRsp
)
// ─── Sub types: MsgReport ───
const (
SubReportBasic uint16 = iota // OS, version, MAC, etc.
SubReportApps // running tunnels
SubReportConnect // connection result
)
// ─── NAT types ───
type NATType int
const (
NATNone NATType = 0 // public IP, no NAT
NATCone NATType = 1 // full/restricted/port-restricted cone
NATSymmetric NATType = 2 // symmetric (port changes per dest)
NATUnknown NATType = 314 // detection failed / UDP blocked
)
func (n NATType) String() string {
switch n {
case NATNone:
return "None"
case NATCone:
return "Cone"
case NATSymmetric:
return "Symmetric"
default:
return "Unknown"
}
}
// CanPunch returns true if at least one side is Cone (or has public IP).
func CanPunch(a, b NATType) bool {
return a == NATNone || b == NATNone || a == NATCone || b == NATCone
}
// ─── Header ───
type Header struct {
DataLen uint32
MainType uint16
SubType uint16
}
// ─── Encode / Decode ───
// Encode packs header + JSON payload into a byte slice.
func Encode(mainType, subType uint16, payload interface{}) ([]byte, error) {
var jsonData []byte
if payload != nil {
var err error
jsonData, err = json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
}
h := Header{
DataLen: uint32(HeaderSize + len(jsonData)),
MainType: mainType,
SubType: subType,
}
buf := new(bytes.Buffer)
buf.Grow(int(h.DataLen))
if err := binary.Write(buf, binary.LittleEndian, h); err != nil {
return nil, err
}
buf.Write(jsonData)
return buf.Bytes(), nil
}
// EncodeRaw packs header + raw (binary) payload.
func EncodeRaw(mainType, subType uint16, payload []byte) []byte {
h := Header{
DataLen: uint32(HeaderSize + len(payload)),
MainType: mainType,
SubType: subType,
}
buf := new(bytes.Buffer)
buf.Grow(int(h.DataLen))
_ = binary.Write(buf, binary.LittleEndian, h)
buf.Write(payload)
return buf.Bytes()
}
// DecodeHeader reads the 8-byte header from r.
func DecodeHeader(data []byte) (Header, error) {
if len(data) < HeaderSize {
return Header{}, io.ErrShortBuffer
}
var h Header
err := binary.Read(bytes.NewReader(data[:HeaderSize]), binary.LittleEndian, &h)
return h, err
}
// DecodePayload unmarshals the JSON portion after the header.
func DecodePayload(data []byte, v interface{}) error {
if len(data) <= HeaderSize {
return nil // empty payload is valid
}
return json.Unmarshal(data[HeaderSize:], v)
}
// ─── Common message structs ───
// LoginReq is sent by client on WSS connect.
type LoginReq struct {
Node string `json:"node"`
Token uint64 `json:"token"`
User string `json:"user,omitempty"`
Version string `json:"version"`
NATType NATType `json:"natType"`
ShareBandwidth int `json:"shareBandwidth"`
RelayEnabled bool `json:"relayEnabled"` // --relay flag
SuperRelay bool `json:"superRelay"` // --super flag
PublicIP string `json:"publicIP,omitempty"`
PublicPort int `json:"publicPort,omitempty"`
}
type LoginRsp struct {
Error int `json:"error"`
Detail string `json:"detail,omitempty"`
Ts int64 `json:"ts"`
Token uint64 `json:"token"`
User string `json:"user"`
Node string `json:"node"`
}
// ReportBasic is the initial system info report after login.
type ReportBasic struct {
OS string `json:"os"`
Mac string `json:"mac"`
LanIP string `json:"lanIP"`
Version string `json:"version"`
HasIPv4 int `json:"hasIPv4"`
HasUPNPorNATPMP int `json:"hasUPNPorNATPMP"`
IPv6 string `json:"IPv6,omitempty"`
PublicIP string `json:"publicIP,omitempty"`
PublicPort int `json:"publicPort,omitempty"`
}
type ReportBasicRsp struct {
Error int `json:"error"`
}
// PunchParams carries the information needed for hole-punching.
type PunchParams struct {
IP string `json:"ip"`
Port int `json:"port"`
NATType NATType `json:"natType"`
Token uint64 `json:"token"` // TOTP for auth
IPv6 string `json:"ipv6,omitempty"`
HasIPv4 int `json:"hasIPv4"`
LinkMode string `json:"linkMode"` // "udp" or "tcp"
}
// ConnectReq is pushed by server to coordinate a connection.
type ConnectReq struct {
From string `json:"from"`
To string `json:"to"`
FromIP string `json:"fromIP"`
Peer PunchParams `json:"peer"`
AppName string `json:"appName,omitempty"`
Protocol string `json:"protocol"` // "tcp" or "udp"
SrcPort int `json:"srcPort"`
DstHost string `json:"dstHost"`
DstPort int `json:"dstPort"`
}
type ConnectRsp struct {
Error int `json:"error"`
Detail string `json:"detail,omitempty"`
From string `json:"from"`
To string `json:"to"`
Peer PunchParams `json:"peer,omitempty"`
}
// RelayNodeReq asks the server for a relay node.
type RelayNodeReq struct {
PeerNode string `json:"peerNode"`
}
type RelayNodeRsp struct {
RelayName string `json:"relayName"`
RelayIP string `json:"relayIP"`
RelayPort int `json:"relayPort"`
RelayToken uint64 `json:"relayToken"`
Mode string `json:"mode"` // "private", "super", "server"
Error int `json:"error"`
}
// AppConfig defines a tunnel application.
type AppConfig struct {
AppName string `json:"appName"`
Protocol string `json:"protocol"` // "tcp" or "udp"
SrcPort int `json:"srcPort"`
PeerNode string `json:"peerNode"`
DstHost string `json:"dstHost"`
DstPort int `json:"dstPort"`
Enabled int `json:"enabled"`
RelayNode string `json:"relayNode,omitempty"` // force specific relay
}
type SDWANNode struct {
Node string `json:"node"`
IP string `json:"ip"`
}
type SDWANConfig struct {
Enabled bool `json:"enabled,omitempty"`
Name string `json:"name,omitempty"`
GatewayCIDR string `json:"gatewayCIDR"`
Mode string `json:"mode,omitempty"` // hub | mesh | fullmesh
IP string `json:"ip,omitempty"` // node self IP if pushed per-node
MTU int `json:"mtu,omitempty"`
Routes []string `json:"routes,omitempty"`
Nodes []SDWANNode `json:"nodes"`
UpdatedAt int64 `json:"updatedAt,omitempty"`
}
type SDWANPeer struct {
Node string `json:"node"`
IP string `json:"ip"`
Online bool `json:"online"`
}
type SDWANPacket struct {
FromNode string `json:"fromNode,omitempty"`
ToNode string `json:"toNode,omitempty"`
SrcIP string `json:"srcIP,omitempty"`
DstIP string `json:"dstIP,omitempty"`
Payload []byte `json:"payload"`
}
// ReportConnect is the connection result reported to server.
type ReportConnect struct {
PeerNode string `json:"peerNode"`
NATType NATType `json:"natType"`
PeerNATType NATType `json:"peerNatType"`
LinkMode string `json:"linkMode"` // "udppunch", "tcppunch", "relay"
Error string `json:"error,omitempty"`
RTT int `json:"rtt,omitempty"` // milliseconds
RelayNode string `json:"relayNode,omitempty"`
Protocol string `json:"protocol,omitempty"`
SrcPort int `json:"srcPort,omitempty"`
DstPort int `json:"dstPort,omitempty"`
DstHost string `json:"dstHost,omitempty"`
Version string `json:"version,omitempty"`
ShareBandwidth int `json:"shareBandWidth,omitempty"`
}