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)
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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`

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.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 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">
</div>
<div class="flex gap-2">
<button class="btn2" @click="autoAssignIPs">自动分配 IP</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>
@@ -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([]);