337 lines
9.4 KiB
Go
337 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"`
|
|
NodeSecret string `json:"nodeSecret,omitempty"`
|
|
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"`
|
|
}
|