sdwan: add hub node selection and auto fallback to mesh

This commit is contained in:
2026-03-05 22:03:26 +08:00
parent 5fe5c76375
commit e96a2e5dd9
5 changed files with 60 additions and 5 deletions

View File

@@ -343,6 +343,10 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if req.Mode == "hub" && req.HubNode == "" {
http.Error(w, "hub mode requires hubNode", http.StatusBadRequest)
return
}
// tenant filter by session/apikey // tenant filter by session/apikey
tenantID := getTenantID(r) tenantID := getTenantID(r)
if tenantID > 0 { if tenantID > 0 {

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"errors"
"log" "log"
"net/netip" "net/netip"
@@ -24,6 +25,15 @@ func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error {
} }
func (s *Server) SetSDWANTenant(tenantID int64, 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 { if err := s.sdwan.saveTenant(tenantID, cfg); err != nil {
return err return err
} }

View File

@@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/openp2p-cn/inp2p/pkg/auth"
"github.com/openp2p-cn/inp2p/internal/store" "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/config"
"github.com/openp2p-cn/inp2p/pkg/protocol" "github.com/openp2p-cn/inp2p/pkg/protocol"
"github.com/openp2p-cn/inp2p/pkg/signal" "github.com/openp2p-cn/inp2p/pkg/signal"
@@ -77,6 +77,15 @@ func New(cfg config.ServerConfig) *Server {
st, err := store.Open(cfg.DBPath) st, err := store.Open(cfg.DBPath)
if err != nil { if err != nil {
log.Printf("[server] open store failed: %v", err) 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{ return &Server{
cfg: cfg, cfg: cfg,
@@ -86,7 +95,7 @@ func New(cfg config.ServerConfig) *Server {
store: st, store: st,
tokens: tokens, tokens: tokens,
upgrader: websocket.Upgrader{ upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, CheckOrigin: func(r *http.Request) bool { return true },
ReadBufferSize: 4096, ReadBufferSize: 4096,
WriteBufferSize: 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() { func (s *Server) StartCleanup() {
go func() { go func() {
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
@@ -567,6 +576,32 @@ func (s *Server) StartCleanup() {
} }
} }
s.mu.Unlock() 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: case <-s.quit:
return return
} }

View File

@@ -297,7 +297,8 @@ type SDWANConfig struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
GatewayCIDR string `json:"gatewayCIDR"` GatewayCIDR string `json:"gatewayCIDR"`
Mode string `json:"mode,omitempty"` // hub | mesh | fullmesh 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"` MTU int `json:"mtu,omitempty"`
Routes []string `json:"routes,omitempty"` Routes []string `json:"routes,omitempty"`
Nodes []SDWANNode `json:"nodes"` Nodes []SDWANNode `json:"nodes"`

View File

@@ -124,11 +124,16 @@
<input class="ipt max-w-xs" v-model="sd.name" placeholder="名称"> <input class="ipt max-w-xs" v-model="sd.name" placeholder="名称">
<input class="ipt max-w-xs" v-model="sd.gatewayCIDR" placeholder="网段,如 10.10.0.0/24"> <input class="ipt max-w-xs" v-model="sd.gatewayCIDR" placeholder="网段,如 10.10.0.0/24">
<select class="ipt max-w-[140px]" v-model="sd.mode"><option value="mesh">mesh</option><option value="hub">hub</option></select> <select class="ipt max-w-[140px]" v-model="sd.mode"><option value="mesh">mesh</option><option value="hub">hub</option></select>
<select v-if="sd.mode==='hub'" class="ipt max-w-[220px]" v-model="sd.hubNode">
<option value="">选择 Hub 节点</option>
<option v-for="n in nodes" :key="'hub'+n.name" :value="n.name">{{ n.alias || n.name }}</option>
</select>
<input class="ipt max-w-[120px]" type="number" min="1200" max="9000" v-model.number="sd.mtu" placeholder="MTU"> <input class="ipt max-w-[120px]" type="number" min="1200" max="9000" v-model.number="sd.mtu" placeholder="MTU">
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button class="btn2" @click="autoAssignIPs">自动分配 IP</button> <button class="btn2" @click="autoAssignIPs">自动分配 IP</button>
<button class="btn" :disabled="busy" @click="saveSDWAN">保存 SDWAN</button> <button class="btn" :disabled="busy" @click="saveSDWAN">保存 SDWAN</button>
<div v-if="sd.mode==='hub'" class="text-xs text-slate-400">Hub 离线将自动回 Mesh</div>
</div> </div>
</div> </div>
@@ -286,7 +291,7 @@ createApp({
const refreshSec = ref(15), timer = ref(null); const refreshSec = ref(15), timer = ref(null);
const health = ref({}), stats = ref({}), nodes = ref([]), nodeKeyword = ref(''); 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 connectForm = ref({ from:'', to:'', srcPort:80, dstPort:80, appName:'manual-connect' });
const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]); const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]);