fix: multi issues - TUN read loop, SDWAN routing for TenantID=0, WS keepalive 10s
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -17,12 +18,12 @@ import (
|
||||
|
||||
// HandleConnectReq processes a connection request from node A to node B.
|
||||
func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error {
|
||||
to := s.GetNode(req.To)
|
||||
to := s.GetNodeForUser(req.To, from.Token)
|
||||
if to == nil || !to.IsOnline() {
|
||||
// Peer offline — respond with error
|
||||
// Peer offline or not visible — respond with generic not found
|
||||
from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{
|
||||
Error: 1,
|
||||
Detail: fmt.Sprintf("node %s offline", req.To),
|
||||
Detail: "node not found",
|
||||
From: req.To,
|
||||
To: req.From,
|
||||
})
|
||||
@@ -38,6 +39,7 @@ func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error
|
||||
Port: from.PublicPort,
|
||||
NATType: from.NATType,
|
||||
HasIPv4: from.HasIPv4,
|
||||
Token: auth.GenTOTP(from.Token, time.Now().Unix()),
|
||||
}
|
||||
from.mu.RUnlock()
|
||||
|
||||
@@ -47,6 +49,7 @@ func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error
|
||||
Port: to.PublicPort,
|
||||
NATType: to.NATType,
|
||||
HasIPv4: to.HasIPv4,
|
||||
Token: auth.GenTOTP(to.Token, time.Now().Unix()),
|
||||
}
|
||||
to.mu.RUnlock()
|
||||
|
||||
|
||||
BIN
internal/server/inp2ps.db-shm
Normal file
BIN
internal/server/inp2ps.db-shm
Normal file
Binary file not shown.
BIN
internal/server/inp2ps.db-wal
Normal file
BIN
internal/server/inp2ps.db-wal
Normal file
Binary file not shown.
@@ -12,13 +12,14 @@ import (
|
||||
)
|
||||
|
||||
type sdwanStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
cfg protocol.SDWANConfig
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
cfg protocol.SDWANConfig
|
||||
multi map[int64]protocol.SDWANConfig
|
||||
}
|
||||
|
||||
func newSDWANStore(path string) *sdwanStore {
|
||||
s := &sdwanStore{path: path}
|
||||
s := &sdwanStore{path: path, multi: make(map[int64]protocol.SDWANConfig)}
|
||||
_ = s.load()
|
||||
return s
|
||||
}
|
||||
@@ -33,6 +34,15 @@ func (s *sdwanStore) load() error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
// try multi-tenant first
|
||||
var m map[int64]protocol.SDWANConfig
|
||||
if err := json.Unmarshal(b, &m); err == nil && len(m) > 0 {
|
||||
for k, v := range m {
|
||||
m[k] = normalizeSDWAN(v)
|
||||
}
|
||||
s.multi = m
|
||||
return nil
|
||||
}
|
||||
var c protocol.SDWANConfig
|
||||
if err := json.Unmarshal(b, &c); err != nil {
|
||||
return err
|
||||
@@ -57,12 +67,40 @@ func (s *sdwanStore) save(cfg protocol.SDWANConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sdwanStore) saveTenant(tenantID int64, cfg protocol.SDWANConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
cfg = normalizeSDWAN(cfg)
|
||||
cfg.UpdatedAt = time.Now().Unix()
|
||||
if s.multi == nil {
|
||||
s.multi = make(map[int64]protocol.SDWANConfig)
|
||||
}
|
||||
s.multi[tenantID] = cfg
|
||||
b, err := json.MarshalIndent(s.multi, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(s.path, b, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sdwanStore) get() protocol.SDWANConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
func (s *sdwanStore) getTenant(tenantID int64) protocol.SDWANConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.multi == nil {
|
||||
return protocol.SDWANConfig{}
|
||||
}
|
||||
return s.multi[tenantID]
|
||||
}
|
||||
|
||||
func normalizeSDWAN(c protocol.SDWANConfig) protocol.SDWANConfig {
|
||||
if c.Mode == "" {
|
||||
c.Mode = "hub"
|
||||
|
||||
@@ -11,6 +11,10 @@ func (s *Server) GetSDWAN() protocol.SDWANConfig {
|
||||
return s.sdwan.get()
|
||||
}
|
||||
|
||||
func (s *Server) GetSDWANTenant(tenantID int64) protocol.SDWANConfig {
|
||||
return s.sdwan.getTenant(tenantID)
|
||||
}
|
||||
|
||||
func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error {
|
||||
if err := s.sdwan.save(cfg); err != nil {
|
||||
return err
|
||||
@@ -19,6 +23,14 @@ func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig) error {
|
||||
if err := s.sdwan.saveTenant(tenantID, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.broadcastSDWANTenant(tenantID, s.sdwan.getTenant(tenantID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) broadcastSDWAN(cfg protocol.SDWANConfig) {
|
||||
if !cfg.Enabled || cfg.GatewayCIDR == "" {
|
||||
return
|
||||
@@ -33,6 +45,20 @@ func (s *Server) broadcastSDWAN(cfg protocol.SDWANConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) broadcastSDWANTenant(tenantID int64, cfg protocol.SDWANConfig) {
|
||||
if !cfg.Enabled || cfg.GatewayCIDR == "" {
|
||||
return
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, n := range s.nodes {
|
||||
if !n.IsOnline() || n.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
_ = n.Conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) pushSDWANPeer(to *NodeInfo, peer protocol.SDWANPeer) {
|
||||
if to == nil || !to.IsOnline() {
|
||||
return
|
||||
@@ -48,7 +74,14 @@ func (s *Server) pushSDWANDel(to *NodeInfo, peer protocol.SDWANPeer) {
|
||||
}
|
||||
|
||||
func (s *Server) announceSDWANNodeOnline(nodeName string) {
|
||||
cfg := s.sdwan.get()
|
||||
// pick tenant config by node
|
||||
s.mu.RLock()
|
||||
newNode := s.nodes[nodeName]
|
||||
s.mu.RUnlock()
|
||||
if newNode == nil {
|
||||
return
|
||||
}
|
||||
cfg := s.sdwan.getTenant(newNode.TenantID)
|
||||
if cfg.GatewayCIDR == "" {
|
||||
return
|
||||
}
|
||||
@@ -64,7 +97,7 @@ func (s *Server) announceSDWANNodeOnline(nodeName string) {
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
newNode := s.nodes[nodeName]
|
||||
newNode = s.nodes[nodeName]
|
||||
if newNode == nil || !newNode.IsOnline() {
|
||||
s.mu.RUnlock()
|
||||
return
|
||||
@@ -74,7 +107,7 @@ func (s *Server) announceSDWANNodeOnline(nodeName string) {
|
||||
continue
|
||||
}
|
||||
other := s.nodes[n.Node]
|
||||
if other == nil || !other.IsOnline() {
|
||||
if other == nil || !other.IsOnline() || other.TenantID != newNode.TenantID {
|
||||
continue
|
||||
}
|
||||
// existing -> new
|
||||
@@ -86,7 +119,13 @@ func (s *Server) announceSDWANNodeOnline(nodeName string) {
|
||||
}
|
||||
|
||||
func (s *Server) announceSDWANNodeOffline(nodeName string) {
|
||||
cfg := s.sdwan.get()
|
||||
s.mu.RLock()
|
||||
old := s.nodes[nodeName]
|
||||
s.mu.RUnlock()
|
||||
if old == nil {
|
||||
return
|
||||
}
|
||||
cfg := s.sdwan.getTenant(old.TenantID)
|
||||
if cfg.GatewayCIDR == "" {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +139,7 @@ func (s *Server) announceSDWANNodeOffline(nodeName string) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, n := range s.nodes {
|
||||
if n.Name == nodeName || !n.IsOnline() {
|
||||
if n.Name == nodeName || !n.IsOnline() || n.TenantID != old.TenantID {
|
||||
continue
|
||||
}
|
||||
s.pushSDWANDel(n, protocol.SDWANPeer{Node: nodeName, IP: selfIP, Online: false})
|
||||
@@ -112,7 +151,13 @@ func (s *Server) RouteSDWANPacket(from *NodeInfo, pkt protocol.SDWANPacket) {
|
||||
if from == nil {
|
||||
return
|
||||
}
|
||||
cfg := s.sdwan.get()
|
||||
// Use global config for untrusted nodes (TenantID=0), otherwise use tenant config
|
||||
var cfg protocol.SDWANConfig
|
||||
if from.TenantID == 0 {
|
||||
cfg = s.sdwan.get()
|
||||
} else {
|
||||
cfg = s.sdwan.getTenant(from.TenantID)
|
||||
}
|
||||
if cfg.GatewayCIDR == "" || pkt.DstIP == "" || len(pkt.Payload) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -124,12 +169,18 @@ func (s *Server) RouteSDWANPacket(from *NodeInfo, pkt protocol.SDWANPacket) {
|
||||
toNode := ""
|
||||
for _, n := range cfg.Nodes {
|
||||
if n.IP == pkt.DstIP {
|
||||
toNode = n.Node
|
||||
break
|
||||
candidate := s.GetNodeForUser(n.Node, from.Token)
|
||||
if candidate != nil && candidate.TenantID == from.TenantID {
|
||||
toNode = n.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
if p, err := netip.ParseAddr(n.IP); err == nil && p == dst {
|
||||
toNode = n.Node
|
||||
break
|
||||
candidate := s.GetNodeForUser(n.Node, from.Token)
|
||||
if candidate != nil && candidate.TenantID == from.TenantID {
|
||||
toNode = n.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if toNode == "" || toNode == from.Name {
|
||||
@@ -138,6 +189,9 @@ func (s *Server) RouteSDWANPacket(from *NodeInfo, pkt protocol.SDWANPacket) {
|
||||
|
||||
s.mu.RLock()
|
||||
to := s.nodes[toNode]
|
||||
if to != nil && to.TenantID != from.TenantID {
|
||||
to = nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
if to == nil || !to.IsOnline() {
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
"github.com/openp2p-cn/inp2p/internal/store"
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
"github.com/openp2p-cn/inp2p/pkg/signal"
|
||||
@@ -17,26 +19,27 @@ import (
|
||||
|
||||
// NodeInfo represents a connected client node.
|
||||
type NodeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Token uint64 `json:"-"`
|
||||
User string `json:"user"`
|
||||
Version string `json:"version"`
|
||||
NATType protocol.NATType `json:"natType"`
|
||||
PublicIP string `json:"publicIP"`
|
||||
PublicPort int `json:"publicPort"`
|
||||
LanIP string `json:"lanIP"`
|
||||
OS string `json:"os"`
|
||||
Mac string `json:"mac"`
|
||||
ShareBandwidth int `json:"shareBandwidth"`
|
||||
RelayEnabled bool `json:"relayEnabled"`
|
||||
SuperRelay bool `json:"superRelay"`
|
||||
HasIPv4 int `json:"hasIPv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
LoginTime time.Time `json:"loginTime"`
|
||||
LastHeartbeat time.Time `json:"lastHeartbeat"`
|
||||
Conn *signal.Conn `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Token uint64 `json:"-"`
|
||||
TenantID int64 `json:"tenantId"`
|
||||
User string `json:"user"`
|
||||
Version string `json:"version"`
|
||||
NATType protocol.NATType `json:"natType"`
|
||||
PublicIP string `json:"publicIP"`
|
||||
PublicPort int `json:"publicPort"`
|
||||
LanIP string `json:"lanIP"`
|
||||
OS string `json:"os"`
|
||||
Mac string `json:"mac"`
|
||||
ShareBandwidth int `json:"shareBandwidth"`
|
||||
RelayEnabled bool `json:"relayEnabled"`
|
||||
SuperRelay bool `json:"superRelay"`
|
||||
HasIPv4 int `json:"hasIPv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
LoginTime time.Time `json:"loginTime"`
|
||||
LastHeartbeat time.Time `json:"lastHeartbeat"`
|
||||
Conn *signal.Conn `json:"-"`
|
||||
Apps []protocol.AppConfig `json:"apps"`
|
||||
mu sync.RWMutex `json:"-"`
|
||||
mu sync.RWMutex `json:"-"`
|
||||
}
|
||||
|
||||
// IsOnline checks if node has sent heartbeat recently.
|
||||
@@ -49,25 +52,43 @@ func (n *NodeInfo) IsOnline() bool {
|
||||
// Server is the INP2P signaling server.
|
||||
type Server struct {
|
||||
cfg config.ServerConfig
|
||||
nodes map[string]*NodeInfo // node name → info
|
||||
nodes map[string]*NodeInfo
|
||||
mu sync.RWMutex
|
||||
upgrader websocket.Upgrader
|
||||
quit chan struct{}
|
||||
sdwanPath string
|
||||
sdwan *sdwanStore
|
||||
store *store.Store
|
||||
tokens map[uint64]bool
|
||||
}
|
||||
|
||||
func (s *Server) Store() *store.Store { return s.store }
|
||||
|
||||
// New creates a new server.
|
||||
func New(cfg config.ServerConfig) *Server {
|
||||
// Use absolute path for sdwan config to avoid working directory issues
|
||||
sdwanPath := "/root/.openclaw/workspace/inp2p/sdwan.json"
|
||||
tokens := make(map[uint64]bool)
|
||||
if cfg.Token != 0 {
|
||||
tokens[cfg.Token] = true
|
||||
}
|
||||
for _, t := range cfg.Tokens {
|
||||
tokens[t] = true
|
||||
}
|
||||
st, err := store.Open(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Printf("[server] open store failed: %v", err)
|
||||
}
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
nodes: make(map[string]*NodeInfo),
|
||||
sdwanPath: sdwanPath,
|
||||
sdwan: newSDWANStore(sdwanPath),
|
||||
store: st,
|
||||
tokens: tokens,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
},
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
@@ -93,6 +114,42 @@ func (s *Server) GetOnlineNodes() []*NodeInfo {
|
||||
return out
|
||||
}
|
||||
|
||||
// GetNodeForUser returns node if token matches (legacy) or tenant matches.
|
||||
func (s *Server) GetNodeForUser(name string, token uint64) *NodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
n := s.nodes[name]
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if n.Token != token && n.TenantID == 0 {
|
||||
return nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Server) GetNodeForTenant(name string, tenantID int64) *NodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
n := s.nodes[name]
|
||||
if n == nil || n.TenantID != tenantID {
|
||||
return nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Server) GetOnlineNodesByTenant(tenantID int64) []*NodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var out []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if n.IsOnline() && n.TenantID == tenantID {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetRelayNodes returns nodes that can serve as relay.
|
||||
// Priority: same-user private relay → super relay
|
||||
func (s *Server) GetRelayNodes(forUser string, excludeNodes ...string) []*NodeInfo {
|
||||
@@ -119,6 +176,28 @@ func (s *Server) GetRelayNodes(forUser string, excludeNodes ...string) []*NodeIn
|
||||
return append(privateRelays, superRelays...)
|
||||
}
|
||||
|
||||
// GetRelayNodesByTenant returns relay nodes within tenant.
|
||||
func (s *Server) GetRelayNodesByTenant(tenantID int64, excludeNodes ...string) []*NodeInfo {
|
||||
excludeSet := make(map[string]bool)
|
||||
for _, n := range excludeNodes {
|
||||
excludeSet[n] = true
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var relays []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if !n.IsOnline() || excludeSet[n.Name] {
|
||||
continue
|
||||
}
|
||||
if n.TenantID == tenantID && (n.RelayEnabled || n.SuperRelay) {
|
||||
relays = append(relays, n)
|
||||
}
|
||||
}
|
||||
return relays
|
||||
}
|
||||
|
||||
// HandleWS is the WebSocket handler for client connections.
|
||||
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := s.upgrader.Upgrade(w, r, nil)
|
||||
@@ -151,8 +230,26 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify token
|
||||
if loginReq.Token != s.cfg.Token {
|
||||
// Verify token: master token OR tenant API key (DB) OR node_secret (DB)
|
||||
valid := s.tokens[loginReq.Token]
|
||||
log.Printf("[server] login check: token=%d, cfg.Token=%d, valid=%v", loginReq.Token, s.cfg.Token, valid)
|
||||
var tenantID int64
|
||||
if !valid && s.store != nil {
|
||||
// try api key (string) or node secret
|
||||
if loginReq.NodeSecret != "" {
|
||||
if ten, err := s.store.VerifyNodeSecret(loginReq.Node, loginReq.NodeSecret); err == nil && ten != nil {
|
||||
valid = true
|
||||
tenantID = ten.ID
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
if ten, err := s.store.VerifyAPIKey(fmt.Sprintf("%d", loginReq.Token)); err == nil && ten != nil {
|
||||
valid = true
|
||||
tenantID = ten.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
log.Printf("[server] login denied: %s (token mismatch)", loginReq.Node)
|
||||
conn.Write(protocol.MsgLogin, protocol.SubLoginRsp, protocol.LoginRsp{
|
||||
Error: 1,
|
||||
@@ -174,6 +271,7 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
node := &NodeInfo{
|
||||
Name: loginReq.Node,
|
||||
Token: loginReq.Token,
|
||||
TenantID: tenantID,
|
||||
User: loginReq.User,
|
||||
Version: loginReq.Version,
|
||||
NATType: loginReq.NATType,
|
||||
@@ -211,11 +309,21 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
s.broadcastNodeOnline(loginReq.Node)
|
||||
|
||||
// Push current SDWAN config right after login (if exists and enabled)
|
||||
if cfg := s.sdwan.get(); cfg.Enabled && cfg.GatewayCIDR != "" {
|
||||
if err := conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg); err != nil {
|
||||
log.Printf("[server] sdwan config push failed: %v", err)
|
||||
} else {
|
||||
log.Printf("[server] sdwan config pushed to %s", loginReq.Node)
|
||||
if node.TenantID > 0 {
|
||||
if cfg := s.sdwan.getTenant(node.TenantID); cfg.Enabled && cfg.GatewayCIDR != "" {
|
||||
if err := conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg); err != nil {
|
||||
log.Printf("[server] sdwan config push failed: %v", err)
|
||||
} else {
|
||||
log.Printf("[server] sdwan config pushed to %s", loginReq.Node)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if cfg := s.sdwan.get(); cfg.Enabled && cfg.GatewayCIDR != "" {
|
||||
if err := conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg); err != nil {
|
||||
log.Printf("[server] sdwan config push failed: %v", err)
|
||||
} else {
|
||||
log.Printf("[server] sdwan config pushed to %s", loginReq.Node)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Event-driven SDWAN peer notification
|
||||
@@ -378,10 +486,13 @@ func (s *Server) handleRelayNodeReq(conn *signal.Conn, requester *NodeInfo, req
|
||||
|
||||
// PushConnect sends a punch coordination message to a peer node.
|
||||
func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol.AppConfig) error {
|
||||
toNode := s.GetNode(toNodeName)
|
||||
toNode := s.GetNodeForUser(toNodeName, fromNode.Token)
|
||||
if toNode == nil || !toNode.IsOnline() {
|
||||
return &NodeOfflineError{Node: toNodeName}
|
||||
}
|
||||
if fromNode.TenantID != 0 && toNode.TenantID != fromNode.TenantID {
|
||||
return &NodeOfflineError{Node: toNodeName}
|
||||
}
|
||||
|
||||
// Push connect request to the destination
|
||||
req := protocol.ConnectReq{
|
||||
@@ -392,6 +503,7 @@ func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol
|
||||
IP: fromNode.PublicIP,
|
||||
NATType: fromNode.NATType,
|
||||
HasIPv4: fromNode.HasIPv4,
|
||||
Token: auth.GenTOTP(fromNode.Token, time.Now().Unix()),
|
||||
},
|
||||
AppName: app.AppName,
|
||||
Protocol: app.Protocol,
|
||||
@@ -406,12 +518,19 @@ func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol
|
||||
// broadcastNodeOnline notifies interested nodes that a peer came online.
|
||||
func (s *Server) broadcastNodeOnline(nodeName string) {
|
||||
s.mu.RLock()
|
||||
newNode := s.nodes[nodeName]
|
||||
defer s.mu.RUnlock()
|
||||
if newNode == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, n := range s.nodes {
|
||||
if n.Name == nodeName {
|
||||
continue
|
||||
}
|
||||
if n.Token != newNode.Token && (newNode.TenantID == 0 || n.TenantID != newNode.TenantID) {
|
||||
continue
|
||||
}
|
||||
// Check if this node has any app targeting the new node
|
||||
n.mu.RLock()
|
||||
interested := false
|
||||
|
||||
185
internal/server/tenant_api.go
Normal file
185
internal/server/tenant_api.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/internal/store"
|
||||
)
|
||||
|
||||
// helpers
|
||||
func BearerToken(r *http.Request) string {
|
||||
h := r.Header.Get("Authorization")
|
||||
if h == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(h, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
if strings.ToLower(parts[0]) != "bearer" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
io.WriteString(w, body)
|
||||
}
|
||||
|
||||
func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
ten, err := s.store.CreateTenant(req.Name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create tenant failed"}`)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Tenant int64 `json:"tenant_id"`
|
||||
Subnet string `json:"subnet"`
|
||||
}{0, "ok", ten.ID, ten.Subnet}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
// /api/v1/admin/tenants/{id}/keys
|
||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||
if len(parts) < 6 || parts[5] != "keys" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
// parts: api v1 admin tenants {id} keys
|
||||
idPart := parts[4]
|
||||
var tenantID int64
|
||||
_, _ = fmt.Sscanf(idPart, "%d", &tenantID)
|
||||
if tenantID == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Scope string `json:"scope"`
|
||||
TTL int64 `json:"ttl"` // seconds
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
var ttl time.Duration
|
||||
if req.TTL > 0 {
|
||||
ttl = time.Duration(req.TTL) * time.Second
|
||||
}
|
||||
key, err := s.store.CreateAPIKey(tenantID, req.Scope, ttl)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create key failed"}`)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
APIKey string `json:"api_key"`
|
||||
Tenant int64 `json:"tenant_id"`
|
||||
}{0, "ok", key, tenantID}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
func (s *Server) HandleTenantEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
// tenant auth by API key
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
tok := BearerToken(r)
|
||||
ten, err := s.store.VerifyAPIKey(tok)
|
||||
if err != nil || ten == nil {
|
||||
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
code, err := s.store.CreateEnrollToken(ten.ID, 10*time.Minute, 5)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create enroll failed"}`)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Code string `json:"enroll_code"`
|
||||
Tenant int64 `json:"tenant_id"`
|
||||
}{0, "ok", code, ten.ID}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Code string `json:"code"`
|
||||
NodeName string `json:"node"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Code == "" || req.NodeName == "" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
et, err := s.store.ConsumeEnrollToken(req.Code)
|
||||
if err != nil {
|
||||
s.store.IncEnrollAttempt(req.Code)
|
||||
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"invalid enroll"}`)
|
||||
return
|
||||
}
|
||||
cred, err := s.store.CreateNodeCredential(et.TenantID, req.NodeName)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create node failed"}`)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
NodeID int64 `json:"node_id"`
|
||||
Secret string `json:"node_secret"`
|
||||
Tenant int64 `json:"tenant_id"`
|
||||
}{0, "ok", cred.NodeID, cred.Secret, cred.TenantID}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
// placeholder to avoid unused import
|
||||
var _ = store.Tenant{}
|
||||
Reference in New Issue
Block a user