mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Resolve conflicts
This commit is contained in:
400
service/admin_panel/migration/postgresql.go
Normal file
400
service/admin_panel/migration/postgresql.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package migration
|
||||
|
||||
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": `
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
CREATE SEQUENCE public.goadmin_menu_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
CREATE TABLE public.goadmin_menu (
|
||||
id integer DEFAULT nextval('public.goadmin_menu_myid_seq'::regclass) NOT NULL,
|
||||
parent_id integer DEFAULT 0 NOT NULL,
|
||||
type integer DEFAULT 0,
|
||||
"order" integer DEFAULT 0 NOT NULL,
|
||||
title character varying(50) NOT NULL,
|
||||
header character varying(100),
|
||||
plugin_name character varying(100) NOT NULL,
|
||||
icon character varying(50) NOT NULL,
|
||||
uri character varying(3000) NOT NULL,
|
||||
uuid character varying(100),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_operation_log_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_operation_log (
|
||||
id integer DEFAULT nextval('public.goadmin_operation_log_myid_seq'::regclass) NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
path character varying(255) NOT NULL,
|
||||
method character varying(10) NOT NULL,
|
||||
ip character varying(15) NOT NULL,
|
||||
input text NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_permissions_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_permissions (
|
||||
id integer DEFAULT nextval('public.goadmin_permissions_myid_seq'::regclass) NOT NULL,
|
||||
name character varying(50) NOT NULL,
|
||||
slug character varying(50) NOT NULL,
|
||||
http_method character varying(255),
|
||||
http_path text NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE public.goadmin_role_menu (
|
||||
role_id integer NOT NULL,
|
||||
menu_id integer NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE public.goadmin_role_permissions (
|
||||
role_id integer NOT NULL,
|
||||
permission_id integer NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE public.goadmin_role_users (
|
||||
role_id integer NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_roles_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_roles (
|
||||
id integer DEFAULT nextval('public.goadmin_roles_myid_seq'::regclass) NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
slug character varying NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_session_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_session (
|
||||
id integer DEFAULT nextval('public.goadmin_session_myid_seq'::regclass) NOT NULL,
|
||||
sid character varying(50) NOT NULL,
|
||||
"values" character varying(3000) NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_site_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_site (
|
||||
id integer DEFAULT nextval('public.goadmin_site_myid_seq'::regclass) NOT NULL,
|
||||
key character varying(100) NOT NULL,
|
||||
value text NOT NULL,
|
||||
type integer DEFAULT 0,
|
||||
description character varying(3000),
|
||||
state integer DEFAULT 0,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE public.goadmin_user_permissions (
|
||||
user_id integer NOT NULL,
|
||||
permission_id integer NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.goadmin_users_myid_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
MAXVALUE 99999999
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE public.goadmin_users (
|
||||
id integer DEFAULT nextval('public.goadmin_users_myid_seq'::regclass) NOT NULL,
|
||||
username character varying(100) NOT NULL,
|
||||
password character varying(100) NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
avatar character varying(255),
|
||||
remember_token character varying(100),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
updated_at timestamp without time zone DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (1, 0, 1, 1, 'Dashboard', NULL, '', 'fa-bar-chart', '/', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (2, 0, 1, 2, 'Admin', NULL, '', 'fa-tasks', '', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (3, 2, 1, 2, 'Users', NULL, '', 'fa-users', '/info/manager', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (4, 2, 1, 3, 'Roles', NULL, '', 'fa-user', '/info/roles', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (5, 2, 1, 4, 'Permission', NULL, '', 'fa-ban', '/info/permission', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (7, 2, 1, 6, 'Operation log', NULL, '', 'fa-history', '/info/op', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (9, 0, 0, 9, 'Users', '', '', 'fa-users', '/info/users', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (14, 0, 0, 12, 'Github', 'Miscellaneous', '', 'fa-github', 'https://github.com/shtorm-7/sing-box-extended', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (15, 0, 0, 13, 'Donate', '', '', 'fa-heart', 'https://github.com/shtorm-7/sing-box-extended#support-the-project', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (13, 0, 0, 7, 'Squads', 'General', '', 'fa-gg', '/info/squads', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (11, 0, 0, 8, 'Nodes', '', '', 'fa-sitemap', '/info/nodes', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (10, 0, 0, 10, 'Connection limiters', 'Limiters', '', 'fa-plug', '/info/connection_limiters', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (8, 0, 0, 11, 'Bandwidth limiters', '', '', 'fa-dashboard', '/info/bandwidth_limiters', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_permissions (id, name, slug, http_method, http_path, created_at, updated_at) VALUES (1, 'All permission', '*', '', '*', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_permissions (id, name, slug, http_method, http_path, created_at, updated_at) VALUES (2, 'Dashboard', 'dashboard', 'GET,PUT,POST,DELETE', '/', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 7, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (2, 7, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (1, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_role_users (role_id, user_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_role_users (role_id, user_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_roles (id, name, slug, created_at, updated_at) VALUES (1, 'Administrator', 'administrator', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_roles (id, name, slug, created_at, updated_at) VALUES (2, 'Operator', 'operator', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (6, 'site_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.436501', '2026-02-15 09:57:02.436501');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (7, 'prohibit_config_modification', 'false', 0, NULL, 1, '2026-02-15 09:57:02.441183', '2026-02-15 09:57:02.441183');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (11, 'login_url', '/login', 0, NULL, 1, '2026-02-15 09:57:02.459525', '2026-02-15 09:57:02.459525');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (16, 'open_admin_api', 'false', 0, NULL, 1, '2026-02-15 09:57:02.483908', '2026-02-15 09:57:02.483908');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (18, 'domain', '', 0, NULL, 1, '2026-02-15 09:57:02.493151', '2026-02-15 09:57:02.493151');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (23, 'asset_root_path', './public/', 0, NULL, 1, '2026-02-15 09:57:02.517213', '2026-02-15 09:57:02.517213');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (24, 'url_prefix', 'admin', 0, NULL, 1, '2026-02-15 09:57:02.521815', '2026-02-15 09:57:02.521815');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (33, 'exclude_theme_components', 'null', 0, NULL, 1, '2026-02-15 09:57:02.565725', '2026-02-15 09:57:02.565725');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (39, 'app_id', 'Qn0eh7HQsrt9', 0, NULL, 1, '2026-02-15 09:57:02.592551', '2026-02-15 09:57:02.592551');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (41, 'auth_user_table', 'goadmin_users', 0, NULL, 1, '2026-02-15 09:57:02.601496', '2026-02-15 09:57:02.601496');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (53, 'bootstrap_file_path', '', 0, NULL, 1, '2026-02-15 09:57:02.658984', '2026-02-15 09:57:02.658984');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (55, 'index_url', '/', 0, NULL, 1, '2026-02-15 09:57:02.668457', '2026-02-15 09:57:02.668457');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (66, 'login_logo', '', 0, NULL, 1, '2026-02-15 09:57:02.719608', '2026-02-15 09:57:02.719608');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (67, 'hide_visitor_user_center_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.724307', '2026-02-15 09:57:02.724307');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (68, 'go_mod_file_path', '', 0, NULL, 1, '2026-02-15 09:57:02.728694', '2026-02-15 09:57:02.728694');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (3, 'logger_encoder_caller', 'full', 0, NULL, 1, '2026-02-15 09:57:02.420312', '2026-02-15 09:57:02.420312');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (60, 'logger_encoder_caller_key', 'caller', 0, NULL, 1, '2026-02-15 09:57:02.692189', '2026-02-15 09:57:02.692189');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (34, 'logo', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.570594', '2026-02-15 09:57:02.570594');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (69, 'env', 'prod', 0, NULL, 1, '2026-02-15 09:57:02.733059', '2026-02-15 09:57:02.733059');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (29, 'color_scheme', 'skin-black', 0, NULL, 1, '2026-02-15 09:57:02.545599', '2026-02-15 09:57:02.545599');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (17, 'allow_del_operation_log', 'false', 0, NULL, 1, '2026-02-15 09:57:02.488458', '2026-02-15 09:57:02.488458');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (35, 'info_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.574649', '2026-02-15 09:57:02.574649');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (22, 'operation_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.512394', '2026-02-15 09:57:02.512394');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (42, 'hide_app_info_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.606071', '2026-02-15 09:57:02.606071');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (12, 'access_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.464612', '2026-02-15 09:57:02.464612');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (32, 'logger_rotate_max_age', '30', 0, NULL, 1, '2026-02-15 09:57:02.560801', '2026-02-15 09:57:02.560801');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (40, 'custom_foot_html', '', 0, NULL, 1, '2026-02-15 09:57:02.597285', '2026-02-15 09:57:02.597285');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (62, 'logger_encoder_duration', 'string', 0, NULL, 1, '2026-02-15 09:57:02.701522', '2026-02-15 09:57:02.701522');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (65, 'logger_encoder_level_key', 'level', 0, NULL, 1, '2026-02-15 09:57:02.715108', '2026-02-15 09:57:02.715108');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (64, 'debug', 'false', 0, NULL, 1, '2026-02-15 09:57:02.710705', '2026-02-15 09:57:02.710705');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (43, 'hide_plugin_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.610825', '2026-02-15 09:57:02.610825');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (54, 'animation_type', '', 0, NULL, 1, '2026-02-15 09:57:02.663713', '2026-02-15 09:57:02.663713');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (48, 'theme', 'sword', 0, NULL, 1, '2026-02-15 09:57:02.634039', '2026-02-15 09:57:02.634039');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (45, 'info_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.620165', '2026-02-15 09:57:02.620165');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (31, 'error_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.555798', '2026-02-15 09:57:02.555798');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (5, 'asset_url', '', 0, NULL, 1, '2026-02-15 09:57:02.431855', '2026-02-15 09:57:02.431855');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (36, 'logger_encoder_encoding', 'console', 0, NULL, 1, '2026-02-15 09:57:02.579052', '2026-02-15 09:57:02.579052');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (27, 'login_title', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.536102', '2026-02-15 09:57:02.536102');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (51, 'animation_duration', '0.00', 0, NULL, 1, '2026-02-15 09:57:02.64867', '2026-02-15 09:57:02.64867');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (19, 'file_upload_engine', '{"name":"local"}', 0, NULL, 1, '2026-02-15 09:57:02.49794', '2026-02-15 09:57:02.49794');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (26, 'logger_encoder_time', 'iso8601', 0, NULL, 1, '2026-02-15 09:57:02.531365', '2026-02-15 09:57:02.531365');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (10, 'custom_404_html', '', 0, NULL, 1, '2026-02-15 09:57:02.454777', '2026-02-15 09:57:02.454777');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (58, 'sql_log', 'false', 0, NULL, 1, '2026-02-15 09:57:02.682567', '2026-02-15 09:57:02.682567');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (2, 'logger_encoder_message_key', 'msg', 0, NULL, 1, '2026-02-15 09:57:02.415189', '2026-02-15 09:57:02.415189');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (46, 'logger_encoder_stacktrace_key', 'stacktrace', 0, NULL, 1, '2026-02-15 09:57:02.624977', '2026-02-15 09:57:02.624977');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (63, 'mini_logo', 'SBE', 0, NULL, 1, '2026-02-15 09:57:02.706145', '2026-02-15 09:57:02.706145');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (38, 'custom_403_html', '', 0, NULL, 1, '2026-02-15 09:57:02.588062', '2026-02-15 09:57:02.588062');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (30, 'language', 'en', 0, NULL, 1, '2026-02-15 09:57:02.550466', '2026-02-15 09:57:02.550466');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (15, 'hide_config_center_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.479097', '2026-02-15 09:57:02.479097');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (59, 'logger_rotate_max_backups', '5', 0, NULL, 1, '2026-02-15 09:57:02.687429', '2026-02-15 09:57:02.687429');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (57, 'custom_head_html', '', 0, NULL, 1, '2026-02-15 09:57:02.677723', '2026-02-15 09:57:02.677723');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (52, 'custom_500_html', '', 0, NULL, 1, '2026-02-15 09:57:02.654236', '2026-02-15 09:57:02.654236');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (44, 'title', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.615471', '2026-02-15 09:57:02.615471');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (47, 'session_life_time', '7200', 0, NULL, 1, '2026-02-15 09:57:02.629619', '2026-02-15 09:57:02.629619');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (8, 'access_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.445593', '2026-02-15 09:57:02.445593');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (49, 'error_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.6385', '2026-02-15 09:57:02.6385');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (50, 'logger_rotate_max_size', '10', 0, NULL, 1, '2026-02-15 09:57:02.643733', '2026-02-15 09:57:02.643733');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (14, 'logger_rotate_compress', 'false', 0, NULL, 1, '2026-02-15 09:57:02.474296', '2026-02-15 09:57:02.474296');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (13, 'logger_encoder_time_key', 'ts', 0, NULL, 1, '2026-02-15 09:57:02.469396', '2026-02-15 09:57:02.469396');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (37, 'animation_delay', '0.00', 0, NULL, 1, '2026-02-15 09:57:02.583815', '2026-02-15 09:57:02.583815');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (20, 'extra', '', 0, NULL, 1, '2026-02-15 09:57:02.50276', '2026-02-15 09:57:02.50276');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (25, 'access_assets_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.526618', '2026-02-15 09:57:02.526618');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (4, 'logger_level', '0', 0, NULL, 1, '2026-02-15 09:57:02.426736', '2026-02-15 09:57:02.426736');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (9, 'footer_info', '', 0, NULL, 1, '2026-02-15 09:57:02.450409', '2026-02-15 09:57:02.450409');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (21, 'no_limit_login_ip', 'false', 0, NULL, 1, '2026-02-15 09:57:02.507609', '2026-02-15 09:57:02.507609');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (28, 'hide_tool_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.540813', '2026-02-15 09:57:02.540813');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (61, 'logger_encoder_level', 'capitalColor', 0, NULL, 1, '2026-02-15 09:57:02.696859', '2026-02-15 09:57:02.696859');
|
||||
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (56, 'logger_encoder_name_key', 'logger', 0, NULL, 1, '2026-02-15 09:57:02.672962', '2026-02-15 09:57:02.672962');
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
|
||||
|
||||
|
||||
INSERT INTO public.goadmin_users (id, username, password, name, avatar, remember_token, created_at, updated_at) VALUES (2, 'operator', '$2a$10$rVqkOzHjN2MdlEprRflb1eGP0oZXuSrbJLOmJagFsCd81YZm0bsh.', 'Operator', '', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
INSERT INTO public.goadmin_users (id, username, password, name, avatar, remember_token, created_at, updated_at) VALUES (1, 'admin', '$2a$10$ilNHHnX5S6EMw.Ffc1Y1JezYCyquFIO.7Z0vLr1eHJUXnGy4cdrtq', 'admin', '', 'tlNcBVK9AvfYH7WEnwB1RKvocJu8FfRy4um3DJtwdHuJy0dwFsLOgAc0xUfh', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_menu_myid_seq', 12, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_operation_log_myid_seq', 11, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_permissions_myid_seq', 2, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_roles_myid_seq', 2, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_session_myid_seq', 7, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_site_myid_seq', 69, true);
|
||||
|
||||
|
||||
SELECT pg_catalog.setval('public.goadmin_users_myid_seq', 2, true);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_menu
|
||||
ADD CONSTRAINT goadmin_menu_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_operation_log
|
||||
ADD CONSTRAINT goadmin_operation_log_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_permissions
|
||||
ADD CONSTRAINT goadmin_permissions_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_roles
|
||||
ADD CONSTRAINT goadmin_roles_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_session
|
||||
ADD CONSTRAINT goadmin_session_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_site
|
||||
ADD CONSTRAINT goadmin_site_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
ALTER TABLE ONLY public.goadmin_users
|
||||
ADD CONSTRAINT goadmin_users_pkey PRIMARY KEY (id);
|
||||
`,
|
||||
"1_initialize_schema.down.sql": ``,
|
||||
}
|
||||
|
||||
func MigratePostgreSQL(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()
|
||||
}
|
||||
13
service/admin_panel/pages/dashboard.go
Normal file
13
service/admin_panel/pages/dashboard.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
)
|
||||
|
||||
func DashboardPage(ctx *context.Context) (types.Panel, error) {
|
||||
|
||||
return types.Panel{
|
||||
Title: "Dashboard",
|
||||
}, nil
|
||||
}
|
||||
188
service/admin_panel/service.go
Normal file
188
service/admin_panel/service.go
Normal file
@@ -0,0 +1,188 @@
|
||||
//go:build with_admin_panel
|
||||
|
||||
package admin_panel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/lib/pq"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
_ "github.com/GoAdminGroup/go-admin/adapter/chi"
|
||||
"github.com/GoAdminGroup/go-admin/engine"
|
||||
"github.com/GoAdminGroup/go-admin/modules/config"
|
||||
_ "github.com/GoAdminGroup/go-admin/modules/db/drivers/sqlite"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/chartjs"
|
||||
_ "github.com/GoAdminGroup/themes/adminlte"
|
||||
_ "github.com/GoAdminGroup/themes/sword"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
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/admin_panel/migration"
|
||||
"github.com/sagernet/sing-box/service/admin_panel/pages"
|
||||
"github.com/sagernet/sing-box/service/admin_panel/tables"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.AdminPanelServiceOptions](registry, C.TypeAdminPanel, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
options option.AdminPanelServiceOptions
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.AdminPanelServiceOptions) (adapter.Service, error) {
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeAdminPanel, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
options: options,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
|
||||
service, ok := boxManager.Get(s.options.Manager)
|
||||
if !ok {
|
||||
return E.New("manager ", s.options.Manager, " not found")
|
||||
}
|
||||
manager, ok := service.(CM.Manager)
|
||||
if !ok {
|
||||
return E.New("invalid ", s.options.Manager, " manager")
|
||||
}
|
||||
switch s.options.Database.Driver {
|
||||
case "postgresql":
|
||||
db, err := sql.Open("postgres", s.options.Database.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if err := migration.MigratePostgreSQL(db); err != nil && err != migrate.ErrNoChange {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unknown driver \"", s.options.Database.Driver, "\"")
|
||||
}
|
||||
var generators = map[string]table.Generator{
|
||||
"squads": tables.SquadTableFactory(
|
||||
manager,
|
||||
s.logger,
|
||||
),
|
||||
"nodes": tables.NodeTableFactory(
|
||||
manager,
|
||||
s.logger,
|
||||
),
|
||||
"users": tables.UserTableFactory(
|
||||
manager,
|
||||
s.logger,
|
||||
),
|
||||
"connection_limiters": tables.ConnectionLimiterTableFactory(
|
||||
manager,
|
||||
s.logger,
|
||||
),
|
||||
"bandwidth_limiters": tables.BandwidthLimiterTableFactory(
|
||||
manager,
|
||||
s.logger,
|
||||
),
|
||||
}
|
||||
eng := engine.Default()
|
||||
chiRouter := chi.NewRouter()
|
||||
template.AddComp(chartjs.NewChart())
|
||||
if err := eng.AddConfig(&config.Config{
|
||||
UrlPrefix: "admin",
|
||||
IndexUrl: "/",
|
||||
LoginUrl: "/login",
|
||||
Databases: config.DatabaseList{
|
||||
"default": config.Database{
|
||||
Driver: s.options.Database.Driver,
|
||||
Dsn: s.options.Database.DSN,
|
||||
},
|
||||
},
|
||||
}).
|
||||
AddGenerators(generators).
|
||||
Use(chiRouter); err != nil {
|
||||
return err
|
||||
}
|
||||
eng.HTML("GET", "/admin", pages.DashboardPage)
|
||||
chiRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin", http.StatusMovedPermanently)
|
||||
})
|
||||
chiRouter.Get("/admin/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin", http.StatusMovedPermanently)
|
||||
})
|
||||
if s.options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: chiRouter,
|
||||
}
|
||||
go func() {
|
||||
err = s.httpServer.Serve(tcpListener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("serve error: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.httpServer),
|
||||
common.PtrOrNil(s.listener),
|
||||
s.tlsConfig,
|
||||
)
|
||||
}
|
||||
20
service/admin_panel/service_stub.go
Normal file
20
service/admin_panel/service_stub.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build !with_admin_panel
|
||||
|
||||
package admin_panel
|
||||
|
||||
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.AdminPanelServiceOptions](registry, C.TypeAdminPanel, func(ctx context.Context, logger log.ContextLogger, tag string, options option.AdminPanelServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`Admin panel is not included in this build, rebuild with -tags with_admin_panel`)
|
||||
})
|
||||
}
|
||||
259
service/admin_panel/tables/bandwidth_limiter.go
Normal file
259
service/admin_panel/tables/bandwidth_limiter.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
func BandwidthLimiterTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) table.Table {
|
||||
return func(ctx *context.Context) table.Table {
|
||||
t := table.NewDefaultTable(ctx, table.Config{
|
||||
CanAdd: true,
|
||||
Editable: true,
|
||||
Deletable: true,
|
||||
Exportable: true,
|
||||
PrimaryKey: table.PrimaryKey{
|
||||
Type: db.Int,
|
||||
Name: table.DefaultPrimaryKeyName,
|
||||
},
|
||||
})
|
||||
squads, err := manager.GetSquads(map[string][]string{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
squadsByID := make(map[int]string, len(squads))
|
||||
squadOptions := make(types.FieldOptions, len(squads))
|
||||
for i, squad := range squads {
|
||||
squadsByID[squad.ID] = squad.Name
|
||||
squadOptions[i] = types.FieldOption{
|
||||
Text: squad.Name,
|
||||
Value: strconv.Itoa(squad.ID),
|
||||
}
|
||||
}
|
||||
info := t.GetInfo().SetFilterFormLayout(form.LayoutFilter)
|
||||
info.AddField("ID", "id", db.Int).
|
||||
FieldSortable()
|
||||
info.AddField("Squads", "squad_ids", db.Varchar).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
values := model.Row["squad_ids"].([]interface{})
|
||||
labels := template.HTML("")
|
||||
labelTpl := label(ctx).SetType("success")
|
||||
labelValues := make([]string, len(values))
|
||||
for i, squadID := range values {
|
||||
labelValues[i] = squadsByID[int(squadID.(float64))]
|
||||
}
|
||||
for key, label := range labelValues {
|
||||
if key == len(labelValues)-1 {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
} else {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
}
|
||||
}
|
||||
return labels
|
||||
})
|
||||
info.AddField("Username", "username", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Outbound", "outbound", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Strategy", "strategy", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Connection", Value: "connection"},
|
||||
{Text: "Global", Value: "global"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Mode", "mode", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Download", Value: "download"},
|
||||
{Text: "Upload", Value: "upload"},
|
||||
{Text: "Duplex", Value: "duplex"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Connection type", "connection_type", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "HWID", Value: "hwid"},
|
||||
{Text: "Mux", Value: "mux"},
|
||||
{Text: "IP", Value: "ip"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Speed", "speed", db.Varchar).
|
||||
FieldSortable()
|
||||
info.AddField("Created at", "created_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
info.AddField("Updated at", "updated_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
|
||||
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
|
||||
filters := make(map[string][]string)
|
||||
listFilters := map[string][]string{
|
||||
"offset": {strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)},
|
||||
"limit": {param.PageSize},
|
||||
}
|
||||
for k, v := range param.Fields {
|
||||
if strings.HasPrefix(k, "__") {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSuffix(k, "__goadmin")
|
||||
filters[key] = v
|
||||
listFilters[key] = v
|
||||
}
|
||||
if param.SortField != "" {
|
||||
if param.SortType == "asc" {
|
||||
listFilters["sort_asc"] = []string{param.SortField}
|
||||
} else {
|
||||
listFilters["sort_desc"] = []string{param.SortField}
|
||||
}
|
||||
}
|
||||
items, err := manager.GetBandwidthLimiters(listFilters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
count, err := manager.GetBandwidthLimitersCount(filters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
var data map[string]interface{}
|
||||
raw, _ := json.Marshal(item)
|
||||
json.Unmarshal(raw, &data)
|
||||
result = append(result, data)
|
||||
}
|
||||
return result, count
|
||||
})
|
||||
|
||||
info.SetDeleteFn(func(ids []string) error {
|
||||
for _, id := range ids {
|
||||
i, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.DeleteBandwidthLimiter(i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
info.SetTable("bandwidth_limiters").SetTitle("Bandwidth Limiters").SetDescription("Bandwidth Limiters")
|
||||
|
||||
formList := t.GetForm()
|
||||
formList.AddField("ID", "id", db.Int, form.Default).
|
||||
FieldNotAllowAdd().
|
||||
FieldNotAllowEdit()
|
||||
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
|
||||
FieldMust().
|
||||
FieldOptions(squadOptions).
|
||||
FieldDisableWhenUpdate()
|
||||
formList.AddField("Username", "username", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate()
|
||||
formList.AddField("Outbound", "outbound", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate()
|
||||
formList.AddField("Strategy", "strategy", db.Varchar, form.SelectSingle).
|
||||
FieldMust().
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Connection", Value: "connection"},
|
||||
{Text: "Global", Value: "global"},
|
||||
}).
|
||||
FieldOnChooseOptionsHide([]string{"", "global"}, "connection_type")
|
||||
formList.AddField("Mode", "mode", db.Varchar, form.SelectSingle).
|
||||
FieldMust().
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Download", Value: "download"},
|
||||
{Text: "Upload", Value: "upload"},
|
||||
{Text: "Duplex", Value: "duplex"},
|
||||
})
|
||||
formList.AddField("Connection type", "connection_type", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "HWID", Value: "hwid"},
|
||||
{Text: "Mux", Value: "mux"},
|
||||
{Text: "IP", Value: "ip"},
|
||||
})
|
||||
formList.AddField("Speed", "speed", db.Varchar, form.Text).
|
||||
FieldMust()
|
||||
|
||||
formList.SetInsertFn(func(values mForm.Values) error {
|
||||
squadIDs := make([]int, len(values["squad_ids[]"]))
|
||||
for i, rawSquadID := range values["squad_ids[]"] {
|
||||
squadID, err := strconv.Atoi(rawSquadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
squadIDs[i] = squadID
|
||||
}
|
||||
_, err := manager.CreateBandwidthLimiter(CM.BandwidthLimiterCreate{
|
||||
SquadIDs: squadIDs,
|
||||
Username: values.Get("username"),
|
||||
Outbound: values.Get("outbound"),
|
||||
Strategy: values.Get("strategy"),
|
||||
Mode: values.Get("mode"),
|
||||
ConnectionType: values.Get("connection_type"),
|
||||
Speed: values.Get("speed"),
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
formList.SetUpdateFn(func(values mForm.Values) error {
|
||||
id, err := strconv.Atoi(values.Get("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = manager.UpdateBandwidthLimiter(id, CM.BandwidthLimiterUpdate{
|
||||
Username: values.Get("username"),
|
||||
Outbound: values.Get("outbound"),
|
||||
Strategy: values.Get("strategy"),
|
||||
Mode: values.Get("mode"),
|
||||
ConnectionType: values.Get("connection_type"),
|
||||
Speed: values.Get("speed"),
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
formList.SetTable("bandwidth_limiters").SetTitle("Bandwidth Limiters").SetDescription("Bandwidth Limiters")
|
||||
return t
|
||||
}
|
||||
}
|
||||
261
service/admin_panel/tables/connection_limiter.go
Normal file
261
service/admin_panel/tables/connection_limiter.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
func ConnectionLimiterTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) table.Table {
|
||||
return func(ctx *context.Context) table.Table {
|
||||
connectionLimiterTable := table.NewDefaultTable(ctx, table.Config{
|
||||
CanAdd: true,
|
||||
Editable: true,
|
||||
Deletable: true,
|
||||
Exportable: true,
|
||||
PrimaryKey: table.PrimaryKey{
|
||||
Type: db.Int,
|
||||
Name: table.DefaultPrimaryKeyName,
|
||||
},
|
||||
})
|
||||
squads, err := manager.GetSquads(map[string][]string{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
squadsByID := make(map[int]string, len(squads))
|
||||
squadOptions := make(types.FieldOptions, len(squads))
|
||||
for i, squad := range squads {
|
||||
squadsByID[squad.ID] = squad.Name
|
||||
squadOptions[i] = types.FieldOption{
|
||||
Text: squad.Name,
|
||||
Value: strconv.Itoa(squad.ID),
|
||||
}
|
||||
}
|
||||
info := connectionLimiterTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
|
||||
info.AddField("ID", "id", db.Int).
|
||||
FieldSortable()
|
||||
info.AddField("Squads", "squad_ids", db.Varchar).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
values := model.Row["squad_ids"].([]interface{})
|
||||
labels := template.HTML("")
|
||||
labelTpl := label(ctx).SetType("success")
|
||||
labelValues := make([]string, len(values))
|
||||
for i, squadID := range values {
|
||||
labelValues[i] = squadsByID[int(squadID.(float64))]
|
||||
}
|
||||
for key, label := range labelValues {
|
||||
if key == len(labelValues)-1 {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
} else {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
}
|
||||
}
|
||||
return labels
|
||||
})
|
||||
info.AddField("Username", "username", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Outbound", "outbound", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Strategy", "strategy", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Connection", Value: "connection"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Connection type", "connection_type", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Mux", Value: "mux"},
|
||||
{Text: "HWID", Value: "hwid"},
|
||||
{Text: "IP", Value: "ip"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Lock type", "lock_type", db.Varchar).
|
||||
FieldFilterable(types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Manager", Value: "manager"},
|
||||
},
|
||||
}).
|
||||
FieldSortable()
|
||||
info.AddField("Count", "count", db.Int).
|
||||
FieldSortable()
|
||||
info.AddField("Created at", "created_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
info.AddField("Updated at", "updated_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
|
||||
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
|
||||
filters := make(map[string][]string)
|
||||
listFilters := map[string][]string{
|
||||
"offset": {strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)},
|
||||
"limit": {param.PageSize},
|
||||
}
|
||||
for k, v := range param.Fields {
|
||||
if strings.HasPrefix(k, "__") {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSuffix(k, "__goadmin")
|
||||
filters[key] = v
|
||||
listFilters[key] = v
|
||||
}
|
||||
if param.SortField != "" {
|
||||
if param.SortType == "asc" {
|
||||
listFilters["sort_asc"] = []string{param.SortField}
|
||||
} else {
|
||||
listFilters["sort_desc"] = []string{param.SortField}
|
||||
}
|
||||
}
|
||||
items, err := manager.GetConnectionLimiters(listFilters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
count, err := manager.GetConnectionLimitersCount(filters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
var data map[string]interface{}
|
||||
raw, _ := json.Marshal(item)
|
||||
json.Unmarshal(raw, &data)
|
||||
result = append(result, data)
|
||||
}
|
||||
return result, count
|
||||
})
|
||||
|
||||
info.SetDeleteFn(func(ids []string) error {
|
||||
for _, id := range ids {
|
||||
i, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.DeleteConnectionLimiter(i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
info.SetTable("connection_limiters").SetTitle("Connection Limiters").SetDescription("Connection Limiters")
|
||||
|
||||
formList := connectionLimiterTable.GetForm()
|
||||
formList.AddField("ID", "id", db.Int, form.Default).
|
||||
FieldNotAllowAdd().
|
||||
FieldNotAllowEdit()
|
||||
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
|
||||
FieldMust().
|
||||
FieldOptions(squadOptions).
|
||||
FieldDisableWhenUpdate()
|
||||
formList.AddField("Username", "username", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate()
|
||||
formList.AddField("Outbound", "outbound", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate()
|
||||
formList.AddField("Strategy", "strategy", db.Varchar, form.SelectSingle).
|
||||
FieldMust().
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Connection", Value: "connection"},
|
||||
}).
|
||||
FieldDefault("connection")
|
||||
formList.AddField("Connection type", "connection_type", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Mux", Value: "mux"},
|
||||
{Text: "HWID", Value: "hwid"},
|
||||
{Text: "IP", Value: "ip"},
|
||||
})
|
||||
formList.AddField("Lock type", "lock_type", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Manager", Value: "manager"},
|
||||
})
|
||||
formList.AddField("Count", "count", db.Int, form.Number).
|
||||
FieldMust().
|
||||
FieldDefault("0")
|
||||
|
||||
formList.SetInsertFn(func(values mForm.Values) error {
|
||||
squadIDs := make([]int, len(values["squad_ids[]"]))
|
||||
for i, rawSquadID := range values["squad_ids[]"] {
|
||||
squadID, err := strconv.Atoi(rawSquadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
squadIDs[i] = squadID
|
||||
}
|
||||
count, err := strconv.ParseUint(values.Get("count"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = manager.CreateConnectionLimiter(CM.ConnectionLimiterCreate{
|
||||
SquadIDs: squadIDs,
|
||||
Username: values.Get("username"),
|
||||
Outbound: values.Get("outbound"),
|
||||
Strategy: values.Get("strategy"),
|
||||
ConnectionType: values.Get("connection_type"),
|
||||
LockType: values.Get("lock_type"),
|
||||
Count: uint32(count),
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
formList.SetUpdateFn(func(values mForm.Values) error {
|
||||
id, err := strconv.Atoi(values.Get("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := strconv.ParseUint(values.Get("count"), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = manager.UpdateConnectionLimiter(id, CM.ConnectionLimiterUpdate{
|
||||
Username: values.Get("username"),
|
||||
Outbound: values.Get("outbound"),
|
||||
Strategy: values.Get("strategy"),
|
||||
ConnectionType: values.Get("connection_type"),
|
||||
LockType: values.Get("lock_type"),
|
||||
Count: uint32(count),
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
formList.SetTable("connection_limiters").SetTitle("Connection Limiters").SetDescription("Connection Limiters")
|
||||
return connectionLimiterTable
|
||||
}
|
||||
}
|
||||
201
service/admin_panel/tables/node.go
Normal file
201
service/admin_panel/tables/node.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/config"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
func label(ctx *context.Context) types.LabelAttribute {
|
||||
return template.Get(ctx, config.GetTheme()).Label().SetType("success")
|
||||
}
|
||||
|
||||
func NodeTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (nodeTable table.Table) {
|
||||
return func(ctx *context.Context) (nodeTable table.Table) {
|
||||
nodeTable = table.NewDefaultTable(ctx, table.Config{
|
||||
CanAdd: true,
|
||||
Editable: true,
|
||||
Deletable: true,
|
||||
Exportable: true,
|
||||
PrimaryKey: table.PrimaryKey{
|
||||
Type: db.Varchar,
|
||||
Name: "uuid",
|
||||
},
|
||||
})
|
||||
squads, err := manager.GetSquads(map[string][]string{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
squadsByID := make(map[int]string, len(squads))
|
||||
squadOptions := make(types.FieldOptions, len(squads))
|
||||
for i, squad := range squads {
|
||||
squadsByID[squad.ID] = squad.Name
|
||||
squadOptions[i] = types.FieldOption{
|
||||
Text: squad.Name,
|
||||
Value: strconv.Itoa(squad.ID),
|
||||
}
|
||||
}
|
||||
info := nodeTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
|
||||
info.AddField("UUID", "uuid", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Name", "name", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Squads", "squad_ids", db.Varchar).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
values := model.Row["squad_ids"].([]interface{})
|
||||
labels := template.HTML("")
|
||||
labelTpl := label(ctx).SetType("success")
|
||||
labelValues := make([]string, len(values))
|
||||
for i, squadID := range values {
|
||||
labelValues[i] = squadsByID[int(squadID.(float64))]
|
||||
}
|
||||
for key, label := range labelValues {
|
||||
if key == len(labelValues)-1 {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
} else {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
}
|
||||
}
|
||||
return labels
|
||||
})
|
||||
info.AddField("Status", "status", db.Varchar).
|
||||
FieldDisplay(func(value types.FieldModel) interface{} {
|
||||
uuid := value.Row["uuid"].(string)
|
||||
return manager.GetNodeStatus(uuid)
|
||||
})
|
||||
info.AddField("Created at", "created_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
info.AddField("Updated at", "updated_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
|
||||
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
|
||||
filters := make(map[string][]string, len(param.Fields))
|
||||
listFilters := make(map[string][]string, len(param.Fields)+2)
|
||||
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
|
||||
listFilters["limit"] = []string{param.PageSize}
|
||||
for key, values := range param.Fields {
|
||||
if key == "__pk" {
|
||||
key = "uuid"
|
||||
} else {
|
||||
if strings.HasPrefix(key, "__") {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSuffix(key, "__goadmin")
|
||||
}
|
||||
filters[key] = values
|
||||
listFilters[key] = values
|
||||
}
|
||||
if param.SortField != "" {
|
||||
if param.SortType == "asc" {
|
||||
listFilters["sort_asc"] = []string{param.SortField}
|
||||
} else {
|
||||
listFilters["sort_desc"] = []string{param.SortField}
|
||||
}
|
||||
}
|
||||
nodes, err := manager.GetNodes(listFilters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
count, err := manager.GetNodesCount(filters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
var data map[string]interface{}
|
||||
rawData, _ := json.Marshal(node)
|
||||
json.Unmarshal(rawData, &data)
|
||||
result = append(result, data)
|
||||
}
|
||||
return result, count
|
||||
})
|
||||
|
||||
info.SetDeleteFn(func(ids []string) error {
|
||||
for _, uuid := range ids {
|
||||
if _, err := manager.DeleteNode(uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
info.SetTable("nodes").SetTitle("Nodes").SetDescription("Nodes")
|
||||
|
||||
defaultUUID, _ := uuid.NewV4()
|
||||
formList := nodeTable.GetForm()
|
||||
formList.AddField("UUID", "uuid", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldNotAllowEdit().
|
||||
FieldDefault(defaultUUID.String())
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).
|
||||
FieldMust()
|
||||
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
|
||||
FieldMust().
|
||||
FieldOptions(squadOptions).
|
||||
FieldDisableWhenUpdate()
|
||||
|
||||
formList.SetInsertFn(func(values mForm.Values) (err error) {
|
||||
squadIDs := make([]int, len(values["squad_ids[]"]))
|
||||
for i, rawSquadID := range values["squad_ids[]"] {
|
||||
squadID, err := strconv.Atoi(rawSquadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
squadIDs[i] = squadID
|
||||
}
|
||||
_, err = manager.CreateNode(CM.NodeCreate{
|
||||
UUID: values.Get("uuid"),
|
||||
Name: values.Get("name"),
|
||||
SquadIDs: squadIDs,
|
||||
})
|
||||
return
|
||||
})
|
||||
|
||||
formList.SetUpdateFn(func(values mForm.Values) (err error) {
|
||||
uuid := values.Get("uuid")
|
||||
_, err = manager.UpdateNode(uuid, CM.NodeUpdate{
|
||||
Name: values.Get("name"),
|
||||
})
|
||||
return
|
||||
})
|
||||
|
||||
formList.SetTable("nodes").SetTitle("Nodes").SetDescription("Nodes")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
164
service/admin_panel/tables/squad.go
Normal file
164
service/admin_panel/tables/squad.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
func SquadTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (squadTable table.Table) {
|
||||
return func(ctx *context.Context) (squadTable table.Table) {
|
||||
squadTable = table.NewDefaultTable(ctx, table.Config{
|
||||
CanAdd: true,
|
||||
Editable: true,
|
||||
Deletable: true,
|
||||
Exportable: true,
|
||||
PrimaryKey: table.PrimaryKey{
|
||||
Type: db.Int,
|
||||
Name: table.DefaultPrimaryKeyName,
|
||||
},
|
||||
})
|
||||
|
||||
info := squadTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
|
||||
info.AddField("ID", "id", db.Int).
|
||||
FieldSortable()
|
||||
info.AddField("Name", "name", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Created At", "created_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldSortable().
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange})
|
||||
info.AddField("Updated At", "updated_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldSortable().
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange})
|
||||
|
||||
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
|
||||
filters := make(map[string][]string, len(param.Fields))
|
||||
listFilters := make(map[string][]string, len(param.Fields)+2)
|
||||
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
|
||||
listFilters["limit"] = []string{param.PageSize}
|
||||
for key, values := range param.Fields {
|
||||
if key == "__pk" {
|
||||
key = "pk"
|
||||
} else if strings.HasPrefix(key, "__") {
|
||||
continue
|
||||
} else {
|
||||
key = strings.TrimSuffix(key, "__goadmin")
|
||||
}
|
||||
filters[key] = values
|
||||
listFilters[key] = values
|
||||
}
|
||||
if param.SortField != "" {
|
||||
if param.SortType == "asc" {
|
||||
listFilters["sort_asc"] = []string{param.SortField}
|
||||
} else {
|
||||
listFilters["sort_desc"] = []string{param.SortField}
|
||||
}
|
||||
}
|
||||
squads, err := manager.GetSquads(listFilters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
count, err := manager.GetSquadsCount(filters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(squads))
|
||||
for _, squad := range squads {
|
||||
var data map[string]interface{}
|
||||
rawData, _ := json.Marshal(squad)
|
||||
json.Unmarshal(rawData, &data)
|
||||
result = append(result, data)
|
||||
}
|
||||
return result, count
|
||||
})
|
||||
|
||||
info.SetDeleteFn(func(ids []string) error {
|
||||
for _, id := range ids {
|
||||
intID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.DeleteSquad(intID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
info.SetTable("squads").SetTitle("Squads").SetDescription("Squads")
|
||||
|
||||
formList := squadTable.GetForm()
|
||||
formList.AddField("ID", "id", db.Int, form.Default).
|
||||
FieldNotAllowAdd().
|
||||
FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).
|
||||
FieldMust()
|
||||
|
||||
formList.SetInsertFn(func(values mForm.Values) (err error) {
|
||||
_, err = manager.CreateSquad(CM.SquadCreate{
|
||||
Name: values.Get("name"),
|
||||
})
|
||||
if err != nil {
|
||||
if ve, ok := err.(validator.ValidationErrors); ok {
|
||||
var errors []string
|
||||
for _, e := range ve {
|
||||
switch e.Tag() {
|
||||
case "required":
|
||||
errors = append(errors, e.StructField()+": required field missing")
|
||||
default:
|
||||
errors = append(errors, e.StructField()+": invalid request")
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("%s", strings.Join(errors, "<br>"))
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
formList.SetUpdateFn(func(values mForm.Values) (err error) {
|
||||
id, err := strconv.Atoi(values.Get("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = manager.UpdateSquad(id, CM.SquadUpdate{
|
||||
Name: values.Get("name"),
|
||||
})
|
||||
return
|
||||
})
|
||||
|
||||
formList.SetTable("squads").SetTitle("Squads").SetDescription("Squads")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
282
service/admin_panel/tables/user.go
Normal file
282
service/admin_panel/tables/user.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
func UserTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (userTable table.Table) {
|
||||
return func(ctx *context.Context) (userTable table.Table) {
|
||||
userTable = table.NewDefaultTable(ctx, table.Config{
|
||||
CanAdd: true,
|
||||
Editable: true,
|
||||
Deletable: true,
|
||||
Exportable: true,
|
||||
PrimaryKey: table.PrimaryKey{
|
||||
Type: db.Int,
|
||||
Name: table.DefaultPrimaryKeyName,
|
||||
},
|
||||
})
|
||||
squads, err := manager.GetSquads(map[string][]string{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
squadsByID := make(map[int]string, len(squads))
|
||||
squadOptions := make(types.FieldOptions, len(squads))
|
||||
for i, squad := range squads {
|
||||
squadsByID[squad.ID] = squad.Name
|
||||
squadOptions[i] = types.FieldOption{
|
||||
Text: squad.Name,
|
||||
Value: strconv.Itoa(squad.ID),
|
||||
}
|
||||
}
|
||||
info := userTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
|
||||
info.AddField("ID", "id", db.Int).
|
||||
FieldSortable()
|
||||
info.AddField("Squads", "squad_ids", db.Varchar).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
values := model.Row["squad_ids"].([]interface{})
|
||||
labels := template.HTML("")
|
||||
labelTpl := label(ctx).SetType("success")
|
||||
labelValues := make([]string, len(values))
|
||||
for i, squadID := range values {
|
||||
labelValues[i] = squadsByID[int(squadID.(float64))]
|
||||
}
|
||||
for key, label := range labelValues {
|
||||
if key == len(labelValues)-1 {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
} else {
|
||||
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
|
||||
}
|
||||
}
|
||||
return labels
|
||||
})
|
||||
info.AddField("Username", "username", db.Varchar).
|
||||
FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Type", "type", db.Varchar).
|
||||
FieldFilterable(
|
||||
types.FilterType{
|
||||
FormType: form.SelectSingle,
|
||||
Options: types.FieldOptions{
|
||||
{Text: "Hysteria", Value: "hysteria"},
|
||||
{Text: "Hysteria2", Value: "hysteria2"},
|
||||
{Text: "Trojan", Value: "trojan"},
|
||||
{Text: "TUIC", Value: "tuic"},
|
||||
{Text: "VLESS", Value: "vless"},
|
||||
{Text: "VMess", Value: "vmess"},
|
||||
},
|
||||
},
|
||||
).
|
||||
FieldSortable()
|
||||
info.AddField("Inbound", "inbound", db.Varchar).FieldFilterable().
|
||||
FieldSortable()
|
||||
info.AddField("Created at", "created_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
info.AddField("Updated at", "updated_at", db.Datetime).
|
||||
FieldDisplay(func(model types.FieldModel) interface{} {
|
||||
t, err := time.Parse(time.RFC3339, model.Value)
|
||||
if err != nil {
|
||||
return model.Value
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}).
|
||||
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
|
||||
FieldSortable()
|
||||
|
||||
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
|
||||
filters := make(map[string][]string, len(param.Fields))
|
||||
listFilters := make(map[string][]string, len(param.Fields)+2)
|
||||
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
|
||||
listFilters["limit"] = []string{param.PageSize}
|
||||
for key, values := range param.Fields {
|
||||
if key == "__pk" {
|
||||
key = "pk"
|
||||
} else {
|
||||
if strings.HasPrefix(key, "__") {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSuffix(key, "__goadmin")
|
||||
}
|
||||
filters[key] = values
|
||||
listFilters[key] = values
|
||||
}
|
||||
if param.SortField != "" {
|
||||
if param.SortType == "asc" {
|
||||
listFilters["sort_asc"] = []string{param.SortField}
|
||||
} else {
|
||||
listFilters["sort_desc"] = []string{param.SortField}
|
||||
}
|
||||
}
|
||||
users, err := manager.GetUsers(listFilters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
count, err := manager.GetUsersCount(filters)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, 0
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(users))
|
||||
for _, user := range users {
|
||||
var data map[string]interface{}
|
||||
rawData, _ := json.Marshal(user)
|
||||
json.Unmarshal(rawData, &data)
|
||||
result = append(result, data)
|
||||
}
|
||||
return result, count
|
||||
})
|
||||
info.SetDeleteFn(func(ids []string) error {
|
||||
for _, id := range ids {
|
||||
value, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.DeleteUser(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
info.SetTable("users").SetTitle("Users").SetDescription("Users")
|
||||
|
||||
formList := userTable.GetForm()
|
||||
formList.AddField("ID", "id", db.Int, form.Default).
|
||||
FieldNotAllowEdit().
|
||||
FieldNotAllowAdd()
|
||||
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
|
||||
FieldMust().
|
||||
FieldOptions(squadOptions).
|
||||
FieldDisableWhenUpdate()
|
||||
formList.AddField("Username", "username", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate()
|
||||
formList.AddField("Type", "type", db.Varchar, form.SelectSingle).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate().
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "Hysteria", Value: "hysteria"},
|
||||
{Text: "Hysteria2", Value: "hysteria2"},
|
||||
{Text: "Trojan", Value: "trojan"},
|
||||
{Text: "TUIC", Value: "tuic"},
|
||||
{Text: "VLESS", Value: "vless"},
|
||||
{Text: "VMess", Value: "vmess"},
|
||||
}).
|
||||
FieldOnChooseOptionsHide([]string{""}, "inbound").
|
||||
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic"}, "uuid").
|
||||
FieldOnChooseOptionsHide([]string{"", "vless", "vmess"}, "password").
|
||||
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic", "vmess"}, "flow").
|
||||
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic", "vless"}, "alter_id")
|
||||
formList.AddField("Inbound", "inbound", db.Varchar, form.Text).
|
||||
FieldMust().
|
||||
FieldDisplayButCanNotEditWhenUpdate().
|
||||
FieldOptionInitFn(func(val types.FieldModel) types.FieldOptions {
|
||||
return types.FieldOptions{
|
||||
{Value: val.Value, Text: val.Value, Selected: true},
|
||||
}
|
||||
})
|
||||
formList.AddField("UUID", "uuid", db.Varchar, form.Text)
|
||||
formList.AddField("Password", "password", db.Varchar, form.Text)
|
||||
formList.AddField("Flow", "flow", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Text: "xtls-rprx-vision", Value: "xtls-rprx-vision"},
|
||||
})
|
||||
formList.AddField("Alter ID", "alter_id", db.Varchar, form.Number).
|
||||
FieldDefault("0")
|
||||
|
||||
formList.SetInsertFn(func(values mForm.Values) (err error) {
|
||||
squadIDs := make([]int, len(values["squad_ids[]"]))
|
||||
for i, rawSquadID := range values["squad_ids[]"] {
|
||||
squadID, err := strconv.Atoi(rawSquadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
squadIDs[i] = squadID
|
||||
}
|
||||
var alterId int
|
||||
if value := values.Get("alter_id"); value != "" {
|
||||
alterId, err = strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = manager.CreateUser(CM.UserCreate{
|
||||
SquadIDs: squadIDs,
|
||||
Username: values.Get("username"),
|
||||
Type: values.Get("type"),
|
||||
Inbound: values.Get("inbound"),
|
||||
UUID: values.Get("uuid"),
|
||||
Password: values.Get("password"),
|
||||
Flow: values.Get("flow"),
|
||||
AlterID: alterId,
|
||||
})
|
||||
if err != nil {
|
||||
if ve, ok := err.(validator.ValidationErrors); ok {
|
||||
var errors []string
|
||||
for _, e := range ve {
|
||||
switch e.Tag() {
|
||||
case "required":
|
||||
errors = append(errors, e.StructField()+": required field missing")
|
||||
case "uuid4":
|
||||
errors = append(errors, e.StructField()+": invalid UUID")
|
||||
default:
|
||||
errors = append(errors, e.StructField()+": invalid request")
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("%s", strings.Join(errors, "<br>"))
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
formList.SetUpdateFn(func(values mForm.Values) (err error) {
|
||||
id, err := strconv.Atoi(values.Get("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var alterId int
|
||||
if value := values.Get("alter_id"); value != "" {
|
||||
alterId, err = strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = manager.UpdateUser(id, CM.UserUpdate{
|
||||
UUID: values.Get("uuid"),
|
||||
Password: values.Get("password"),
|
||||
Flow: values.Get("flow"),
|
||||
AlterID: alterId,
|
||||
})
|
||||
return
|
||||
})
|
||||
|
||||
formList.SetTable("users").SetTitle("Users").SetDescription("Users")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
164
service/manager/constant/dto.go
Normal file
164
service/manager/constant/dto.go
Normal 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"`
|
||||
}
|
||||
5
service/manager/constant/error.go
Normal file
5
service/manager/constant/error.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
var ErrNotFound = E.New("not found")
|
||||
48
service/manager/constant/manager.go
Normal file
48
service/manager/constant/manager.go
Normal 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)
|
||||
}
|
||||
20
service/manager/constant/node.go
Normal file
20
service/manager/constant/node.go
Normal 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
|
||||
}
|
||||
38
service/manager/constant/repository.go
Normal file
38
service/manager/constant/repository.go
Normal 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)
|
||||
}
|
||||
155
service/manager/repository/postgresql/filter.go
Normal file
155
service/manager/repository/postgresql/filter.go
Normal 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
|
||||
}
|
||||
}
|
||||
132
service/manager/repository/postgresql/migration.go
Normal file
132
service/manager/repository/postgresql/migration.go
Normal 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()
|
||||
}
|
||||
1347
service/manager/repository/postgresql/repository.go
Normal file
1347
service/manager/repository/postgresql/repository.go
Normal file
File diff suppressed because it is too large
Load Diff
598
service/manager/service.go
Normal file
598
service/manager/service.go
Normal 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
|
||||
}
|
||||
20
service/manager/service_stub.go
Normal file
20
service/manager/service_stub.go
Normal 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`)
|
||||
})
|
||||
}
|
||||
18
service/node/constant/bandwidth.go
Normal file
18
service/node/constant/bandwidth.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
type BandwidthLimiterManager interface {
|
||||
AddBandwidthLimiterStrategyManager(outbound adapter.Outbound) error
|
||||
GetBandwidthLimiterStrategyManager(tag string) (BandwidthLimiterStrategyManager, bool)
|
||||
GetBandwidthLimiterStrategyManagerTags() []string
|
||||
}
|
||||
|
||||
type BandwidthLimiterStrategyManager interface {
|
||||
UpdateBandwidthLimiter(limiter C.BandwidthLimiter)
|
||||
UpdateBandwidthLimiters(limiter []C.BandwidthLimiter)
|
||||
DeleteBandwidthLimiter(username string)
|
||||
}
|
||||
18
service/node/constant/connection.go
Normal file
18
service/node/constant/connection.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
type ConnectionLimiterManager interface {
|
||||
AddConnectionLimiterStrategyManager(outbound adapter.Outbound) error
|
||||
GetConnectionLimiterStrategyManager(tag string) (ConnectionLimiterStrategyManager, bool)
|
||||
GetConnectionLimiterStrategyManagerTags() []string
|
||||
}
|
||||
|
||||
type ConnectionLimiterStrategyManager interface {
|
||||
UpdateConnectionLimiter(limiter C.ConnectionLimiter)
|
||||
UpdateConnectionLimiters(limiter []C.ConnectionLimiter)
|
||||
DeleteConnectionLimiter(username string)
|
||||
}
|
||||
18
service/node/constant/inbound.go
Normal file
18
service/node/constant/inbound.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
type InboundManager interface {
|
||||
AddUserManager(inbound adapter.Inbound) error
|
||||
GetUserManager(tag string) (UserManager, bool)
|
||||
GetUserManagerTags() []string
|
||||
}
|
||||
|
||||
type UserManager interface {
|
||||
UpdateUser(user C.User)
|
||||
UpdateUsers(users []C.User)
|
||||
DeleteUser(username string)
|
||||
}
|
||||
88
service/node/inbound/hysteria.go
Normal file
88
service/node/inbound/hysteria.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type HysteriaManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*HysteriaUserManager
|
||||
}
|
||||
|
||||
func NewHysteriaManager() *HysteriaManager {
|
||||
return &HysteriaManager{
|
||||
inbounds: make(map[string]*HysteriaUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &HysteriaUserManager{
|
||||
inbound: inbound.(*hysteria.Inbound),
|
||||
usersMap: make(map[string]option.HysteriaUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type HysteriaUserManager struct {
|
||||
inbound *hysteria.Inbound
|
||||
usersMap map[string]option.HysteriaUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *HysteriaUserManager) postUpdate() {
|
||||
users := make([]option.HysteriaUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *HysteriaUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.HysteriaUser{Name: user.Username, AuthString: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *HysteriaUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.HysteriaUser{Name: user.Username, AuthString: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *HysteriaUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
88
service/node/inbound/hysteria2.go
Normal file
88
service/node/inbound/hysteria2.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria2"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type Hysteria2Manager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*Hysteria2UserManager
|
||||
}
|
||||
|
||||
func NewHysteria2Manager() *Hysteria2Manager {
|
||||
return &Hysteria2Manager{
|
||||
inbounds: make(map[string]*Hysteria2UserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &Hysteria2UserManager{
|
||||
inbound: inbound.(*hysteria2.Inbound),
|
||||
usersMap: make(map[string]option.Hysteria2User),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type Hysteria2UserManager struct {
|
||||
inbound *hysteria2.Inbound
|
||||
usersMap map[string]option.Hysteria2User
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *Hysteria2UserManager) postUpdate() {
|
||||
users := make([]option.Hysteria2User, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *Hysteria2UserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.Hysteria2User{Name: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *Hysteria2UserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.Hysteria2User{Name: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *Hysteria2UserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
88
service/node/inbound/trojan.go
Normal file
88
service/node/inbound/trojan.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/trojan"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type TrojanManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*TrojanUserManager
|
||||
}
|
||||
|
||||
func NewTrojanManager() *TrojanManager {
|
||||
return &TrojanManager{
|
||||
inbounds: make(map[string]*TrojanUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TrojanManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &TrojanUserManager{
|
||||
inbound: inbound.(*trojan.Inbound),
|
||||
usersMap: make(map[string]option.TrojanUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *TrojanManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *TrojanManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type TrojanUserManager struct {
|
||||
inbound *trojan.Inbound
|
||||
usersMap map[string]option.TrojanUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *TrojanUserManager) postUpdate() {
|
||||
users := make([]option.TrojanUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *TrojanUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.TrojanUser{Name: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *TrojanUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.TrojanUser{Name: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *TrojanUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
88
service/node/inbound/tuic.go
Normal file
88
service/node/inbound/tuic.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/tuic"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type TUICManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*TUICUserManager
|
||||
}
|
||||
|
||||
func NewTUICManager() *TUICManager {
|
||||
return &TUICManager{
|
||||
inbounds: make(map[string]*TUICUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TUICManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &TUICUserManager{
|
||||
inbound: inbound.(*tuic.Inbound),
|
||||
usersMap: make(map[string]option.TUICUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *TUICManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *TUICManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type TUICUserManager struct {
|
||||
inbound *tuic.Inbound
|
||||
usersMap map[string]option.TUICUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *TUICUserManager) postUpdate() {
|
||||
users := make([]option.TUICUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *TUICUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.TUICUser{Name: user.Username, UUID: user.UUID, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *TUICUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.TUICUser{Name: user.Username, UUID: user.UUID, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *TUICUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
88
service/node/inbound/vless.go
Normal file
88
service/node/inbound/vless.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/vless"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type VLESSManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*VLESSUserManager
|
||||
}
|
||||
|
||||
func NewVLESSManager() *VLESSManager {
|
||||
return &VLESSManager{
|
||||
inbounds: make(map[string]*VLESSUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *VLESSManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &VLESSUserManager{
|
||||
inbound: inbound.(*vless.Inbound),
|
||||
usersMap: make(map[string]option.VLESSUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *VLESSManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *VLESSManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type VLESSUserManager struct {
|
||||
inbound *vless.Inbound
|
||||
usersMap map[string]option.VLESSUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *VLESSUserManager) postUpdate() {
|
||||
users := make([]option.VLESSUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *VLESSUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.VLESSUser{Name: user.Username, UUID: user.UUID, Flow: user.Flow}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *VLESSUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.VLESSUser{Name: user.Username, UUID: user.UUID, Flow: user.Flow}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *VLESSUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
88
service/node/inbound/vmess.go
Normal file
88
service/node/inbound/vmess.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/vmess"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type VMessManager struct {
|
||||
inbounds map[string]*VMessUserManager
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewVMessManager() *VMessManager {
|
||||
return &VMessManager{
|
||||
inbounds: make(map[string]*VMessUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *VMessManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &VMessUserManager{
|
||||
inbound: inbound.(*vmess.Inbound),
|
||||
usersMap: make(map[string]option.VMessUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *VMessManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *VMessManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag, _ := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type VMessUserManager struct {
|
||||
inbound *vmess.Inbound
|
||||
usersMap map[string]option.VMessUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *VMessUserManager) postUpdate() {
|
||||
users := make([]option.VMessUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *VMessUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.VMessUser{Name: user.Username, UUID: user.UUID, AlterId: user.AlterID}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *VMessUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.VMessUser{Name: user.Username, UUID: user.UUID, AlterId: user.AlterID}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *VMessUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
107
service/node/limiter/bandwidth.go
Normal file
107
service/node/limiter/bandwidth.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/protocol/limiter/bandwidth"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ManagedBandwidthStrategy interface {
|
||||
UpdateStrategies(strategies map[string]bandwidth.BandwidthStrategy)
|
||||
}
|
||||
|
||||
type BandwidthLimiterManager struct {
|
||||
managers map[string]*BandwidthLimiterStrategyManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewBandwidthLimiterManager() *BandwidthLimiterManager {
|
||||
return &BandwidthLimiterManager{
|
||||
managers: make(map[string]*BandwidthLimiterStrategyManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BandwidthLimiterManager) AddBandwidthLimiterStrategyManager(outbound adapter.Outbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
limiter, ok := outbound.(*bandwidth.Outbound)
|
||||
if !ok {
|
||||
return E.New("invalid bandwidth limiter: ", outbound.Tag())
|
||||
}
|
||||
strategy, ok := limiter.GetStrategy().(ManagedBandwidthStrategy)
|
||||
if !ok {
|
||||
return E.New("strategy for outbound ", outbound.Tag(), " is not manager")
|
||||
}
|
||||
m.managers[outbound.Tag()] = &BandwidthLimiterStrategyManager{
|
||||
strategy: strategy,
|
||||
strategiesMap: make(map[string]bandwidth.BandwidthStrategy),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BandwidthLimiterManager) GetBandwidthLimiterStrategyManager(tag string) (constant.BandwidthLimiterStrategyManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
manager, ok := m.managers[tag]
|
||||
return manager, ok
|
||||
}
|
||||
|
||||
func (m *BandwidthLimiterManager) GetBandwidthLimiterStrategyManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.managers))
|
||||
for tag, _ := range m.managers {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type BandwidthLimiterStrategyManager struct {
|
||||
strategy ManagedBandwidthStrategy
|
||||
strategiesMap map[string]bandwidth.BandwidthStrategy
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *BandwidthLimiterStrategyManager) postUpdate() {
|
||||
i.strategy.UpdateStrategies(i.strategiesMap)
|
||||
}
|
||||
|
||||
func (i *BandwidthLimiterStrategyManager) UpdateBandwidthLimiter(limiter CM.BandwidthLimiter) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
strategy, err := bandwidth.CreateStrategy(limiter.Strategy, limiter.Mode, limiter.ConnectionType, limiter.RawSpeed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
i.strategiesMap[limiter.Username] = strategy
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *BandwidthLimiterStrategyManager) UpdateBandwidthLimiters(limiters []CM.BandwidthLimiter) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.strategiesMap)
|
||||
newStrategiesMap := make(map[string]bandwidth.BandwidthStrategy)
|
||||
for _, limiter := range limiters {
|
||||
strategy, err := bandwidth.CreateStrategy(limiter.Strategy, limiter.Mode, limiter.ConnectionType, limiter.RawSpeed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newStrategiesMap[limiter.Username] = strategy
|
||||
}
|
||||
i.strategiesMap = newStrategiesMap
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *BandwidthLimiterStrategyManager) DeleteBandwidthLimiter(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.strategiesMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
195
service/node/limiter/connection.go
Normal file
195
service/node/limiter/connection.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/protocol/limiter/connection"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ManagedConnectionStrategy interface {
|
||||
UpdateStrategies(strategies map[string]connection.ConnectionStrategy)
|
||||
}
|
||||
|
||||
type ConnectionLimiterManager struct {
|
||||
nodeManager CM.NodeManager
|
||||
managers map[string]*ConnectionLimiterStrategyManager
|
||||
logger log.Logger
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnectionLimiterManager(nodeManager CM.NodeManager, logger log.Logger) *ConnectionLimiterManager {
|
||||
return &ConnectionLimiterManager{
|
||||
nodeManager: nodeManager,
|
||||
managers: make(map[string]*ConnectionLimiterStrategyManager),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConnectionLimiterManager) AddConnectionLimiterStrategyManager(outbound adapter.Outbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
limiter, ok := outbound.(*connection.Outbound)
|
||||
if !ok {
|
||||
return E.New("invalid connection limiter: ", outbound.Tag())
|
||||
}
|
||||
strategy, ok := limiter.GetStrategy().(ManagedConnectionStrategy)
|
||||
if !ok {
|
||||
return E.New("strategy ", strategy, " is not manager")
|
||||
}
|
||||
m.managers[outbound.Tag()] = &ConnectionLimiterStrategyManager{
|
||||
strategy: strategy,
|
||||
strategiesMap: make(map[string]connection.ConnectionStrategy),
|
||||
manager: m,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ConnectionLimiterManager) GetConnectionLimiterStrategyManager(tag string) (constant.ConnectionLimiterStrategyManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
manager, ok := m.managers[tag]
|
||||
return manager, ok
|
||||
}
|
||||
|
||||
func (m *ConnectionLimiterManager) GetConnectionLimiterStrategyManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.managers))
|
||||
for tag, _ := range m.managers {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type ConnectionLimiterStrategyManager struct {
|
||||
strategy ManagedConnectionStrategy
|
||||
strategiesMap map[string]connection.ConnectionStrategy
|
||||
tag string
|
||||
manager *ConnectionLimiterManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) postUpdate() {
|
||||
i.strategy.UpdateStrategies(i.strategiesMap)
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) UpdateConnectionLimiter(limiter CM.ConnectionLimiter) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
lock, err := i.createLock(limiter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strategy, err := connection.CreateStrategy(limiter.Strategy, limiter.ConnectionType, lock)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
i.strategiesMap[limiter.Username] = strategy
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) UpdateConnectionLimiters(limiters []CM.ConnectionLimiter) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.strategiesMap)
|
||||
newStrategiesMap := make(map[string]connection.ConnectionStrategy)
|
||||
for _, limiter := range limiters {
|
||||
lock, err := i.createLock(limiter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strategy, err := connection.CreateStrategy(limiter.Strategy, limiter.ConnectionType, lock)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newStrategiesMap[limiter.Username] = strategy
|
||||
}
|
||||
i.strategiesMap = newStrategiesMap
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) DeleteConnectionLimiter(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.strategiesMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) createLock(limiter CM.ConnectionLimiter) (connection.LockIDGetter, error) {
|
||||
switch limiter.LockType {
|
||||
case "manager":
|
||||
return i.newManagerLock(limiter.ID), nil
|
||||
case "":
|
||||
return connection.NewDefaultLock(limiter.Count), nil
|
||||
default:
|
||||
return nil, E.New("unknown lock type \"", limiter.LockType, "\"")
|
||||
}
|
||||
}
|
||||
|
||||
type ManagerLock struct {
|
||||
handleId string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
handles uint32
|
||||
}
|
||||
|
||||
func (i *ConnectionLimiterStrategyManager) newManagerLock(limiterId int) connection.LockIDGetter {
|
||||
conns := make(map[string]*ManagerLock)
|
||||
mtx := sync.Mutex{}
|
||||
return func(id string) (connection.CloseHandlerFunc, context.Context, error) {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
conn, ok := conns[id]
|
||||
if !ok {
|
||||
nodeManager := i.manager.nodeManager
|
||||
handleId, err := nodeManager.AcquireLock(limiterId, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second * 5):
|
||||
err := nodeManager.RefreshLock(limiterId, id, handleId)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
conn = &ManagerLock{
|
||||
handleId: handleId,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
conns[id] = conn
|
||||
}
|
||||
conn.handles++
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
conn.handles--
|
||||
if conn.handles == 0 {
|
||||
conn.cancel()
|
||||
i.manager.nodeManager.ReleaseLock(limiterId, id, conn.handleId)
|
||||
delete(conns, id)
|
||||
}
|
||||
})
|
||||
}, conn.ctx, nil
|
||||
}
|
||||
}
|
||||
235
service/node/service.go
Normal file
235
service/node/service.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
"github.com/sagernet/sing-box/service/node/inbound"
|
||||
"github.com/sagernet/sing-box/service/node/limiter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.NodeServiceOptions](registry, C.TypeNode, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
inboundManagers map[string]constant.InboundManager
|
||||
bandwidthManager constant.BandwidthLimiterManager
|
||||
connectionManager constant.ConnectionLimiterManager
|
||||
options option.NodeServiceOptions
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeServiceOptions) (adapter.Service, error) {
|
||||
return &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeManager, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
|
||||
serviceManager, ok := boxManager.Get(s.options.Manager)
|
||||
if !ok {
|
||||
return E.New("manager ", s.options.Manager, " not found")
|
||||
}
|
||||
nodeManager, ok := serviceManager.(CM.NodeManager)
|
||||
if !ok {
|
||||
return E.New("invalid ", s.options.Manager, " manager")
|
||||
}
|
||||
inboundManager := service.FromContext[adapter.InboundManager](s.ctx)
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](s.ctx)
|
||||
s.inboundManagers = map[string]constant.InboundManager{
|
||||
"hysteria": inbound.NewHysteriaManager(),
|
||||
"hysteria2": inbound.NewHysteria2Manager(),
|
||||
"trojan": inbound.NewTrojanManager(),
|
||||
"tuic": inbound.NewTUICManager(),
|
||||
"vless": inbound.NewVLESSManager(),
|
||||
"vmess": inbound.NewVMessManager(),
|
||||
}
|
||||
s.connectionManager = limiter.NewConnectionLimiterManager(nodeManager, s.logger)
|
||||
s.bandwidthManager = limiter.NewBandwidthLimiterManager()
|
||||
for _, tag := range s.options.Inbounds {
|
||||
inbound, ok := inboundManager.Get(tag)
|
||||
if !ok {
|
||||
return E.New("inbound ", tag, " not found")
|
||||
}
|
||||
inboundManager, ok := s.inboundManagers[inbound.Type()]
|
||||
if !ok {
|
||||
return E.New("inbound manager for ", tag, " not found")
|
||||
}
|
||||
err := inboundManager.AddUserManager(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, limiter := range s.options.ConnectionLimiters {
|
||||
outbound, ok := outboundManager.Outbound(limiter)
|
||||
if !ok {
|
||||
return E.New("outbound ", limiter, " not found")
|
||||
}
|
||||
err := s.connectionManager.AddConnectionLimiterStrategyManager(outbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, limiter := range s.options.BandwidthLimiters {
|
||||
outbound, ok := outboundManager.Outbound(limiter)
|
||||
if !ok {
|
||||
return E.New("outbound ", limiter, " not found")
|
||||
}
|
||||
err := s.bandwidthManager.AddBandwidthLimiterStrategyManager(outbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nodeManager.AddNode(s.options.UUID, s)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUser(user CM.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.inboundManagers[user.Type]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
userManager, ok := manager.GetUserManager(user.Inbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
userManager.UpdateUser(user)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUsers(users []CM.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
typedUsers := make(map[string][]CM.User)
|
||||
for _, user := range users {
|
||||
u, ok := typedUsers[user.Type]
|
||||
if !ok {
|
||||
typedUsers[user.Type] = make([]CM.User, 0)
|
||||
}
|
||||
typedUsers[user.Type] = append(u, user)
|
||||
}
|
||||
for type_, users := range typedUsers {
|
||||
manager, ok := s.inboundManagers[type_]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, user := range users {
|
||||
userManager, ok := manager.GetUserManager(user.Inbound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
userManager.UpdateUsers(users)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUser(user CM.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.inboundManagers[user.Type]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
userManager, ok := manager.GetUserManager(user.Inbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
userManager.DeleteUser(user.Username)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConnectionLimiter(limiter CM.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
manager.UpdateConnectionLimiter(limiter)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConnectionLimiters(limiters []CM.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
for _, limiter := range limiters {
|
||||
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
manager.UpdateConnectionLimiters(limiters)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) DeleteConnectionLimiter(limiter CM.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
manager.DeleteConnectionLimiter(limiter.Username)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateBandwidthLimiter(limiter CM.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
manager.UpdateBandwidthLimiter(limiter)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateBandwidthLimiters(limiters []CM.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
for _, limiter := range limiters {
|
||||
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
manager.UpdateBandwidthLimiters(limiters)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) DeleteBandwidthLimiter(limiter CM.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
manager.DeleteBandwidthLimiter(limiter.Username)
|
||||
}
|
||||
|
||||
func (s *Service) IsLocal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) IsOnline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
return nil
|
||||
}
|
||||
274
service/node_manager/client/service.go
Normal file
274
service/node_manager/client/service.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/node_manager/manager"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.NodeManagerClientServiceOptions](registry, C.TypeNodeManagerClient, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
dialer N.Dialer
|
||||
creds credentials.TransportCredentials
|
||||
options option.NodeManagerClientServiceOptions
|
||||
|
||||
conn *grpc.ClientConn
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerClientServiceOptions) (adapter.Service, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := insecure.NewCredentials()
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds = &tlsCreds{tlsConfig}
|
||||
}
|
||||
return &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeManager, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: outboundDialer,
|
||||
creds: creds,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AddNode(uuid string, node CM.ConnectedNode) error {
|
||||
go func() {
|
||||
isRetry := false
|
||||
for {
|
||||
if !isRetry {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
isRetry = true
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
break
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
conn, err := s.getConn()
|
||||
if err != nil {
|
||||
s.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
client := pb.NewManagerClient(conn)
|
||||
stream, err := client.AddNode(s.ctx, &pb.Node{Uuid: uuid})
|
||||
if err != nil {
|
||||
s.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
err = s.handler(node, stream)
|
||||
if err != nil {
|
||||
s.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) AcquireLock(limiterId int, id string) (string, error) {
|
||||
conn, err := s.getConn()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client := pb.NewManagerClient(conn)
|
||||
lockReply, err := client.AcquireLock(s.ctx, &pb.AcquireLockRequest{LimiterId: int32(limiterId), Id: id})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lockReply.HandleId, err
|
||||
}
|
||||
|
||||
func (s *Service) RefreshLock(limiterId int, id string, handleId string) error {
|
||||
conn, err := s.getConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := pb.NewManagerClient(conn)
|
||||
_, err = client.RefreshLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) ReleaseLock(limiterId int, id string, handleId string) error {
|
||||
conn, err := s.getConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := pb.NewManagerClient(conn)
|
||||
_, err = client.ReleaseLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) getConn() (*grpc.ClientConn, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if s.conn != nil {
|
||||
state := s.conn.GetState()
|
||||
if state != connectivity.Shutdown && state != connectivity.TransientFailure {
|
||||
return s.conn, nil
|
||||
}
|
||||
}
|
||||
for {
|
||||
conn, err := s.createConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.conn = conn
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) createConn() (*grpc.ClientConn, error) {
|
||||
conn, err := grpc.NewClient(
|
||||
s.options.ServerOptions.Build().String(),
|
||||
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return s.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
|
||||
}),
|
||||
grpc.WithTransportCredentials(s.creds),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (s *Service) handler(node CM.ConnectedNode, stream grpc.ServerStreamingClient[pb.NodeData]) error {
|
||||
for {
|
||||
data, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch data.Op {
|
||||
case pb.OpType_updateUser:
|
||||
s.logger.DebugContext(s.ctx, "update user")
|
||||
node.UpdateUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
|
||||
case pb.OpType_updateUsers:
|
||||
s.logger.DebugContext(s.ctx, "update users")
|
||||
users := data.Data.(*pb.NodeData_Users).Users.Values
|
||||
convertedUsers := make([]CM.User, len(users))
|
||||
for i, user := range users {
|
||||
convertedUsers[i] = s.convertUser(user)
|
||||
}
|
||||
node.UpdateUsers(convertedUsers)
|
||||
case pb.OpType_deleteUser:
|
||||
s.logger.DebugContext(s.ctx, "delete user")
|
||||
node.DeleteUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
|
||||
|
||||
case pb.OpType_updateConnectionLimiter:
|
||||
s.logger.DebugContext(s.ctx, "update connection limiter")
|
||||
node.UpdateConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
|
||||
case pb.OpType_updateConnectionLimiters:
|
||||
s.logger.DebugContext(s.ctx, "update connection limiters")
|
||||
limiters := data.Data.(*pb.NodeData_ConnectionLimiters).ConnectionLimiters.Values
|
||||
convertedLimiters := make([]CM.ConnectionLimiter, len(limiters))
|
||||
for i, limiter := range limiters {
|
||||
convertedLimiters[i] = s.convertConnectionLimiter(limiter)
|
||||
}
|
||||
node.UpdateConnectionLimiters(convertedLimiters)
|
||||
case pb.OpType_deleteConnectionLimiter:
|
||||
s.logger.DebugContext(s.ctx, "delete connection limiter")
|
||||
node.DeleteConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
|
||||
|
||||
case pb.OpType_updateBandwidthLimiter:
|
||||
s.logger.DebugContext(s.ctx, "update bandwidth limiter")
|
||||
node.UpdateBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
|
||||
case pb.OpType_updateBandwidthLimiters:
|
||||
s.logger.DebugContext(s.ctx, "update bandwidth limiters")
|
||||
limiters := data.Data.(*pb.NodeData_BandwidthLimiters).BandwidthLimiters.Values
|
||||
convertedLimiters := make([]CM.BandwidthLimiter, len(limiters))
|
||||
for i, limiter := range limiters {
|
||||
convertedLimiters[i] = s.convertBandwidthLimiter(limiter)
|
||||
}
|
||||
node.UpdateBandwidthLimiters(convertedLimiters)
|
||||
case pb.OpType_deleteBandwidthLimiter:
|
||||
s.logger.DebugContext(s.ctx, "delete bandwidth limiter")
|
||||
node.DeleteBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) convertUser(user *pb.User) CM.User {
|
||||
return CM.User{
|
||||
ID: int(user.Id),
|
||||
Username: user.Username,
|
||||
Type: user.Type,
|
||||
Inbound: user.Inbound,
|
||||
UUID: user.Uuid,
|
||||
Password: user.Password,
|
||||
Flow: user.Flow,
|
||||
AlterID: int(user.AlterId),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) convertBandwidthLimiter(limiter *pb.BandwidthLimiter) CM.BandwidthLimiter {
|
||||
return CM.BandwidthLimiter{
|
||||
ID: int(limiter.Id),
|
||||
Username: limiter.Username,
|
||||
Outbound: limiter.Outbound,
|
||||
Strategy: limiter.Strategy,
|
||||
Mode: limiter.Mode,
|
||||
ConnectionType: limiter.ConnectionType,
|
||||
Speed: limiter.Speed,
|
||||
RawSpeed: limiter.RawSpeed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) convertConnectionLimiter(limiter *pb.ConnectionLimiter) CM.ConnectionLimiter {
|
||||
return CM.ConnectionLimiter{
|
||||
ID: int(limiter.Id),
|
||||
Username: limiter.Username,
|
||||
Outbound: limiter.Outbound,
|
||||
Strategy: limiter.Strategy,
|
||||
ConnectionType: limiter.ConnectionType,
|
||||
LockType: limiter.LockType,
|
||||
Count: limiter.Count,
|
||||
}
|
||||
}
|
||||
44
service/node_manager/client/tls.go
Normal file
44
service/node_manager/client/tls.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
type tlsCreds struct {
|
||||
// TLS configuration
|
||||
config tls.Config
|
||||
}
|
||||
|
||||
func (c tlsCreds) Info() credentials.ProtocolInfo {
|
||||
return credentials.ProtocolInfo{
|
||||
SecurityProtocol: "tls",
|
||||
SecurityVersion: "1.2",
|
||||
ServerName: c.config.ServerName(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
conn, err := tls.ClientHandshake(ctx, rawConn, c.config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return conn, credentials.TLSInfo{State: conn.ConnectionState()}, err
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
return nil, nil, E.New("not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsCreds) Clone() credentials.TransportCredentials {
|
||||
return &tlsCreds{config: c.config.Clone()}
|
||||
}
|
||||
|
||||
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
|
||||
c.config.SetServerName(serverNameOverride)
|
||||
return nil
|
||||
}
|
||||
1023
service/node_manager/manager/manager.pb.go
Normal file
1023
service/node_manager/manager/manager.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
104
service/node_manager/manager/manager.proto
Normal file
104
service/node_manager/manager/manager.proto
Normal file
@@ -0,0 +1,104 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/sagernet/sing-box/service/remotemanager/manager";
|
||||
|
||||
package manager.v1;
|
||||
|
||||
service Manager {
|
||||
rpc AddNode (Node) returns (stream NodeData);
|
||||
rpc AcquireLock(AcquireLockRequest) returns (LockData);
|
||||
rpc RefreshLock(LockData) returns (Empty);
|
||||
rpc ReleaseLock(LockData) returns (Empty);
|
||||
}
|
||||
|
||||
message Node {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
enum OpType {
|
||||
updateUsers = 0;
|
||||
updateUser = 1;
|
||||
deleteUser = 2;
|
||||
|
||||
updateBandwidthLimiters = 3;
|
||||
updateBandwidthLimiter = 4;
|
||||
deleteBandwidthLimiter = 5;
|
||||
|
||||
updateConnectionLimiters = 6;
|
||||
updateConnectionLimiter = 7;
|
||||
deleteConnectionLimiter = 8;
|
||||
}
|
||||
|
||||
message User {
|
||||
int32 id = 1;
|
||||
string username = 3;
|
||||
string type = 4;
|
||||
string inbound = 5;
|
||||
string uuid = 6;
|
||||
string password = 7;
|
||||
string flow = 8;
|
||||
int32 alter_id = 9;
|
||||
}
|
||||
|
||||
message UserList {
|
||||
repeated User values = 1;
|
||||
}
|
||||
|
||||
message BandwidthLimiter {
|
||||
int32 id = 1;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string mode = 6;
|
||||
string connection_type = 7;
|
||||
string speed = 8;
|
||||
uint64 raw_speed = 9;
|
||||
}
|
||||
|
||||
message BandwidthLimiterList {
|
||||
repeated BandwidthLimiter values = 1;
|
||||
}
|
||||
|
||||
message ConnectionLimiter {
|
||||
int32 id = 1;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string connection_type = 6;
|
||||
string lock_type = 7;
|
||||
uint32 count = 8;
|
||||
}
|
||||
|
||||
message ConnectionLimiterList {
|
||||
repeated ConnectionLimiter values = 1;
|
||||
}
|
||||
|
||||
message NodeData {
|
||||
|
||||
OpType op = 1;
|
||||
|
||||
oneof data {
|
||||
UserList users = 2;
|
||||
User user = 3;
|
||||
BandwidthLimiterList bandwidth_limiters = 4;
|
||||
BandwidthLimiter bandwidth_limiter = 5;
|
||||
ConnectionLimiterList connection_limiters = 6;
|
||||
ConnectionLimiter connection_limiter = 7;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
message AcquireLockRequest {
|
||||
int32 limiter_id = 1;
|
||||
string id = 2;
|
||||
}
|
||||
|
||||
message LockData {
|
||||
int32 limiter_id = 1;
|
||||
string id = 2;
|
||||
string handleId = 3;
|
||||
}
|
||||
|
||||
message Empty {
|
||||
|
||||
}
|
||||
239
service/node_manager/manager/manager_grpc.pb.go
Normal file
239
service/node_manager/manager/manager_grpc.pb.go
Normal file
@@ -0,0 +1,239 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.1
|
||||
// source: manager/manager.proto
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Manager_AddNode_FullMethodName = "/manager.v1.Manager/AddNode"
|
||||
Manager_AcquireLock_FullMethodName = "/manager.v1.Manager/AcquireLock"
|
||||
Manager_RefreshLock_FullMethodName = "/manager.v1.Manager/RefreshLock"
|
||||
Manager_ReleaseLock_FullMethodName = "/manager.v1.Manager/ReleaseLock"
|
||||
)
|
||||
|
||||
// ManagerClient is the client API for Manager service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ManagerClient interface {
|
||||
AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error)
|
||||
AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error)
|
||||
RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
|
||||
ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
|
||||
}
|
||||
|
||||
type managerClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewManagerClient(cc grpc.ClientConnInterface) ManagerClient {
|
||||
return &managerClient{cc}
|
||||
}
|
||||
|
||||
func (c *managerClient) AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &Manager_ServiceDesc.Streams[0], Manager_AddNode_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[Node, NodeData]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Manager_AddNodeClient = grpc.ServerStreamingClient[NodeData]
|
||||
|
||||
func (c *managerClient) AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(LockData)
|
||||
err := c.cc.Invoke(ctx, Manager_AcquireLock_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *managerClient) RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Empty)
|
||||
err := c.cc.Invoke(ctx, Manager_RefreshLock_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *managerClient) ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Empty)
|
||||
err := c.cc.Invoke(ctx, Manager_ReleaseLock_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ManagerServer is the server API for Manager service.
|
||||
// All implementations must embed UnimplementedManagerServer
|
||||
// for forward compatibility.
|
||||
type ManagerServer interface {
|
||||
AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error
|
||||
AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error)
|
||||
RefreshLock(context.Context, *LockData) (*Empty, error)
|
||||
ReleaseLock(context.Context, *LockData) (*Empty, error)
|
||||
mustEmbedUnimplementedManagerServer()
|
||||
}
|
||||
|
||||
// UnimplementedManagerServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedManagerServer struct{}
|
||||
|
||||
func (UnimplementedManagerServer) AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error {
|
||||
return status.Error(codes.Unimplemented, "method AddNode not implemented")
|
||||
}
|
||||
func (UnimplementedManagerServer) AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AcquireLock not implemented")
|
||||
}
|
||||
func (UnimplementedManagerServer) RefreshLock(context.Context, *LockData) (*Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RefreshLock not implemented")
|
||||
}
|
||||
func (UnimplementedManagerServer) ReleaseLock(context.Context, *LockData) (*Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ReleaseLock not implemented")
|
||||
}
|
||||
func (UnimplementedManagerServer) mustEmbedUnimplementedManagerServer() {}
|
||||
func (UnimplementedManagerServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeManagerServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ManagerServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeManagerServer interface {
|
||||
mustEmbedUnimplementedManagerServer()
|
||||
}
|
||||
|
||||
func RegisterManagerServer(s grpc.ServiceRegistrar, srv ManagerServer) {
|
||||
// If the following call panics, it indicates UnimplementedManagerServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Manager_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Manager_AddNode_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(Node)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(ManagerServer).AddNode(m, &grpc.GenericServerStream[Node, NodeData]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Manager_AddNodeServer = grpc.ServerStreamingServer[NodeData]
|
||||
|
||||
func _Manager_AcquireLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AcquireLockRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ManagerServer).AcquireLock(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Manager_AcquireLock_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ManagerServer).AcquireLock(ctx, req.(*AcquireLockRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Manager_RefreshLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(LockData)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ManagerServer).RefreshLock(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Manager_RefreshLock_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ManagerServer).RefreshLock(ctx, req.(*LockData))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Manager_ReleaseLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(LockData)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ManagerServer).ReleaseLock(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Manager_ReleaseLock_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ManagerServer).ReleaseLock(ctx, req.(*LockData))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Manager_ServiceDesc is the grpc.ServiceDesc for Manager service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Manager_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "manager.v1.Manager",
|
||||
HandlerType: (*ManagerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "AcquireLock",
|
||||
Handler: _Manager_AcquireLock_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RefreshLock",
|
||||
Handler: _Manager_RefreshLock_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReleaseLock",
|
||||
Handler: _Manager_ReleaseLock_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "AddNode",
|
||||
Handler: _Manager_AddNode_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "manager/manager.proto",
|
||||
}
|
||||
203
service/node_manager/server/node.go
Normal file
203
service/node_manager/server/node.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
CS "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/node_manager/manager"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type RemoteNode struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
stream grpc.ServerStreamingServer[pb.NodeData]
|
||||
errChan chan error
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewRemoteNode(ctx context.Context, logger log.ContextLogger, stream grpc.ServerStreamingServer[pb.NodeData]) (*RemoteNode, chan error) {
|
||||
errChan := make(chan error)
|
||||
return &RemoteNode{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
stream: stream,
|
||||
errChan: errChan,
|
||||
}, errChan
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateUser(user CS.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateUser,
|
||||
Data: &pb.NodeData_User{User: s.convertUser(user)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateUsers(users []CS.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
pbUsers := make([]*pb.User, len(users))
|
||||
for i, user := range users {
|
||||
pbUsers[i] = s.convertUser(user)
|
||||
}
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateUsers,
|
||||
Data: &pb.NodeData_Users{Users: &pb.UserList{Values: pbUsers}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) DeleteUser(user CS.User) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_deleteUser,
|
||||
Data: &pb.NodeData_User{User: s.convertUser(user)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateConnectionLimiter(limiter CS.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateConnectionLimiter,
|
||||
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateConnectionLimiters(limiters []CS.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
pbLimiters := make([]*pb.ConnectionLimiter, len(limiters))
|
||||
for i, limiters := range limiters {
|
||||
pbLimiters[i] = s.convertConnectionLimiter(limiters)
|
||||
}
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateConnectionLimiters,
|
||||
Data: &pb.NodeData_ConnectionLimiters{ConnectionLimiters: &pb.ConnectionLimiterList{Values: pbLimiters}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) DeleteConnectionLimiter(limiter CS.ConnectionLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_deleteConnectionLimiter,
|
||||
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateBandwidthLimiter(limiter CS.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateBandwidthLimiter,
|
||||
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) UpdateBandwidthLimiters(limiters []CS.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
pbLimiters := make([]*pb.BandwidthLimiter, len(limiters))
|
||||
for i, limiters := range limiters {
|
||||
pbLimiters[i] = s.convertBandwidthLimiter(limiters)
|
||||
}
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_updateBandwidthLimiters,
|
||||
Data: &pb.NodeData_BandwidthLimiters{BandwidthLimiters: &pb.BandwidthLimiterList{Values: pbLimiters}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) DeleteBandwidthLimiter(limiter CS.BandwidthLimiter) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.send(&pb.NodeData{
|
||||
Op: pb.OpType_deleteBandwidthLimiter,
|
||||
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RemoteNode) IsLocal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *RemoteNode) IsOnline() bool {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
select {
|
||||
case <-s.stream.Context().Done():
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteNode) Close() error {
|
||||
s.close(E.New("server connection is closed"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteNode) send(data *pb.NodeData) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
s.close(E.New("server connection is closed"))
|
||||
return
|
||||
case <-s.stream.Context().Done():
|
||||
s.close(E.New("client connection is closed"))
|
||||
return
|
||||
default:
|
||||
}
|
||||
err := s.stream.Send(data)
|
||||
if err != nil {
|
||||
s.close(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteNode) close(err error) {
|
||||
s.errChan <- err
|
||||
close(s.errChan)
|
||||
}
|
||||
|
||||
func (s *RemoteNode) convertUser(user CS.User) *pb.User {
|
||||
return &pb.User{
|
||||
Id: int32(user.ID),
|
||||
Username: user.Username,
|
||||
Type: user.Type,
|
||||
Inbound: user.Inbound,
|
||||
Uuid: user.UUID,
|
||||
Password: user.Password,
|
||||
Flow: user.Flow,
|
||||
AlterId: int32(user.AlterID),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteNode) convertConnectionLimiter(limiter CS.ConnectionLimiter) *pb.ConnectionLimiter {
|
||||
return &pb.ConnectionLimiter{
|
||||
Id: int32(limiter.ID),
|
||||
Username: limiter.Username,
|
||||
Outbound: limiter.Outbound,
|
||||
Strategy: limiter.Strategy,
|
||||
ConnectionType: limiter.ConnectionType,
|
||||
LockType: limiter.LockType,
|
||||
Count: limiter.Count,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteNode) convertBandwidthLimiter(limiter CS.BandwidthLimiter) *pb.BandwidthLimiter {
|
||||
return &pb.BandwidthLimiter{
|
||||
Id: int32(limiter.ID),
|
||||
Username: limiter.Username,
|
||||
Outbound: limiter.Outbound,
|
||||
Strategy: limiter.Strategy,
|
||||
Mode: limiter.Mode,
|
||||
ConnectionType: limiter.ConnectionType,
|
||||
Speed: limiter.Speed,
|
||||
RawSpeed: limiter.RawSpeed,
|
||||
}
|
||||
}
|
||||
139
service/node_manager/server/service.go
Normal file
139
service/node_manager/server/service.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/node_manager/manager"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.NodeManagerServerServiceOptions](registry, C.TypeNodeManagerServer, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
pb.UnimplementedManagerServer
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
grpcServer *grpc.Server
|
||||
manager CM.Manager
|
||||
options option.NodeManagerServerServiceOptions
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerServerServiceOptions) (adapter.Service, error) {
|
||||
return &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeManager, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AddNode(node *pb.Node, stream grpc.ServerStreamingServer[pb.NodeData]) error {
|
||||
remoteNode, errChan := NewRemoteNode(s.ctx, s.logger, stream)
|
||||
err := s.manager.AddNode(node.Uuid, remoteNode)
|
||||
if err != nil {
|
||||
if err == CM.ErrNotFound {
|
||||
return err
|
||||
} else {
|
||||
s.logger.Error(err)
|
||||
return E.New("internal error")
|
||||
}
|
||||
}
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
func (s *Service) AcquireLock(ctx context.Context, request *pb.AcquireLockRequest) (*pb.LockData, error) {
|
||||
handleId, err := s.manager.AcquireLock(int(request.LimiterId), request.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.LockData{HandleId: handleId}, nil
|
||||
}
|
||||
|
||||
func (s *Service) RefreshLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
|
||||
return nil, s.manager.RefreshLock(int(data.LimiterId), data.Id, data.HandleId)
|
||||
}
|
||||
|
||||
func (s *Service) ReleaseLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
|
||||
return nil, s.manager.ReleaseLock(int(data.LimiterId), data.Id, data.HandleId)
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
|
||||
service, ok := boxManager.Get(s.options.Manager)
|
||||
if !ok {
|
||||
return E.New("manager ", s.options.Manager, " not found")
|
||||
}
|
||||
s.manager, ok = service.(CM.Manager)
|
||||
if !ok {
|
||||
return E.New("invalid", s.options.Manager, " manager")
|
||||
}
|
||||
if s.options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
s.grpcServer = grpc.NewServer()
|
||||
pb.RegisterManagerServer(s.grpcServer, s)
|
||||
go func() {
|
||||
err = s.grpcServer.Serve(tcpListener)
|
||||
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
||||
s.logger.Error("serve error: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user