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
+5 -5
View File
@@ -4,13 +4,13 @@ import (
"sync"
"sync/atomic"
"github.com/cedar2025/xboard-node/internal/panel"
"github.com/cedar2025/xboard-node/internal/model"
)
// Limiter enforces per-user device limits and detects removed users.
type Limiter struct {
mu sync.RWMutex
users map[int]panel.User
users map[int]model.UserSpec
// uuidDeviceLimit provides O(1) UUID→device-limit lookup for the
// kernel gate-keeping hot path (called per new connection).
@@ -21,18 +21,18 @@ type Limiter struct {
func New() *Limiter {
return &Limiter{
users: make(map[int]panel.User),
users: make(map[int]model.UserSpec),
uuidDeviceLimit: make(map[string]int),
}
}
// UpdateUsers refreshes the user limit configuration and returns
// the IDs of users that were present before but are now removed.
func (l *Limiter) UpdateUsers(users []panel.User) []int {
func (l *Limiter) UpdateUsers(users []model.UserSpec) []int {
l.mu.Lock()
defer l.mu.Unlock()
newUsers := make(map[int]panel.User, len(users))
newUsers := make(map[int]model.UserSpec, len(users))
for _, u := range users {
newUsers[u.ID] = u
}
+11 -11
View File
@@ -4,7 +4,7 @@ import (
"sort"
"testing"
"github.com/cedar2025/xboard-node/internal/panel"
"github.com/cedar2025/xboard-node/internal/model"
)
func TestNew(t *testing.T) {
@@ -16,7 +16,7 @@ func TestNew(t *testing.T) {
func TestUpdateUsers(t *testing.T) {
l := New()
users := []panel.User{
users := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 3, DeviceLimit: 2},
{ID: 2, UUID: "u2", SpeedLimit: 0, DeviceLimit: 0},
}
@@ -29,14 +29,14 @@ func TestUpdateUsers(t *testing.T) {
func TestUpdateUsers_DetectsRemoved(t *testing.T) {
l := New()
l.UpdateUsers([]panel.User{
l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1"},
{ID: 2, UUID: "u2"},
{ID: 3, UUID: "u3"},
})
// Remove user 2
removed := l.UpdateUsers([]panel.User{
removed := l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1"},
{ID: 3, UUID: "u3"},
})
@@ -48,14 +48,14 @@ func TestUpdateUsers_DetectsRemoved(t *testing.T) {
func TestUpdateUsers_DetectsMultipleRemoved(t *testing.T) {
l := New()
l.UpdateUsers([]panel.User{
l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1"},
{ID: 2, UUID: "u2"},
{ID: 3, UUID: "u3"},
})
// Remove users 1 and 3
removed := l.UpdateUsers([]panel.User{
removed := l.UpdateUsers([]model.UserSpec{
{ID: 2, UUID: "u2"},
})
@@ -67,12 +67,12 @@ func TestUpdateUsers_DetectsMultipleRemoved(t *testing.T) {
func TestUpdateUsers_NoRemovals(t *testing.T) {
l := New()
l.UpdateUsers([]panel.User{
l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1"},
})
// Add user, keep existing
removed := l.UpdateUsers([]panel.User{
removed := l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1"},
{ID: 2, UUID: "u2"},
})
@@ -84,10 +84,10 @@ func TestUpdateUsers_NoRemovals(t *testing.T) {
func TestUpdateUsers_ReplacesAll(t *testing.T) {
l := New()
l.UpdateUsers([]panel.User{{ID: 1, DeviceLimit: 1}})
l.UpdateUsers([]model.UserSpec{{ID: 1, DeviceLimit: 1}})
// Replace with different users
removed := l.UpdateUsers([]panel.User{{ID: 2, DeviceLimit: 1}})
removed := l.UpdateUsers([]model.UserSpec{{ID: 2, DeviceLimit: 1}})
if len(removed) != 1 || removed[0] != 1 {
t.Errorf("expected removed=[1], got %v", removed)
@@ -96,7 +96,7 @@ func TestUpdateUsers_ReplacesAll(t *testing.T) {
func TestGetDeviceLimitByUUID(t *testing.T) {
l := New()
l.UpdateUsers([]panel.User{
l.UpdateUsers([]model.UserSpec{
{ID: 1, UUID: "u1", DeviceLimit: 3},
{ID: 2, UUID: "u2", DeviceLimit: 0},
})
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"sync"
"sync/atomic"
"github.com/cedar2025/xboard-node/internal/panel"
"github.com/cedar2025/xboard-node/internal/model"
"golang.org/x/time/rate"
)
@@ -45,7 +45,7 @@ func (t *SpeedTracker) SetLogCallback(f SpeedTrackerLogCallback) {
// UpdateBuckets updates the UUID→userID mapping and syncs existing limiters.
func (t *SpeedTracker) UpdateBuckets() {
currentUsers := make([]panel.User, 0, 32)
currentUsers := make([]model.UserSpec, 0, 32)
t.limiter.mu.RLock()
for _, u := range t.limiter.users {
currentUsers = append(currentUsers, u)
+7 -7
View File
@@ -4,14 +4,14 @@ import (
"testing"
"time"
"github.com/cedar2025/xboard-node/internal/panel"
"github.com/cedar2025/xboard-node/internal/model"
)
func TestSpeedTracker_UpdateBuckets(t *testing.T) {
l := New()
st := NewSpeedTracker(l)
users := []panel.User{
users := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 3, DeviceLimit: 0}, // 3 Mbps
{ID: 2, UUID: "u2", SpeedLimit: 0, DeviceLimit: 0}, // unlimited
{ID: 3, UUID: "u3", SpeedLimit: 10, DeviceLimit: 2}, // 10 Mbps
@@ -60,7 +60,7 @@ func TestSpeedTracker_UpdateBuckets_PreservesExisting(t *testing.T) {
l := New()
st := NewSpeedTracker(l)
users := []panel.User{
users := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 3, DeviceLimit: 0},
}
l.UpdateUsers(users)
@@ -72,7 +72,7 @@ func TestSpeedTracker_UpdateBuckets_PreservesExisting(t *testing.T) {
}
// Update same user with different speed — should reuse same *rate.Limiter pointer
users2 := []panel.User{
users2 := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 5, DeviceLimit: 0},
}
l.UpdateUsers(users2)
@@ -97,7 +97,7 @@ func TestSpeedTracker_UpdateBuckets_RemovesUsers(t *testing.T) {
l := New()
st := NewSpeedTracker(l)
users := []panel.User{
users := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 3, DeviceLimit: 0},
{ID: 2, UUID: "u2", SpeedLimit: 5, DeviceLimit: 0},
}
@@ -109,7 +109,7 @@ func TestSpeedTracker_UpdateBuckets_RemovesUsers(t *testing.T) {
}
// Remove user 2
users2 := []panel.User{
users2 := []model.UserSpec{
{ID: 1, UUID: "u1", SpeedLimit: 3, DeviceLimit: 0},
}
l.UpdateUsers(users2)
@@ -128,7 +128,7 @@ func TestSpeedTracker_UpdateBuckets_RemovesUsers(t *testing.T) {
func TestSpeedTracker_UpdateBuckets_LogCallbackMayCallLimitedUserCount(t *testing.T) {
l := New()
st := NewSpeedTracker(l)
l.UpdateUsers([]panel.User{{ID: 1, UUID: "u1", SpeedLimit: 1}})
l.UpdateUsers([]model.UserSpec{{ID: 1, UUID: "u1", SpeedLimit: 1}})
st.SetLogCallback(func(msg string) {
_ = st.LimitedUserCount()
})