262 lines
6.1 KiB
Go
262 lines
6.1 KiB
Go
package controlplane
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/cedar2025/xboard-node/internal/model"
|
|
)
|
|
|
|
// MailboxState is the coalesced snapshot drained from a NodeMailbox.
|
|
type MailboxState struct {
|
|
Config *model.NodeSpec
|
|
Users []model.UserSpec
|
|
DeviceUsers map[int][]string
|
|
HasConfig bool
|
|
HasUsers bool
|
|
HasDevices bool
|
|
NeedsReconcile bool
|
|
}
|
|
|
|
// NodeMailbox buffers WS events for a machine-mode node service that is not
|
|
// yet ready to process them. Events are coalesced (latest-wins) and drained
|
|
// as a single snapshot once the service marks the mailbox as ready.
|
|
type NodeMailbox struct {
|
|
mu sync.Mutex
|
|
ready bool
|
|
config *model.NodeSpec
|
|
users []model.UserSpec
|
|
deviceUsers map[int][]string
|
|
dirtyConfig bool
|
|
dirtyUsers bool
|
|
dirtyDevices bool
|
|
needsReconcile bool
|
|
hasFullUserState bool
|
|
notifyCh chan struct{}
|
|
}
|
|
|
|
func NewNodeMailbox() *NodeMailbox {
|
|
return &NodeMailbox{notifyCh: make(chan struct{}, 1)}
|
|
}
|
|
|
|
func (m *NodeMailbox) MarkReady() {
|
|
m.mu.Lock()
|
|
m.ready = true
|
|
m.mu.Unlock()
|
|
m.notify()
|
|
}
|
|
|
|
// SeedBaseline initializes the mailbox with the REST-derived baseline so that
|
|
// subsequent delta events can be applied incrementally instead of triggering
|
|
// a full REST reconciliation.
|
|
func (m *NodeMailbox) SeedBaseline(users []model.UserSpec, config *model.NodeSpec) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if users != nil && !m.hasFullUserState {
|
|
m.users = cloneUsers(users)
|
|
m.hasFullUserState = true
|
|
}
|
|
if config != nil && m.config == nil {
|
|
m.config = cloneNodeSpec(config)
|
|
}
|
|
}
|
|
|
|
func (m *NodeMailbox) Apply(event Event) {
|
|
m.mu.Lock()
|
|
changed := false
|
|
|
|
switch event.Type {
|
|
case EventSyncConfig:
|
|
if event.Config != nil {
|
|
m.config = cloneNodeSpec(event.Config)
|
|
m.dirtyConfig = true
|
|
changed = true
|
|
}
|
|
case EventSyncUsers:
|
|
if event.Users != nil {
|
|
m.users = cloneUsers(event.Users)
|
|
m.dirtyUsers = true
|
|
m.hasFullUserState = true
|
|
changed = true
|
|
}
|
|
case EventSyncDevices:
|
|
if event.DeviceUsers != nil {
|
|
m.deviceUsers = cloneDeviceUsers(event.DeviceUsers)
|
|
m.dirtyDevices = true
|
|
changed = true
|
|
}
|
|
case EventSyncUserDelta:
|
|
if len(event.DeltaUsers) > 0 {
|
|
if !m.hasFullUserState {
|
|
m.needsReconcile = true
|
|
changed = true
|
|
} else if !applyUserDelta(&m.users, event.DeltaAction, event.DeltaUsers) {
|
|
m.needsReconcile = true
|
|
changed = true
|
|
} else {
|
|
m.dirtyUsers = true
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if changed {
|
|
m.notify()
|
|
}
|
|
}
|
|
|
|
func (m *NodeMailbox) notify() {
|
|
select {
|
|
case m.notifyCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *NodeMailbox) NotifyCh() <-chan struct{} { return m.notifyCh }
|
|
|
|
func (m *NodeMailbox) DrainIfReady() MailboxState {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if !m.ready {
|
|
return MailboxState{}
|
|
}
|
|
state := MailboxState{NeedsReconcile: m.needsReconcile}
|
|
if m.dirtyConfig && m.config != nil {
|
|
state.Config = cloneNodeSpec(m.config)
|
|
state.HasConfig = true
|
|
m.dirtyConfig = false
|
|
}
|
|
if m.dirtyUsers {
|
|
state.Users = cloneUsers(m.users)
|
|
state.HasUsers = true
|
|
m.dirtyUsers = false
|
|
}
|
|
if m.dirtyDevices && m.deviceUsers != nil {
|
|
state.DeviceUsers = cloneDeviceUsers(m.deviceUsers)
|
|
state.HasDevices = true
|
|
m.dirtyDevices = false
|
|
}
|
|
m.needsReconcile = false
|
|
return state
|
|
}
|
|
|
|
func cloneNodeSpec(spec *model.NodeSpec) *model.NodeSpec {
|
|
if spec == nil {
|
|
return nil
|
|
}
|
|
clone := *spec
|
|
if spec.NetworkSettings != nil {
|
|
clone.NetworkSettings = make(map[string]interface{}, len(spec.NetworkSettings))
|
|
for k, v := range spec.NetworkSettings {
|
|
clone.NetworkSettings[k] = v
|
|
}
|
|
}
|
|
if spec.TLSSettings != nil {
|
|
clone.TLSSettings = make(map[string]interface{}, len(spec.TLSSettings))
|
|
for k, v := range spec.TLSSettings {
|
|
clone.TLSSettings[k] = v
|
|
}
|
|
}
|
|
if spec.Routes != nil {
|
|
clone.Routes = append([]model.RouteRule(nil), spec.Routes...)
|
|
}
|
|
if spec.CustomOutbounds != nil {
|
|
clone.CustomOutbounds = append([]model.OutboundConfig(nil), spec.CustomOutbounds...)
|
|
}
|
|
if spec.CustomRoutes != nil {
|
|
clone.CustomRoutes = make([]map[string]any, len(spec.CustomRoutes))
|
|
for i, route := range spec.CustomRoutes {
|
|
if route == nil {
|
|
continue
|
|
}
|
|
copied := make(map[string]any, len(route))
|
|
for k, v := range route {
|
|
copied[k] = v
|
|
}
|
|
clone.CustomRoutes[i] = copied
|
|
}
|
|
}
|
|
if spec.CustomRouteRules != nil {
|
|
clone.CustomRouteRules = append([]model.CustomRouteRule(nil), spec.CustomRouteRules...)
|
|
}
|
|
if spec.CertConfig != nil {
|
|
certCopy := *spec.CertConfig
|
|
if spec.CertConfig.DNSEnv != nil {
|
|
certCopy.DNSEnv = make(map[string]string, len(spec.CertConfig.DNSEnv))
|
|
for k, v := range spec.CertConfig.DNSEnv {
|
|
certCopy.DNSEnv[k] = v
|
|
}
|
|
}
|
|
clone.CertConfig = &certCopy
|
|
}
|
|
if spec.Multiplex != nil {
|
|
muxCopy := *spec.Multiplex
|
|
if spec.Multiplex.Brutal != nil {
|
|
brutalCopy := *spec.Multiplex.Brutal
|
|
muxCopy.Brutal = &brutalCopy
|
|
}
|
|
clone.Multiplex = &muxCopy
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
func cloneUsers(users []model.UserSpec) []model.UserSpec {
|
|
if users == nil {
|
|
return nil
|
|
}
|
|
cloned := make([]model.UserSpec, len(users))
|
|
copy(cloned, users)
|
|
return cloned
|
|
}
|
|
|
|
func cloneDeviceUsers(devices map[int][]string) map[int][]string {
|
|
if devices == nil {
|
|
return nil
|
|
}
|
|
cloned := make(map[int][]string, len(devices))
|
|
for uid, ips := range devices {
|
|
dup := make([]string, len(ips))
|
|
copy(dup, ips)
|
|
cloned[uid] = dup
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func applyUserDelta(users *[]model.UserSpec, action string, delta []model.UserSpec) bool {
|
|
current := cloneUsers(*users)
|
|
index := make(map[int]int, len(current))
|
|
for i, user := range current {
|
|
index[user.ID] = i
|
|
}
|
|
|
|
switch action {
|
|
case "add":
|
|
for _, user := range delta {
|
|
if idx, ok := index[user.ID]; ok {
|
|
current[idx] = user
|
|
continue
|
|
}
|
|
index[user.ID] = len(current)
|
|
current = append(current, user)
|
|
}
|
|
case "remove":
|
|
removeIDs := make(map[int]struct{}, len(delta))
|
|
for _, user := range delta {
|
|
removeIDs[user.ID] = struct{}{}
|
|
}
|
|
filtered := current[:0]
|
|
for _, user := range current {
|
|
if _, ok := removeIDs[user.ID]; ok {
|
|
continue
|
|
}
|
|
filtered = append(filtered, user)
|
|
}
|
|
current = append([]model.UserSpec(nil), filtered...)
|
|
default:
|
|
return false
|
|
}
|
|
|
|
*users = current
|
|
return true
|
|
}
|