feat: add standalone controlplane support

This commit is contained in:
xboard
2026-04-11 05:36:59 +08:00
parent e9672fbdbe
commit 598b6b531b
34 changed files with 2383 additions and 707 deletions
+32 -13
View File
@@ -19,8 +19,8 @@ import (
"github.com/cedar2025/xboard-node/internal/config"
"github.com/cedar2025/xboard-node/internal/kernel"
"github.com/cedar2025/xboard-node/internal/model"
"github.com/cedar2025/xboard-node/internal/nlog"
"github.com/cedar2025/xboard-node/internal/panel"
)
// drainTimeout is how long stop() waits for in-flight connections to finish
@@ -40,8 +40,8 @@ type SingBox struct {
ctx context.Context
cancel context.CancelFunc
users []panel.User
nodeConfig *panel.NodeConfig
users []model.UserSpec
nodeConfig *model.NodeSpec
certFile string
keyFile string
@@ -71,6 +71,17 @@ var _ kernel.Kernel = (*SingBox)(nil)
func (s *SingBox) Name() string { return "sing-box" }
func (s *SingBox) Capabilities() kernel.Capabilities {
return kernel.Capabilities{
PerUserSpeedLimit: true,
DeviceLimit: true,
BuiltInTrafficStats: false,
AliveIPTracking: true,
ForceCloseConnection: false,
ForceCloseUser: true,
}
}
func (s *SingBox) Protocols() []string {
return []string{
"vmess", "vless", "trojan", "shadowsocks",
@@ -78,7 +89,7 @@ func (s *SingBox) Protocols() []string {
}
}
func (s *SingBox) Start(nodeConfig *panel.NodeConfig, users []panel.User, certFile, keyFile string) error {
func (s *SingBox) Start(nodeConfig *model.NodeSpec, users []model.UserSpec, certFile, keyFile string) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -182,7 +193,7 @@ func recycleOldBox(oldBox *box.Box, oldCancel context.CancelFunc, oldCtx context
// Reload hot-swaps the inbound users and routing rules without restarting the box.
// Routes, outbounds, and the connTracker stay alive so in-flight connections
// continue to be tracked correctly.
func (s *SingBox) Reload(nodeConfig *panel.NodeConfig, users []panel.User, certFile, keyFile string) error {
func (s *SingBox) Reload(nodeConfig *model.NodeSpec, users []model.UserSpec, certFile, keyFile string) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -257,6 +268,10 @@ func (s *SingBox) Reload(nodeConfig *panel.NodeConfig, users []panel.User, certF
if opts, ok := inb.Options.(*option.AnyTLSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.MieruUser]:
if opts, ok := inb.Options.(*option.MieruInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[auth.User]:
switch opts := inb.Options.(type) {
case *option.NaiveInboundOptions:
@@ -413,7 +428,7 @@ func (s *SingBox) ClearGlobalDevices() {
// ─── User management (non-disruptive) ───────────────────────────────────────
// AddUsers hot-swaps users into running inbounds. Zero connection disruption.
func (s *SingBox) AddUsers(users []panel.User) (int, error) {
func (s *SingBox) AddUsers(users []model.UserSpec) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -425,7 +440,7 @@ func (s *SingBox) AddUsers(users []panel.User) (int, error) {
for _, u := range s.users {
existing[u.ID] = struct{}{}
}
var toAdd []panel.User
var toAdd []model.UserSpec
for _, u := range users {
if _, dup := existing[u.ID]; !dup {
toAdd = append(toAdd, u)
@@ -435,7 +450,7 @@ func (s *SingBox) AddUsers(users []panel.User) (int, error) {
return 0, nil
}
merged := append(append([]panel.User{}, s.users...), toAdd...)
merged := append(append([]model.UserSpec{}, s.users...), toAdd...)
if err := s.reloadInboundsLocked(merged); err != nil {
return 0, err
}
@@ -445,7 +460,7 @@ func (s *SingBox) AddUsers(users []panel.User) (int, error) {
// RemoveUsers hot-swaps users out of running inbounds. Zero connection disruption
// for remaining users.
func (s *SingBox) RemoveUsers(users []panel.User) (int, error) {
func (s *SingBox) RemoveUsers(users []model.UserSpec) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -457,7 +472,7 @@ func (s *SingBox) RemoveUsers(users []panel.User) (int, error) {
for _, u := range users {
removeSet[u.ID] = struct{}{}
}
var kept []panel.User
var kept []model.UserSpec
removed := 0
for _, u := range s.users {
if _, rm := removeSet[u.ID]; rm {
@@ -478,7 +493,7 @@ func (s *SingBox) RemoveUsers(users []panel.User) (int, error) {
}
// UpdateUsers replaces the entire user set atomically via hot-swap.
func (s *SingBox) UpdateUsers(users []panel.User) (added, removed int, err error) {
func (s *SingBox) UpdateUsers(users []model.UserSpec) (added, removed int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -507,7 +522,7 @@ func (s *SingBox) UpdateUsers(users []panel.User) (added, removed int, err error
// reloadInboundsLocked hot-swaps inbound users using UpdatableInbound.
// Must be called with s.mu held.
func (s *SingBox) reloadInboundsLocked(users []panel.User) error {
func (s *SingBox) reloadInboundsLocked(users []model.UserSpec) error {
cfgMap := buildConfig(s.cfg, s.nodeConfig, users, s.certFile, s.keyFile)
data, err := json.Marshal(cfgMap)
if err != nil {
@@ -564,6 +579,10 @@ func (s *SingBox) reloadInboundsLocked(users []panel.User) error {
if opts, ok := inb.Options.(*option.AnyTLSInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[option.MieruUser]:
if opts, ok := inb.Options.(*option.MieruInboundOptions); ok {
err = v.UpdateUsers(opts.Users)
}
case adapter.UpdatableInbound[auth.User]:
switch opts := inb.Options.(type) {
case *option.NaiveInboundOptions:
@@ -630,7 +649,7 @@ func (s *SingBox) CloseUserConnections(_ context.Context, uuid string) error {
// buildUserMap creates a UUID→userID mapping used by ConnTracker to attribute
// connections to the correct user. All sing-box protocols use the user's UUID
// as the inbound name/username, so this covers every protocol.
func buildUserMap(users []panel.User) map[string]int {
func buildUserMap(users []model.UserSpec) map[string]int {
m := make(map[string]int, len(users))
for _, u := range users {
m[u.UUID] = u.ID