Add admin panel, manager, node_manager, bandwidth limiter, connection limiter, bonding, failover, vless encryption, mkcp transport

This commit is contained in:
Shtorm
2026-02-26 22:44:31 +03:00
parent 0f38dbba4c
commit ded1eb9635
115 changed files with 12582 additions and 301 deletions

View File

@@ -0,0 +1,164 @@
package constant
import "time"
type Squad struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type SquadCreate struct {
Name string `json:"name" validate:"required"`
}
type SquadUpdate struct {
Name string `json:"name" validate:"required"`
}
type Node struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type NodeCreate struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
}
type NodeUpdate struct {
Name string `json:"name" validate:"required"`
}
type BaseNode struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
}
type User struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Type string `json:"type" validate:"required"`
Inbound string `json:"inbound" validate:"required"`
UUID string `json:"uuid" validate:"required"`
Password string `json:"password" validate:"required"`
Flow string `json:"flow" validate:"required"`
AlterID int `json:"alter_id" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type UserCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Type string `json:"type" validate:"required,oneof=hysteria hysteria2 trojan tuic vless vmess"`
Inbound string `json:"inbound" validate:"required"`
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type UserUpdate struct {
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type BaseUser struct {
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type ConnectionLimiter struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"connection_type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type ConnectionLimiterCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type ConnectionLimiterUpdate struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type BaseConnectionLimiter struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type BandwidthLimiter struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
RawSpeed uint64 `json:"raw_speed" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type BandwidthLimiterCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
}
type BandwidthLimiterUpdate struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
}
type BaseBandwidthLimiter struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
RawSpeed uint64 `json:"raw_speed" validate:"required"`
}

View File

@@ -0,0 +1,5 @@
package constant
import E "github.com/sagernet/sing/common/exceptions"
var ErrNotFound = E.New("not found")

View File

@@ -0,0 +1,48 @@
package constant
type NodeManager interface {
AddNode(id string, node ConnectedNode) error
AcquireLock(limiterId int, id string) (string, error)
RefreshLock(limiterId int, id string, handleId string) error
ReleaseLock(limiterId int, id string, handleId string) error
}
type Manager interface {
NodeManager
CreateSquad(user SquadCreate) (Squad, error)
GetSquads(filters map[string][]string) ([]Squad, error)
GetSquadsCount(filters map[string][]string) (int, error)
GetSquad(id int) (Squad, error)
UpdateSquad(id int, user SquadUpdate) (Squad, error)
DeleteSquad(id int) (Squad, error)
CreateNode(node NodeCreate) (Node, error)
GetNodes(filters map[string][]string) ([]Node, error)
GetNodesCount(filters map[string][]string) (int, error)
GetNode(uuid string) (Node, error)
GetNodeStatus(uuid string) string
UpdateNode(uuid string, node NodeUpdate) (Node, error)
DeleteNode(uuid string) (Node, error)
CreateUser(user UserCreate) (User, error)
GetUsers(filters map[string][]string) ([]User, error)
GetUsersCount(filters map[string][]string) (int, error)
GetUser(id int) (User, error)
UpdateUser(id int, user UserUpdate) (User, error)
DeleteUser(id int) (User, error)
CreateBandwidthLimiter(limiter BandwidthLimiterCreate) (BandwidthLimiter, error)
GetBandwidthLimiters(filters map[string][]string) ([]BandwidthLimiter, error)
GetBandwidthLimitersCount(filters map[string][]string) (int, error)
GetBandwidthLimiter(id int) (BandwidthLimiter, error)
UpdateBandwidthLimiter(id int, limiter BandwidthLimiterUpdate) (BandwidthLimiter, error)
DeleteBandwidthLimiter(id int) (BandwidthLimiter, error)
CreateConnectionLimiter(limiter ConnectionLimiterCreate) (ConnectionLimiter, error)
GetConnectionLimiters(filters map[string][]string) ([]ConnectionLimiter, error)
GetConnectionLimitersCount(filters map[string][]string) (int, error)
GetConnectionLimiter(id int) (ConnectionLimiter, error)
UpdateConnectionLimiter(id int, limiter ConnectionLimiterUpdate) (ConnectionLimiter, error)
DeleteConnectionLimiter(id int) (ConnectionLimiter, error)
}

View File

@@ -0,0 +1,20 @@
package constant
type ConnectedNode interface {
UpdateUser(user User)
UpdateUsers(users []User)
DeleteUser(user User)
UpdateConnectionLimiter(limiter ConnectionLimiter)
UpdateConnectionLimiters(limiter []ConnectionLimiter)
DeleteConnectionLimiter(limiter ConnectionLimiter)
UpdateBandwidthLimiter(limiter BandwidthLimiter)
UpdateBandwidthLimiters(limiter []BandwidthLimiter)
DeleteBandwidthLimiter(limiter BandwidthLimiter)
IsLocal() bool
IsOnline() bool
Close() error
}

View File

@@ -0,0 +1,38 @@
package constant
type Repository interface {
CreateSquad(user SquadCreate) (Squad, error)
GetSquads(filters map[string][]string) ([]Squad, error)
GetSquadsCount(filters map[string][]string) (int, error)
GetSquad(id int) (Squad, error)
UpdateSquad(id int, user SquadUpdate) (Squad, error)
DeleteSquad(id int) (Squad, error)
CreateNode(node NodeCreate) (Node, error)
GetNodes(filters map[string][]string) ([]Node, error)
GetNodesCount(filters map[string][]string) (int, error)
GetNode(uuid string) (Node, error)
UpdateNode(uuid string, node NodeUpdate) (Node, error)
DeleteNode(uuid string) (Node, error)
CreateUser(user UserCreate) (User, error)
GetUsers(filters map[string][]string) ([]User, error)
GetUsersCount(filters map[string][]string) (int, error)
GetUser(id int) (User, error)
UpdateUser(id int, user UserUpdate) (User, error)
DeleteUser(id int) (User, error)
CreateConnectionLimiter(limiter ConnectionLimiterCreate) (ConnectionLimiter, error)
GetConnectionLimiters(filters map[string][]string) ([]ConnectionLimiter, error)
GetConnectionLimitersCount(filters map[string][]string) (int, error)
GetConnectionLimiter(id int) (ConnectionLimiter, error)
UpdateConnectionLimiter(id int, limiter ConnectionLimiterUpdate) (ConnectionLimiter, error)
DeleteConnectionLimiter(id int) (ConnectionLimiter, error)
CreateBandwidthLimiter(limiter BandwidthLimiterCreate) (BandwidthLimiter, error)
GetBandwidthLimiters(filters map[string][]string) ([]BandwidthLimiter, error)
GetBandwidthLimitersCount(filters map[string][]string) (int, error)
GetBandwidthLimiter(id int) (BandwidthLimiter, error)
UpdateBandwidthLimiter(id int, limiter BandwidthLimiterUpdate) (BandwidthLimiter, error)
DeleteBandwidthLimiter(id int) (BandwidthLimiter, error)
}

View File

@@ -0,0 +1,155 @@
package postgresql
import (
"encoding/json"
"strconv"
"github.com/huandu/go-sqlbuilder"
"github.com/sagernet/sing/common/byteformats"
)
type Filter func(sb *sqlbuilder.SelectBuilder, value []string) error
func EqualFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.Equal(field, value[0]))
return nil
}
}
func EqualOrNullFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.Or(sb.Equal(field, value[0]), sb.IsNull(field)))
return nil
}
}
func GreaterThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.GreaterThan(field, value[0]))
return nil
}
}
func LessThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.LessThan(field, value[0]))
return nil
}
}
func GreaterEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.GreaterEqualThan(field, value[0]))
return nil
}
}
func LessEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.LessEqualThan(field, value[0]))
return nil
}
}
func SpeedGreaterEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
bytesSpeed, err := json.Marshal(value[0])
if err != nil {
return err
}
speed := &byteformats.NetworkBytesCompat{}
err = speed.UnmarshalJSON(bytesSpeed)
if err != nil {
return err
}
sb.Where(sb.GreaterEqualThan(field, speed.Value()))
return nil
}
}
func SpeedLessEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
bytesSpeed, err := json.Marshal(value[0])
if err != nil {
return err
}
speed := &byteformats.NetworkBytesCompat{}
err = speed.UnmarshalJSON(bytesSpeed)
if err != nil {
return err
}
sb.Where(sb.LessEqualThan(field, speed.Value()))
return nil
}
}
func ExistsAndWhereInFilter(subquery *sqlbuilder.SelectBuilder, field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
values := make([]interface{}, len(value))
for i, v := range value {
values[i] = v
}
subquery.Where(subquery.In(field, values...))
sb.Where(sb.Exists(subquery))
return nil
}
}
func SortAscFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.OrderByAsc(value[0])
return nil
}
}
func SortDescFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.OrderByDesc(value[0])
return nil
}
}
func ReplacedSortAscFilter(replace map[string]string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
if replacedValue, ok := replace[value[0]]; ok {
sb.OrderByAsc(replacedValue)
} else {
sb.OrderByAsc(value[0])
}
return nil
}
}
func ReplacedSortDescFilter(replace map[string]string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
if replacedValue, ok := replace[value[0]]; ok {
sb.OrderByDesc(replacedValue)
} else {
sb.OrderByDesc(value[0])
}
return nil
}
}
func LimitFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
limit, err := strconv.Atoi(value[0])
if err != nil {
return err
}
sb.Limit(limit)
return nil
}
}
func OffsetFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
offset, err := strconv.Atoi(value[0])
if err != nil {
return err
}
sb.Offset(offset)
return nil
}
}

View File

@@ -0,0 +1,132 @@
package postgresql
import (
"database/sql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/sagernet/sing-box/common/migrate/source"
)
var migrations = map[string]string{
"1_initialize_schema.up.sql": `
CREATE TABLE squads (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE nodes (
uuid VARCHAR(36) PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE node_to_squad (
node_uuid VARCHAR(36) NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (node_uuid, squad_id),
FOREIGN KEY (node_uuid) REFERENCES nodes(uuid) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
node_uuid VARCHAR(36),
username TEXT NOT NULL,
type TEXT NOT NULL,
inbound TEXT NOT NULL,
uuid TEXT NOT NULL,
password TEXT NOT NULL,
flow TEXT NOT NULL,
alter_id INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, inbound)
);
CREATE TABLE user_to_squad (
user_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (user_id, squad_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE connection_limiters (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
outbound TEXT NOT NULL,
strategy TEXT NOT NULL,
connection_type TEXT NOT NULL,
lock_type TEXT NOT NULL,
count INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, outbound)
);
CREATE TABLE connection_limiter_to_squad (
connection_limiter_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (connection_limiter_id, squad_id),
FOREIGN KEY (connection_limiter_id) REFERENCES connection_limiters(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE bandwidth_limiters (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
outbound TEXT NOT NULL,
strategy TEXT NOT NULL,
mode TEXT NOT NULL,
connection_type TEXT NOT NULL,
speed TEXT NOT NULL,
raw_speed BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, outbound)
);
CREATE TABLE bandwidth_limiter_to_squad (
bandwidth_limiter_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (bandwidth_limiter_id, squad_id),
FOREIGN KEY (bandwidth_limiter_id) REFERENCES bandwidth_limiters(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
`,
"1_initialize_schema.down.sql": `
DROP TABLE IF EXISTS squas;
DROP TABLE IF EXISTS nodes;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS bandwidth_limiters;
DROP TABLE IF EXISTS connection_limiters;
`,
}
func Migrate(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return err
}
sourceDriver := source.NewRawDriver(migrations)
if err := sourceDriver.Init(); err != nil {
return err
}
m, err := migrate.NewWithInstance(
"raw",
sourceDriver,
"postgres",
driver,
)
if err != nil {
return err
}
return m.Up()
}

File diff suppressed because it is too large Load Diff

598
service/manager/service.go Normal file
View File

@@ -0,0 +1,598 @@
//go:build with_manager
package manager
import (
"context"
"strconv"
"sync"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofrs/uuid/v5"
"github.com/patrickmn/go-cache"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/manager/repository/postgresql"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.ManagerServiceOptions](registry, C.TypeManager, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
repository constant.Repository
nodes map[string]constant.ConnectedNode
limiterLocks map[int]map[string]*cache.Cache
userValidator *validator.Validate
defaultValidator *validator.Validate
mtx sync.RWMutex
connLockMtx sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerServiceOptions) (adapter.Service, error) {
var repository constant.Repository
var err error
switch options.Database.Driver {
case "postgresql":
repository, err = postgresql.NewPostgreSQLRepository(ctx, options.Database.DSN)
if err != nil {
return nil, err
}
default:
return nil, E.New("unknown driver \"", options.Database.Driver, "\"")
}
userValidator := validator.New()
userValidator.RegisterStructValidation(func(sl validator.StructLevel) {
user := sl.Current().Interface().(constant.UserCreate)
switch user.Type {
case "vless":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
case "vmess":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
if user.AlterID == 0 {
sl.ReportError(user.AlterID, "alter_id", "AlterID", "required", "")
}
case "trojan", "shadowsocks", "hysteria", "hysteria2":
if user.Password == "" {
sl.ReportError(user.Password, "password", "Password", "required", "")
}
case "tuic":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
if user.Password == "" {
sl.ReportError(user.Password, "password", "Password", "required", "")
}
}
}, constant.UserCreate{})
return &Service{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
repository: repository,
nodes: make(map[string]constant.ConnectedNode, 0),
limiterLocks: make(map[int]map[string]*cache.Cache),
userValidator: userValidator,
defaultValidator: validator.New(),
}, nil
}
func (s *Service) CreateSquad(node constant.SquadCreate) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Squad{}, err
}
createdSquad, err := s.repository.CreateSquad(node)
if err != nil {
return createdSquad, err
}
return createdSquad, nil
}
func (s *Service) GetSquads(filters map[string][]string) ([]constant.Squad, error) {
return s.repository.GetSquads(filters)
}
func (s *Service) GetSquadsCount(filters map[string][]string) (int, error) {
return s.repository.GetSquadsCount(filters)
}
func (s *Service) GetSquad(id int) (constant.Squad, error) {
return s.repository.GetSquad(id)
}
func (s *Service) UpdateSquad(id int, squad constant.SquadUpdate) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(squad)
if err != nil {
return constant.Squad{}, err
}
updatedSquad, err := s.repository.UpdateSquad(id, squad)
if err != nil {
return updatedSquad, err
}
return updatedSquad, nil
}
func (s *Service) DeleteSquad(id int) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedSquad, err := s.repository.DeleteSquad(id)
if err != nil {
return deletedSquad, err
}
return deletedSquad, nil
}
func (s *Service) CreateNode(node constant.NodeCreate) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Node{}, err
}
createdNode, err := s.repository.CreateNode(node)
if err != nil {
return createdNode, err
}
return createdNode, nil
}
func (s *Service) GetNodes(filters map[string][]string) ([]constant.Node, error) {
return s.repository.GetNodes(filters)
}
func (s *Service) GetNodesCount(filters map[string][]string) (int, error) {
return s.repository.GetNodesCount(filters)
}
func (s *Service) GetNode(uuid string) (constant.Node, error) {
return s.repository.GetNode(uuid)
}
func (s *Service) GetNodeStatus(uuid string) string {
s.mtx.RLock()
defer s.mtx.RUnlock()
node, ok := s.nodes[uuid]
if !ok || !node.IsOnline() {
return "offline"
}
return "online"
}
func (s *Service) UpdateNode(uuid string, node constant.NodeUpdate) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Node{}, err
}
updatedNode, err := s.repository.UpdateNode(uuid, node)
if err != nil {
return updatedNode, err
}
return updatedNode, nil
}
func (s *Service) DeleteNode(uuid string) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedNode, err := s.repository.DeleteNode(uuid)
if err != nil {
return deletedNode, err
}
node, ok := s.nodes[uuid]
if ok {
node.Close()
delete(s.nodes, uuid)
}
return deletedNode, nil
}
func (s *Service) CreateUser(user constant.UserCreate) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.userValidator.Struct(user)
if err != nil {
return constant.User{}, err
}
createdUser, err := s.repository.CreateUser(user)
if err != nil {
return createdUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateUser(createdUser)
}
}
return createdUser, nil
}
func (s *Service) GetUsers(filters map[string][]string) ([]constant.User, error) {
return s.repository.GetUsers(filters)
}
func (s *Service) GetUsersCount(filters map[string][]string) (int, error) {
return s.repository.GetUsersCount(filters)
}
func (s *Service) GetUser(id int) (constant.User, error) {
return s.repository.GetUser(id)
}
func (s *Service) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
updatedUser, err := s.repository.UpdateUser(id, user)
if err != nil {
return updatedUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateUser(updatedUser)
}
}
return updatedUser, nil
}
func (s *Service) DeleteUser(id int) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedUser, err := s.repository.DeleteUser(id)
if err != nil {
return deletedUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteUser(deletedUser)
}
}
return deletedUser, nil
}
func (s *Service) CreateConnectionLimiter(limiter constant.ConnectionLimiterCreate) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.ConnectionLimiter{}, err
}
createdLimiter, err := s.repository.CreateConnectionLimiter(limiter)
if err != nil {
return createdLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateConnectionLimiter(createdLimiter)
}
}
return createdLimiter, nil
}
func (s *Service) GetConnectionLimiters(filters map[string][]string) ([]constant.ConnectionLimiter, error) {
return s.repository.GetConnectionLimiters(filters)
}
func (s *Service) GetConnectionLimitersCount(filters map[string][]string) (int, error) {
return s.repository.GetConnectionLimitersCount(filters)
}
func (s *Service) GetConnectionLimiter(id int) (constant.ConnectionLimiter, error) {
return s.repository.GetConnectionLimiter(id)
}
func (s *Service) UpdateConnectionLimiter(id int, limiter constant.ConnectionLimiterUpdate) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.ConnectionLimiter{}, err
}
updatedLimiter, err := s.repository.UpdateConnectionLimiter(id, limiter)
if err != nil {
return updatedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateConnectionLimiter(updatedLimiter)
}
}
if limiter.LockType != "manager" {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
delete(s.limiterLocks, id)
}
return updatedLimiter, nil
}
func (s *Service) DeleteConnectionLimiter(id int) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedLimiter, err := s.repository.DeleteConnectionLimiter(id)
if err != nil {
return deletedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteConnectionLimiter(deletedLimiter)
}
}
if deletedLimiter.LockType == "manager" {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
delete(s.limiterLocks, id)
}
return deletedLimiter, nil
}
func (s *Service) CreateBandwidthLimiter(limiter constant.BandwidthLimiterCreate) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.BandwidthLimiter{}, err
}
createdLimiter, err := s.repository.CreateBandwidthLimiter(limiter)
if err != nil {
return createdLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateBandwidthLimiter(createdLimiter)
}
}
return createdLimiter, nil
}
func (s *Service) GetBandwidthLimiters(filters map[string][]string) ([]constant.BandwidthLimiter, error) {
return s.repository.GetBandwidthLimiters(filters)
}
func (s *Service) GetBandwidthLimitersCount(filters map[string][]string) (int, error) {
return s.repository.GetBandwidthLimitersCount(filters)
}
func (s *Service) GetBandwidthLimiter(id int) (constant.BandwidthLimiter, error) {
return s.repository.GetBandwidthLimiter(id)
}
func (s *Service) UpdateBandwidthLimiter(id int, limiter constant.BandwidthLimiterUpdate) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.BandwidthLimiter{}, err
}
updatedLimiter, err := s.repository.UpdateBandwidthLimiter(id, limiter)
if err != nil {
return updatedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateBandwidthLimiter(updatedLimiter)
}
}
return updatedLimiter, nil
}
func (s *Service) DeleteBandwidthLimiter(id int) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedLimiter, err := s.repository.DeleteBandwidthLimiter(id)
if err != nil {
return deletedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteBandwidthLimiter(deletedLimiter)
}
}
return deletedLimiter, nil
}
func (s *Service) AddNode(uuid string, node constant.ConnectedNode) error {
s.mtx.Lock()
defer s.mtx.Unlock()
var node_ constant.Node
var err error
node_, err = s.repository.GetNode(uuid)
if err != nil {
return err
}
squadIDs := convertIntSliceToStringSlice(node_.SquadIDs)
users, err := s.repository.GetUsers(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateUsers(users)
bandwidthLimiters, err := s.repository.GetBandwidthLimiters(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateBandwidthLimiters(bandwidthLimiters)
connectionLimiters, err := s.repository.GetConnectionLimiters(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateConnectionLimiters(connectionLimiters)
s.nodes[uuid] = node
return nil
}
func (s *Service) AcquireLock(limiterId int, id string) (string, error) {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
limiter, err := s.repository.GetConnectionLimiter(limiterId)
if err != nil {
return "", err
}
if limiter.LockType != "manager" {
return "", E.New("invalid lock type")
}
locks, ok := s.limiterLocks[limiterId]
if !ok {
locks = make(map[string]*cache.Cache)
s.limiterLocks[limiter.ID] = locks
}
lock, ok := locks[id]
if !ok {
if len(locks) == int(limiter.Count) {
return "", E.New("not enough free locks")
}
lock = cache.New(time.Second*30, time.Second)
lock.OnEvicted(func(_ string, _ interface{}) {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
if lock.ItemCount() == 0 {
delete(locks, id)
}
})
locks[id] = lock
}
handleID, err := uuid.NewV4()
if err != nil {
return "", err
}
lock.SetDefault(handleID.String(), new(struct{}))
return handleID.String(), nil
}
func (s *Service) RefreshLock(limiterId int, id string, handleId string) error {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
locks, ok := s.limiterLocks[limiterId]
if !ok {
return E.New("limiter not found")
}
lock, ok := locks[id]
if !ok {
return E.New("lock not found")
}
err := lock.Replace(handleId, new(struct{}), time.Second*30)
return err
}
func (s *Service) ReleaseLock(limiterId int, id string, handleId string) error {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
locks, ok := s.limiterLocks[limiterId]
if !ok {
return E.New("limiter not found")
}
lock, ok := locks[id]
if !ok {
return E.New("lock not found")
}
go lock.Delete(handleId)
return nil
}
func (s *Service) Start(stage adapter.StartStage) error {
return nil
}
func (s *Service) Close() error {
return nil
}
func (s *Service) closeAllNodes() {
for _, node := range s.nodes {
node.Close()
}
}
func convertIntSliceToStringSlice(values []int) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = strconv.Itoa(v)
}
return result
}

View File

@@ -0,0 +1,20 @@
//go:build !with_manager
package manager
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *service.Registry) {
service.Register[option.ManagerServiceOptions](registry, C.TypeManager, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerServiceOptions) (adapter.Service, error) {
return nil, E.New(`Manager is not included in this build, rebuild with -tags with_manager`)
})
}