From e96a2e5dd9dff50be574dc7ef42b6aaa8f9da229 Mon Sep 17 00:00:00 2001 From: openclaw Date: Thu, 5 Mar 2026 22:03:26 +0800 Subject: [PATCH] sdwan: add hub node selection and auto fallback to mesh --- cmd/inp2ps/main.go | 4 ++++ internal/server/sdwan_api.go | 10 +++++++++ internal/server/server.go | 41 +++++++++++++++++++++++++++++++++--- pkg/protocol/protocol.go | 3 ++- web/index.html | 7 +++++- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/cmd/inp2ps/main.go b/cmd/inp2ps/main.go index d853cae..7c7542a 100644 --- a/cmd/inp2ps/main.go +++ b/cmd/inp2ps/main.go @@ -343,6 +343,10 @@ func main() { http.Error(w, err.Error(), http.StatusBadRequest) return } + if req.Mode == "hub" && req.HubNode == "" { + http.Error(w, "hub mode requires hubNode", http.StatusBadRequest) + return + } // tenant filter by session/apikey tenantID := getTenantID(r) if tenantID > 0 { diff --git a/internal/server/sdwan_api.go b/internal/server/sdwan_api.go index bf92270..f47970d 100644 --- a/internal/server/sdwan_api.go +++ b/internal/server/sdwan_api.go @@ -1,6 +1,7 @@ package server import ( + "errors" "log" "net/netip" @@ -24,6 +25,15 @@ func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error { } func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig) error { + if cfg.Mode == "hub" { + if cfg.HubNode == "" { + return errors.New("hub mode requires hubNode") + } + hub := s.GetNode(cfg.HubNode) + if hub == nil || !hub.IsOnline() || hub.TenantID != tenantID || !hub.RelayEnabled { + return errors.New("hub node must be online and relay-enabled") + } + } if err := s.sdwan.saveTenant(tenantID, cfg); err != nil { return err } diff --git a/internal/server/server.go b/internal/server/server.go index 19b0abc..f57ce03 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,8 +10,8 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/openp2p-cn/inp2p/pkg/auth" "github.com/openp2p-cn/inp2p/internal/store" + "github.com/openp2p-cn/inp2p/pkg/auth" "github.com/openp2p-cn/inp2p/pkg/config" "github.com/openp2p-cn/inp2p/pkg/protocol" "github.com/openp2p-cn/inp2p/pkg/signal" @@ -77,6 +77,15 @@ func New(cfg config.ServerConfig) *Server { st, err := store.Open(cfg.DBPath) if err != nil { log.Printf("[server] open store failed: %v", err) + } else { + // bootstrap default admin/admin in tenant 1 + if _, gErr := st.GetTenantByID(1); gErr != nil { + if _, _, _, cErr := st.CreateTenantWithUsers("default", "admin", "admin"); cErr != nil { + log.Printf("[server] bootstrap default tenant failed: %v", cErr) + } else { + log.Printf("[server] bootstrap default tenant created (tenant=1, admin/admin)") + } + } } return &Server{ cfg: cfg, @@ -86,7 +95,7 @@ func New(cfg config.ServerConfig) *Server { store: st, tokens: tokens, upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 4096, WriteBufferSize: 4096, }, @@ -550,7 +559,7 @@ func (s *Server) broadcastNodeOnline(nodeName string) { } } -// StartCleanup periodically removes stale nodes. +// StartCleanup periodically removes stale nodes and checks SDWAN hub health. func (s *Server) StartCleanup() { go func() { ticker := time.NewTicker(30 * time.Second) @@ -567,6 +576,32 @@ func (s *Server) StartCleanup() { } } s.mu.Unlock() + + // hub offline -> auto mesh (tenant configs) + if s.sdwan != nil { + sd := s.sdwan + sd.mu.RLock() + m := make(map[int64]protocol.SDWANConfig, len(sd.multi)) + for k, v := range sd.multi { + m[k] = v + } + sd.mu.RUnlock() + for tid, cfg := range m { + if cfg.Mode != "hub" || cfg.HubNode == "" { + continue + } + hub := s.GetNode(cfg.HubNode) + if hub != nil && hub.IsOnline() && hub.TenantID == tid { + continue + } + // auto fallback to mesh + cfg.Mode = "mesh" + cfg.HubNode = "" + _ = s.sdwan.saveTenant(tid, cfg) + s.broadcastSDWANTenant(tid, cfg) + log.Printf("[sdwan] hub offline, auto fallback to mesh (tenant=%d)", tid) + } + } case <-s.quit: return } diff --git a/pkg/protocol/protocol.go b/pkg/protocol/protocol.go index f80e94b..41e3651 100644 --- a/pkg/protocol/protocol.go +++ b/pkg/protocol/protocol.go @@ -297,7 +297,8 @@ type SDWANConfig struct { 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 + HubNode string `json:"hubNode,omitempty"` + 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"` diff --git a/web/index.html b/web/index.html index 5e9527c..402e7b9 100644 --- a/web/index.html +++ b/web/index.html @@ -124,11 +124,16 @@ +
+
Hub 离线将自动回 Mesh
@@ -286,7 +291,7 @@ createApp({ const refreshSec = ref(15), timer = ref(null); const health = ref({}), stats = ref({}), nodes = ref([]), nodeKeyword = ref(''); - const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', mtu:1420, nodes:[], routes:['10.10.0.0/24'] }); + const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', hubNode:'', mtu:1420, nodes:[], routes:['10.10.0.0/24'] }); const connectForm = ref({ from:'', to:'', srcPort:80, dstPort:80, appName:'manual-connect' }); const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]);