fix(runtime): закрыть fix2 P0 runtime pass без legacy

This commit is contained in:
2026-04-28 03:28:19 +05:00
parent e2901db6cd
commit 96d9bbcece
31 changed files with 264 additions and 631 deletions
+9 -6
View File
@@ -14,6 +14,7 @@ import (
"net"
"net/http"
"os"
"path/filepath"
)
func resolveUiBindHost() (string, error) {
@@ -32,7 +33,6 @@ func runServer(port string) error {
defer releaseResource()
middleware.InitLog()
service.InitForward()
if err := initFile(); err != nil {
return err
}
@@ -77,16 +77,19 @@ func releaseResource() {
if err := service.ReleaseHysteria2(); err != nil {
logrus.Errorf(err.Error())
}
if err := service.RemoveByComment(); err != nil {
logrus.Errorf(err.Error())
}
}
func initFile() error {
var dirs = []string{constant.LogDir, constant.SqliteDBDir, constant.BinDir, constant.ExportPathDir}
var dirs = []string{
constant.LogDir,
constant.SqliteDBDir,
constant.BinDir,
constant.ExportPathDir,
filepath.Dir(constant.SqliteDBPath),
}
for _, item := range dirs {
if !util.Exists(item) {
if err := os.Mkdir(item, os.ModePerm); err != nil {
if err := os.MkdirAll(item, 0o750); err != nil {
logrus.Errorf("%s create err: %v", item, err)
return errors.New(fmt.Sprintf("%s create err", item))
}
-5
View File
@@ -78,11 +78,6 @@ func UpdateConfigs(c *gin.Context) {
}
}
if key == constant.Hysteria2ConfigPortHopping {
vo.Fail("port hopping out of scope in HY2XS production", c)
return
}
if key == constant.ResetTrafficCron {
resetTrafficCron, err := service.GetConfig(constant.ResetTrafficCron)
if err != nil {
+100 -20
View File
@@ -13,17 +13,18 @@ import (
"hy2xs-admin/util"
"log"
"os"
"strconv"
"strings"
"time"
)
var sqlInitStr = "CREATE TABLE IF NOT EXISTS account\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT NOT NULL UNIQUE DEFAULT '',\n pass TEXT NOT NULL DEFAULT '',\n con_pass TEXT NOT NULL DEFAULT '',\n quota INTEGER NOT NULL DEFAULT 0,\n download INTEGER NOT NULL DEFAULT 0,\n upload INTEGER NOT NULL DEFAULT 0,\n expire_time INTEGER NOT NULL DEFAULT 0,\n kick_util_time INTEGER NOT NULL DEFAULT 0,\n device_no INTEGER NOT NULL DEFAULT 3,\n role TEXT NOT NULL DEFAULT 'user',\n deleted INTEGER NOT NULL DEFAULT 0,\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nALTER TABLE account\n ADD COLUMN login_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN con_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN remark INTEGER NOT NULL DEFAULT '';\nCREATE INDEX IF NOT EXISTS account_deleted_index ON account (deleted);\nCREATE INDEX IF NOT EXISTS account_username_index ON account (username);\nCREATE INDEX IF NOT EXISTS account_con_pass_index ON account (con_pass);\nCREATE INDEX IF NOT EXISTS account_pass_index ON account (pass);\nINSERT INTO account (id, username, pass, con_pass, quota, download, upload, expire_time, device_no, role)\nSELECT 1 ,'sysadmin', '02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd', 'sysadmin.sysadmin', -1, 0, 0, 253370736000000, 6, 'admin'\n WHERE NOT EXISTS (SELECT 1 FROM account WHERE id = 1);\nCREATE TABLE IF NOT EXISTS config\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL UNIQUE DEFAULT '',\n value TEXT NOT NULL DEFAULT '',\n remark TEXT NOT NULL DEFAULT '',\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX IF NOT EXISTS config_key_index ON config (key);\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_PORT', '8081', 'HY2XS admin Web Port'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_PORT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_CONTEXT', '/', 'HY2XS admin Web Context'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_CONTEXT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_CRT_PATH', '', 'HY2XS admin CRT File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_CRT_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_KEY_PATH', '', 'HY2XS admin KEY File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_KEY_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'JWT_SECRET', hex(randomblob(10)), 'JWT Secret'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'JWT_SECRET');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_ENABLE', '0', 'Hysteria2 Switch'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_ENABLE');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG', '', 'Hysteria2 Config'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_TRAFFIC_TIME', '1', 'Hysteria2 Traffic Time'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_TRAFFIC_TIME');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG_REMARK', '', 'Hysteria2 Config Remark'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_REMARK');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG_PORT_HOPPING', '', 'Hysteria2 Config Port Hopping'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_PORT_HOPPING');\nINSERT INTO config (key, value, remark)\nSELECT 'RESET_TRAFFIC_CRON', '', 'Reset Traffic Cron'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'RESET_TRAFFIC_CRON');\nINSERT INTO config (key, value, remark)\nSELECT 'CLASH_EXTENSION', '', 'Clash Subscription Extension'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'CLASH_EXTENSION');"
var sqlInitStr = "CREATE TABLE IF NOT EXISTS account\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT NOT NULL UNIQUE DEFAULT '',\n pass TEXT NOT NULL DEFAULT '',\n con_pass TEXT NOT NULL DEFAULT '',\n quota INTEGER NOT NULL DEFAULT 0,\n download INTEGER NOT NULL DEFAULT 0,\n upload INTEGER NOT NULL DEFAULT 0,\n expire_time INTEGER NOT NULL DEFAULT 0,\n kick_util_time INTEGER NOT NULL DEFAULT 0,\n device_no INTEGER NOT NULL DEFAULT 3,\n role TEXT NOT NULL DEFAULT 'user',\n deleted INTEGER NOT NULL DEFAULT 0,\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nALTER TABLE account\n ADD COLUMN login_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN con_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN remark INTEGER NOT NULL DEFAULT '';\nALTER TABLE account\n ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0;\nCREATE INDEX IF NOT EXISTS account_deleted_index ON account (deleted);\nCREATE INDEX IF NOT EXISTS account_username_index ON account (username);\nCREATE INDEX IF NOT EXISTS account_con_pass_index ON account (con_pass);\nCREATE INDEX IF NOT EXISTS account_pass_index ON account (pass);\nCREATE TABLE IF NOT EXISTS config\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL UNIQUE DEFAULT '',\n value TEXT NOT NULL DEFAULT '',\n remark TEXT NOT NULL DEFAULT '',\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX IF NOT EXISTS config_key_index ON config (key);\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_PORT', '8081', 'HY2XS admin Web Port'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_PORT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_CONTEXT', '/', 'HY2XS admin Web Context'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_CONTEXT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_CRT_PATH', '', 'HY2XS admin CRT File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_CRT_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_KEY_PATH', '', 'HY2XS admin KEY File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_KEY_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'JWT_SECRET', hex(randomblob(10)), 'JWT Secret'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'JWT_SECRET');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_ENABLE', '0', 'Hysteria2 Switch'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_ENABLE');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG', '', 'Hysteria2 Config'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_TRAFFIC_TIME', '1', 'Hysteria2 Traffic Time'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_TRAFFIC_TIME');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG_REMARK', '', 'Hysteria2 Config Remark'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_REMARK');\nINSERT INTO config (key, value, remark)\nSELECT 'RESET_TRAFFIC_CRON', '', 'Reset Traffic Cron'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'RESET_TRAFFIC_CRON');\nINSERT INTO config (key, value, remark)\nSELECT 'CLASH_EXTENSION', '', 'Clash Subscription Extension'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'CLASH_EXTENSION');"
var sqliteDB *gorm.DB
func InitSqliteDB() error {
var err error
sqliteDB, err = gorm.Open(sqlite.Open(fmt.Sprintf("%s%s", os.Getenv("HUI_DATA"), constant.SqliteDBPath)), &gorm.Config{
sqliteDB, err = gorm.Open(sqlite.Open(constant.SqliteDBPath), &gorm.Config{
TranslateError: true,
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
@@ -72,53 +73,132 @@ func InitSql(port string) error {
}
}
if err := ensureAccountSchema(); err != nil {
return err
}
if err := ensureSecureBootstrapAdmin(); err != nil {
return err
}
if err := ensureTrafficStatsSecret(); err != nil {
return err
}
return nil
}
func envInt(name string, fallback int) int {
if raw := strings.TrimSpace(os.Getenv(name)); raw != "" {
if value, err := strconv.Atoi(raw); err == nil {
return value
}
}
return fallback
}
func envBoolAsInt(name string, fallback int) int {
raw := strings.TrimSpace(strings.ToLower(os.Getenv(name)))
if raw == "" {
return fallback
}
if raw == "true" || raw == "1" {
return 1
}
if raw == "false" || raw == "0" {
return 0
}
return fallback
}
func ensureAccountSchema() error {
if tx := sqliteDB.Exec("ALTER TABLE account ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0"); tx.Error != nil && !strings.Contains(tx.Error.Error(), "duplicate column name") {
logrus.Errorf("sqlite exec err: %v", tx.Error)
return errors.New("sqlite exec err")
}
return nil
}
func ensureSecureBootstrapAdmin() error {
admin, err := GetAccount("role = 'admin' and deleted = 0")
if err != nil {
return nil
adminUser := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_USER"))
if adminUser == "" {
adminUser = "admin"
}
if admin.Pass == nil {
return nil
}
if !util.IsBcryptHash(*admin.Pass) {
adminPassword := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD"))
if adminPassword == "" {
password, pwdErr := util.RandomString(18)
if pwdErr != nil {
return pwdErr
}
hash, hashErr := util.HashPassword(password)
if hashErr != nil {
return hashErr
adminPassword = password
}
conPass, conErr := util.RandomString(28)
forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1)
quota := int64(-1)
expireTime := int64(253370736000000)
deviceNo := int64(envInt("HY2XS_ADMIN_DEVICE_NO", 6))
role := "admin"
deleted := int64(0)
conPassSecret, conErr := util.RandomString(28)
if conErr != nil {
return conErr
}
if updateErr := UpdateAccount([]int64{*admin.Id}, map[string]interface{}{
"pass": hash,
"force_password_change": 1,
"con_pass": fmt.Sprintf("%s.%s", *admin.Username, conPass),
}); updateErr != nil {
return updateErr
conPass := fmt.Sprintf("%s.%s", adminUser, conPassSecret)
hash, hashErr := util.HashPassword(adminPassword)
if hashErr != nil {
return hashErr
}
admin, err := GetAccount("role = 'admin' and deleted = 0")
if err != nil {
username := adminUser
account := entity.Account{
Username: &username,
Pass: &hash,
ConPass: &conPass,
Quota: &quota,
ExpireTime: &expireTime,
DeviceNo: &deviceNo,
Role: &role,
Deleted: &deleted,
ForcePasswordChange: func() *int64 { v := int64(forcePasswordChange); return &v }(),
}
if _, saveErr := SaveAccount(account); saveErr != nil {
return saveErr
}
return nil
}
if admin.Pass == nil {
return nil
}
updates := map[string]interface{}{
"username": adminUser,
"pass": hash,
"con_pass": conPass,
"force_password_change": forcePasswordChange,
}
if updateErr := UpdateAccount([]int64{*admin.Id}, updates); updateErr != nil {
return updateErr
}
return nil
}
func ensureTrafficStatsSecret() error {
envSecret := strings.TrimSpace(os.Getenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET"))
if envSecret != "" {
if existing, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret); err == nil {
if existing.Value != nil && *existing.Value != envSecret {
return UpdateConfig([]string{constant.Hysteria2TrafficStatsSecret}, map[string]interface{}{"value": envSecret})
}
return nil
}
key := constant.Hysteria2TrafficStatsSecret
remark := "Hysteria2 trafficStats API secret"
if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &envSecret, Remark: &remark}); saveErr != nil {
return saveErr
}
return nil
}
if _, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret); err == nil {
return nil
}
-6
View File
@@ -25,9 +25,6 @@ CREATE INDEX IF NOT EXISTS account_deleted_index ON account (deleted);
CREATE INDEX IF NOT EXISTS account_username_index ON account (username);
CREATE INDEX IF NOT EXISTS account_con_pass_index ON account (con_pass);
CREATE INDEX IF NOT EXISTS account_pass_index ON account (pass);
INSERT INTO account (id, username, pass, con_pass, quota, download, upload, expire_time, device_no, role)
SELECT 1 ,'sysadmin', '02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd', 'sysadmin.sysadmin', -1, 0, 0, 253370736000000, 6, 'admin'
WHERE NOT EXISTS (SELECT 1 FROM account WHERE id = 1);
CREATE TABLE IF NOT EXISTS config
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -66,9 +63,6 @@ INSERT INTO config (key, value, remark)
SELECT 'HYSTERIA2_CONFIG_REMARK', '', 'Hysteria2 Config Remark'
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_REMARK');
INSERT INTO config (key, value, remark)
SELECT 'HYSTERIA2_CONFIG_PORT_HOPPING', '', 'Hysteria2 Config Port Hopping'
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_PORT_HOPPING');
INSERT INTO config (key, value, remark)
SELECT 'RESET_TRAFFIC_CRON', '', 'Reset Traffic Cron'
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'RESET_TRAFFIC_CRON');
INSERT INTO config (key, value, remark)
-8
View File
@@ -1,8 +0,0 @@
## Development environment
# Variables must use the VITE_ prefix to be exposed externally
NODE_ENV='development'
VITE_APP_TITLE = 'h-ui'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/hui'
-5
View File
@@ -1,5 +0,0 @@
## Production environment
VITE_APP_TITLE = 'h-ui'
VITE_APP_PORT = 80
VITE_APP_BASE_API = '/hui'
+1 -3
View File
@@ -37,7 +37,6 @@
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"fast-glob": "^3.2.11",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
@@ -57,8 +56,7 @@
"unplugin-vue-components": "^0.23.0",
"vite": "^4.3.1",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^0.35.0",
"vite-plugin-dynamic-base": "1.0.2"
"vue-tsc": "^0.35.0"
},
"engines": {
"node": ">=18.12.0"
+14 -151
View File
@@ -78,9 +78,6 @@ importers:
eslint-plugin-vue:
specifier: ^9.9.0
version: 9.9.0(eslint@8.34.0)
fast-glob:
specifier: ^3.2.11
version: 3.3.1
postcss:
specifier: ^8.4.21
version: 8.4.21
@@ -135,9 +132,6 @@ importers:
vite:
specifier: ^4.3.1
version: 4.3.1(@types/node@20.7.0)(sass@1.58.3)
vite-plugin-dynamic-base:
specifier: 1.0.2
version: 1.0.2
vite-plugin-svg-icons:
specifier: ^2.0.1
version: 2.0.1(vite@4.3.1(@types/node@20.7.0)(sass@1.58.3))
@@ -437,6 +431,7 @@ packages:
'@humanwhocodes/config-array@0.11.11':
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
engines: {node: '>=10.10.0'}
deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
@@ -444,6 +439,7 @@ packages:
'@humanwhocodes/object-schema@1.2.1':
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
deprecated: Use @eslint/object-schema instead
'@iconify-json/ep@1.1.8':
resolution: {integrity: sha512-pHCrsWU1R9/pTDU+Fps4+mjqOQFLtpGdXWegkhQ1P1DlgQAlCPyICtl6E1s8b7VwJMeZXaK84HA02UF6WD0o/Q==}
@@ -527,81 +523,6 @@ packages:
rollup:
optional: true
'@swc/core-darwin-arm64@1.11.24':
resolution: {integrity: sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
'@swc/core-darwin-x64@1.11.24':
resolution: {integrity: sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
'@swc/core-linux-arm-gnueabihf@1.11.24':
resolution: {integrity: sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
'@swc/core-linux-arm64-gnu@1.11.24':
resolution: {integrity: sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-arm64-musl@1.11.24':
resolution: {integrity: sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-x64-gnu@1.11.24':
resolution: {integrity: sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-linux-x64-musl@1.11.24':
resolution: {integrity: sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-win32-arm64-msvc@1.11.24':
resolution: {integrity: sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
'@swc/core-win32-ia32-msvc@1.11.24':
resolution: {integrity: sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
'@swc/core-win32-x64-msvc@1.11.24':
resolution: {integrity: sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
'@swc/core@1.11.24':
resolution: {integrity: sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '>=0.5.17'
peerDependenciesMeta:
'@swc/helpers':
optional: true
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/types@0.1.21':
resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==}
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
@@ -783,9 +704,11 @@ packages:
'@volar/vue-code-gen@0.35.0':
resolution: {integrity: sha512-IqIkzs3eRlJjABVXXBf5ik4/a8lQ3wqKemWMR33+KzOmj5xtXS+lgMMLNXJpj0Mw2nOLVl0I2KuIaTHFy485DQ==}
deprecated: 'WARNING: This project has been renamed to @vue/language-core. Install using @vue/language-core instead.'
'@volar/vue-typescript@0.35.0':
resolution: {integrity: sha512-Ojn98P888wWbk1qeR5f6spgtJZMwUos+OkJfZpzyafN5S21LxF58o7DhhNG8rMm1zMNUQHb+evlp5AXI5zHOmA==}
deprecated: 'WARNING: This project has been renamed to @vue/typescript. Install using @vue/typescript instead.'
'@vue/compiler-core@3.2.45':
resolution: {integrity: sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==}
@@ -1368,6 +1291,7 @@ packages:
eslint@8.34.0:
resolution: {integrity: sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
@@ -1547,6 +1471,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-modules@2.0.0:
resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
@@ -1690,6 +1615,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -1700,10 +1626,12 @@ packages:
is-accessor-descriptor@0.1.6:
resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==}
engines: {node: '>=0.10.0'}
deprecated: Please upgrade to v0.1.7
is-accessor-descriptor@1.0.0:
resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==}
engines: {node: '>=0.10.0'}
deprecated: Please upgrade to v1.0.1
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
@@ -1721,10 +1649,12 @@ packages:
is-data-descriptor@0.1.4:
resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==}
engines: {node: '>=0.10.0'}
deprecated: Please upgrade to v0.1.5
is-data-descriptor@1.0.0:
resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==}
engines: {node: '>=0.10.0'}
deprecated: Please upgrade to v1.0.1
is-descriptor@0.1.6:
resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==}
@@ -2084,9 +2014,6 @@ packages:
node-fetch-native@1.4.0:
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
node-html-parser@5.4.2:
resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==}
node-releases@2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
@@ -2419,6 +2346,7 @@ packages:
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rollup@3.29.3:
@@ -2728,6 +2656,7 @@ packages:
tar@6.2.0:
resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -2909,9 +2838,6 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vite-plugin-dynamic-base@1.0.2:
resolution: {integrity: sha512-0M1+W+EAFdZ2Aom5j2ToDkhZtHk1pS1vYesT+5sSEq7t+WARF1nl+yWsjOcyNdZZDt0fT80bNXPLJGoJuKSOjg==}
vite-plugin-svg-icons@2.0.1:
resolution: {integrity: sha512-6ktD+DhV6Rz3VtedYvBKKVA2eXF+sAQVaKkKLDSqGUfnhqXl3bj5PPkVTl3VexfTuZy66PmINi8Q6eFnVfRUmA==}
peerDependencies:
@@ -2962,6 +2888,7 @@ packages:
vue-i18n@9.0.0:
resolution: {integrity: sha512-iks0eJDv/4cK/7tl/ooMUroNVVIGOK4kKS1PIHmPQk7QjT/sDfFM84vjPKgpARbw0GjJsOiADL43jufNfs9e9A==}
engines: {node: '>= 10'}
deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html
peerDependencies:
vue: ^3.0.0
@@ -3418,58 +3345,6 @@ snapshots:
optionalDependencies:
rollup: 3.29.3
'@swc/core-darwin-arm64@1.11.24':
optional: true
'@swc/core-darwin-x64@1.11.24':
optional: true
'@swc/core-linux-arm-gnueabihf@1.11.24':
optional: true
'@swc/core-linux-arm64-gnu@1.11.24':
optional: true
'@swc/core-linux-arm64-musl@1.11.24':
optional: true
'@swc/core-linux-x64-gnu@1.11.24':
optional: true
'@swc/core-linux-x64-musl@1.11.24':
optional: true
'@swc/core-win32-arm64-msvc@1.11.24':
optional: true
'@swc/core-win32-ia32-msvc@1.11.24':
optional: true
'@swc/core-win32-x64-msvc@1.11.24':
optional: true
'@swc/core@1.11.24':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.21
optionalDependencies:
'@swc/core-darwin-arm64': 1.11.24
'@swc/core-darwin-x64': 1.11.24
'@swc/core-linux-arm-gnueabihf': 1.11.24
'@swc/core-linux-arm64-gnu': 1.11.24
'@swc/core-linux-arm64-musl': 1.11.24
'@swc/core-linux-x64-gnu': 1.11.24
'@swc/core-linux-x64-musl': 1.11.24
'@swc/core-win32-arm64-msvc': 1.11.24
'@swc/core-win32-ia32-msvc': 1.11.24
'@swc/core-win32-x64-msvc': 1.11.24
'@swc/counter@0.1.3': {}
'@swc/types@0.1.21':
dependencies:
'@swc/counter': 0.1.3
'@sxzz/popperjs-es@2.11.7': {}
'@trysound/sax@0.2.0': {}
@@ -5147,11 +5022,6 @@ snapshots:
node-fetch-native@1.4.0: {}
node-html-parser@5.4.2:
dependencies:
css-select: 4.3.0
he: 1.2.0
node-releases@2.0.13: {}
normalize-package-data@2.5.0:
@@ -6068,13 +5938,6 @@ snapshots:
vary@1.1.2: {}
vite-plugin-dynamic-base@1.0.2:
dependencies:
'@swc/core': 1.11.24
node-html-parser: 5.4.2
transitivePeerDependencies:
- '@swc/helpers'
vite-plugin-svg-icons@2.0.1(vite@4.3.1(@types/node@20.7.0)(sass@1.58.3)):
dependencies:
'@types/svgo': 2.6.4
-2
View File
@@ -157,8 +157,6 @@ export default {
config: {
enable: "Enable/Disable",
remark: "Remark",
portHopping:
"Port Hopping, Multiple individual ports: 1234,5678,9012; A range of ports: 20000-50000; A combination of both: 1234,5000-6000,7044,8000-9000",
clashExtension: "Clash subscription extension",
listen:
"When the IP address is omitted, the server will listen on all interfaces, both IPv4 and IPv6. To listen on IPv4 only, you can use 0.0.0.0:443. To listen on IPv6 only, you can use [::]:443.",
-1
View File
@@ -152,7 +152,6 @@ export default {
config: {
enable: "Включить/отключить",
remark: "Комментарий",
portHopping: "Port hopping: отдельные порты, диапазоны или их комбинации",
clashExtension: "Расширение подписки Clash",
listen: "Адрес и порт прослушивания. Для IPv4 используйте 0.0.0.0:443.",
tlsType: "Тип TLS",
-1
View File
@@ -9,7 +9,6 @@ declare module "*.vue" {
// TypeScript-подсказки для переменных окружения
interface ImportMetaEnv {
VITE_APP_TITLE: string;
VITE_APP_PORT: string;
VITE_APP_BASE_API: string;
}
@@ -121,14 +121,6 @@
<el-input v-model="configForm.remark" clearable />
</el-form-item>
</el-tooltip>
<el-tooltip
:content="$t('hysteria.config.portHopping')"
placement="bottom"
>
<el-form-item label="portHopping" prop="portHopping">
<el-input v-model="configForm.portHopping" clearable />
</el-form-item>
</el-tooltip>
<el-tooltip
:content="$t('hysteria.config.clashExtension')"
placement="bottom"
@@ -1089,7 +1081,6 @@ const { t } = useI18n();
const hysteria2EnableKey = "HYSTERIA2_ENABLE";
const hysteria2Remark = "HYSTERIA2_CONFIG_REMARK";
const hysteria2ConfigPortHopping = "HYSTERIA2_CONFIG_PORT_HOPPING";
const clashExtension = "CLASH_EXTENSION";
const dataFormRef = ref(ElForm);
@@ -1133,7 +1124,6 @@ const state = reactive({
},
configForm: {
remark: "",
portHopping: "",
clashExtension: "",
},
dataForm: { ...defaultHysteria2ServerConfig } as Hysteria2ServerConfig,
@@ -1292,7 +1282,6 @@ const handleExport = async () => {
const handleReset = () => {
state.configForm = {
remark: "",
portHopping: "",
clashExtension: "",
};
state.dataForm = deepCopy(defaultHysteria2ServerConfig);
@@ -1339,10 +1328,6 @@ const submitForm = () => {
key: hysteria2Remark,
value: state.configForm.remark,
},
{
key: hysteria2ConfigPortHopping,
value: state.configForm.portHopping,
},
{
key: clashExtension,
value: state.configForm.clashExtension,
@@ -1479,7 +1464,6 @@ const setConfig = () => {
keys: [
hysteria2EnableKey,
hysteria2Remark,
hysteria2ConfigPortHopping,
clashExtension,
],
}).then((response) => {
@@ -1489,8 +1473,6 @@ const setConfig = () => {
state.enableForm.enable = configVo.value;
} else if (configVo.key === hysteria2Remark) {
state.configForm.remark = configVo.value;
} else if (configVo.key === hysteria2ConfigPortHopping) {
state.configForm.portHopping = configVo.value;
} else if (configVo.key === clashExtension) {
state.configForm.clashExtension = configVo.value;
}
@@ -1624,5 +1606,3 @@ onMounted(() => {
margin: 0 auto;
}
</style>
+1 -4
View File
@@ -21,12 +21,10 @@ const pathSrc = path.resolve(__dirname, "src");
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
const DRIVE_LETTER_REGEX = /^[a-z]:/i;
import { dynamicBase } from "vite-plugin-dynamic-base";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, process.cwd());
return {
base: mode === "production" ? "/__dynamic_base__/" : "/",
base: "/",
resolve: {
alias: {
"@": pathSrc,
@@ -57,7 +55,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
},
},
plugins: [
dynamicBase({}),
vue(),
UnoCSS({
/* options */
-1
View File
@@ -11,7 +11,6 @@ const (
Hysteria2Config = "HYSTERIA2_CONFIG"
Hysteria2TrafficTime = "HYSTERIA2_TRAFFIC_TIME"
Hysteria2ConfigRemark = "HYSTERIA2_CONFIG_REMARK"
Hysteria2ConfigPortHopping = "HYSTERIA2_CONFIG_PORT_HOPPING"
ResetTrafficCron = "RESET_TRAFFIC_CRON"
ClashExtension = "CLASH_EXTENSION"
)
+24 -9
View File
@@ -1,20 +1,35 @@
package constant
const (
LogDir = "logs/"
SqliteDBDir = "data/"
BinDir = "bin/"
ExportPathDir = "export/"
import (
"os"
"path/filepath"
)
SqliteDBPath = "data/h_ui.db"
func getenv(name string, fallback string) string {
value := os.Getenv(name)
if value == "" {
return fallback
}
return value
}
var (
DataRootDir = getenv("HUI_DATA", "/var/lib/hy2xs-admin")
LogDir = getenv("HUI_LOG", "/var/log/hy2xs")
SqliteDBDir = DataRootDir
BinDir = filepath.Join(DataRootDir, "bin")
ExportPathDir = filepath.Join(DataRootDir, "export")
SqliteDBPath = filepath.Join(DataRootDir, "h_ui.db")
Hysteria2ConfigPath = "/etc/hysteria/config.yaml"
Hysteria2BinPath = "/usr/local/bin/hysteria"
SystemLogPath = "logs/hy2xs-admin.log"
Hysteria2LogPath = "logs/hysteria2.log"
SystemLogPath = filepath.Join(LogDir, "hy2xs-admin.log")
Hysteria2LogPath = filepath.Join(LogDir, "hysteria2.log")
)
const (
TokenType = "Bearer"
Version = "v0.0.22"
)
+3
View File
@@ -43,6 +43,9 @@ func Login(username string, plainPassword string) (string, bool, error) {
}
requirePasswordChange := legacy
if account.ForcePasswordChange != nil {
requirePasswordChange = *account.ForcePasswordChange != 0
}
return token, requirePasswordChange, nil
}
-234
View File
@@ -1,234 +0,0 @@
package service
import (
"errors"
"fmt"
"hy2xs-admin/dao"
"hy2xs-admin/model/constant"
"hy2xs-admin/util"
"strings"
)
var (
netManager string
ingressInterface string
Add = "add"
Delete = "delete"
Table = "hui_porthopping"
Comment = "hui_hysteria_porthopping"
)
func InitForward() {
if nft, err := util.Exec("command -v nft"); err == nil && strings.TrimSpace(nft) != "" {
netManager = "nft"
} else if iptables, err := util.Exec("command -v iptables"); err == nil && strings.TrimSpace(iptables) != "" {
netManager = "iptables"
}
if ii, err := util.Exec("ls /sys/class/net | grep -E '^en|^eth'"); err == nil && strings.TrimSpace(ii) != "" {
iiList := strings.Split(ii, "\n")
ingressInterface = strings.TrimSpace(iiList[0])
}
}
func InitTableAndChain() error {
if netManager == "nft" {
_, err := util.Exec(fmt.Sprintf("nft add table inet %s", Table))
if err != nil {
return err
}
_, err = util.Exec(fmt.Sprintf("nft add chain inet %s prerouting { type nat hook prerouting priority dstnat\\; policy accept\\; }", Table))
if err != nil {
return err
}
}
return nil
}
func InitPortHopping() error {
if err := RemoveByComment(); err != nil {
return err
}
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return err
}
// set port forward
hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping)
if err != nil {
return err
}
if *hysteria2ConfigPortHopping.Value != "" {
listen := strings.Split(*hysteria2Config.Listen, ":")
if len(listen) == 2 {
portHoppings := strings.Split(*hysteria2ConfigPortHopping.Value, ",")
for _, item := range portHoppings {
if err := portForward(item, listen[1], Add); err != nil {
return err
}
}
}
}
return nil
}
func portForward(rules string, target string, option string) error {
switch netManager {
case "nft":
switch option {
case Add, Delete:
return nftForward(rules, target, option)
default:
return errors.New("unsupported command option")
}
case "iptables":
switch option {
case Add:
return iptablesForward(rules, target, "-A")
case Delete:
return iptablesForward(rules, target, "-D")
default:
return errors.New("unsupported command option")
}
default:
return errors.New("port hopping not supported on this system")
}
}
func RemoveByComment() error {
switch netManager {
case "nft":
return ntfRemoveByComment(Comment)
case "iptables":
return iptablesRemoveByComment(Comment)
default:
return errors.New("port hopping not supported on this system")
}
}
func nftForward(rules string, target string, option string) error {
if ingressInterface == "" {
return fmt.Errorf("no network interface detected")
}
// nft list ruleset
// Создать таблицу: nft add table inet hui_hysteria_porthopping
// Создать chain: nft add chain inet hui_hysteria_porthopping prerouting { type nat hook prerouting priority dstnat\; policy accept\; }
// Добавить rule: nft add rule inet hui_hysteria_porthopping prerouting iifname enp1s0 udp dport {30000-40000} counter redirect to :444 comment hui_hysteria_porthopping
_, err := util.Exec(fmt.Sprintf("nft %s rule inet %s prerouting iifname %s udp dport {%s} counter redirect to :%s comment %s", option, Table, ingressInterface, rules, target, Comment))
if err != nil {
return err
}
return nil
}
func ntfRemoveByComment(comment string) error {
rules, err := nftRules()
if err != nil {
return err
}
for _, rule := range rules {
if strings.Contains(rule, comment) {
parts := strings.Fields(rule)
handle := parts[len(parts)-1]
_, err := util.Exec(fmt.Sprintf("nft delete rule inet %s prerouting handle %s", Table, strings.TrimSpace(handle)))
if err != nil {
return err
}
}
}
return nil
}
func nftRules() ([]string, error) {
listOutput, err := util.Exec(fmt.Sprintf("nft list ruleset | grep -q %s && echo 'found' || echo 'not found'", Comment))
if err != nil {
return nil, err
}
if strings.TrimSpace(listOutput) == "not found" {
return []string{}, nil
}
output, err := util.Exec(fmt.Sprintf("nft --handle list chain inet %s prerouting", Table))
if err != nil {
return nil, err
}
rules := strings.Split(output, "\n")
return rules, nil
}
func iptablesForward(rules string, target string, option string) error {
if ingressInterface == "" {
return fmt.Errorf("no network interface detected")
}
rulePairs := strings.Split(rules, ",")
for _, pair := range rulePairs {
ports := ""
portRange := strings.Split(pair, "-")
if len(portRange) == 1 {
ports = strings.TrimSpace(portRange[0])
} else if len(portRange) == 2 {
startPort := strings.TrimSpace(portRange[0])
endPort := strings.TrimSpace(portRange[1])
ports = startPort + ":" + endPort
} else {
return fmt.Errorf("invalid port range format: %s", pair)
}
if len(ports) != 0 {
if err := iptablesAddRule(option, ports, target); err != nil {
return err
}
}
}
return nil
}
func iptablesAddRule(option, ports, target string) error {
protocols := [2]string{"iptables", "ip6tables"}
for _, protocol := range protocols {
// iptables -t nat -A PREROUTING -i enp1s0 -p udp --dport 30000:40000 -j REDIRECT --to-port 444 -m comment --comment hui_hysteria_porthopping
_, err := util.Exec(fmt.Sprintf("%s -t nat %s PREROUTING -i %s -p udp --dport %s -j REDIRECT --to-port %s -m comment --comment %s", protocol, option, ingressInterface, ports, target, Comment))
if err != nil {
return err
}
}
return nil
}
func iptablesRemoveByComment(comment string) error {
protocols := [2]string{"iptables", "ip6tables"}
for _, protocol := range protocols {
rules, err := iptablesRules(protocol)
if err != nil {
return err
}
for _, rule := range rules {
if strings.Contains(rule, comment) {
parts := strings.Fields(rule)
handle := parts[0]
_, err := util.Exec(fmt.Sprintf("%s -t nat -D PREROUTING %s", protocol, strings.TrimSpace(handle)))
if err != nil {
return err
}
}
}
}
return nil
}
func iptablesRules(protocol string) ([]string, error) {
// iptables -t nat -L PREROUTING -v --line-numbers
output, err := util.Exec(fmt.Sprintf("%s -t nat -L PREROUTING -v --line-numbers", protocol))
if err != nil {
return nil, err
}
rules := strings.Split(output, "\n")
return rules, nil
}
+2 -1
View File
@@ -109,4 +109,5 @@
## Пример
См. [examples/post-install.env.example](examples/post-install.env.example).
Используйте canonical runtime-файл `package/config/hy2xs.env` как базовый шаблон значений
и переносите его параметры в `/etc/hy2xs/hy2xs.env`.
-1
View File
@@ -51,7 +51,6 @@
10. [10-access-layer-out-of-scope.md](10-access-layer-out-of-scope.md)
11. [11-testing-and-acceptance.md](11-testing-and-acceptance.md)
12. [12-operations-and-troubleshooting.md](12-operations-and-troubleshooting.md)
13. [examples/post-install.env.example](examples/post-install.env.example)
## Жёсткие рамки baseline
-47
View File
@@ -1,47 +0,0 @@
# post-install.env example
# Baseline reference file for a clean Debian 12 deployment.
DEPLOY_TARGET_OS=debian12
DEPLOY_TIMESTAMP=2026-04-13T10:00:00Z
PACKAGE_NAME=hy2xs-install-package
PACKAGE_BUILD_ID=build-20260413-001
PACKAGE_VERSION=0.1.0
ORCH_SOURCE_STACK=bun-typescript
ORCH_BUILD_MODE=bun-compile
ORCH_BUILD_ID=build-20260413-001
ORCH_ENTRYPOINT=/usr/local/lib/hy2xs/hy2xs-orchestrator
DEPLOY_DOMAIN=example.com
PUBLIC_HOST=example.com
PUBLIC_PORT=443
SSH_PORT=22
HY2XS_FIREWALL_ENABLED=true
HY2XS_FIREWALL_STAGED_APPLY=true
HY2_SOURCE=official-upstream
HY2_VERSION=v2.8.1
HY2_TLS_MODE=acme
HY2_ACME_EMAIL=admin@example.com
HY2_TLS_CERT_PATH=
HY2_TLS_KEY_PATH=
HY2_LISTEN_HOST=0.0.0.0
HY2_PORT=443
HY2_AUTH_MODE=http
HY2_AUTH_URL=http://127.0.0.1:8081/hui/hysteria2/auth
HY2_TRAFFIC_STATS_LISTEN=127.0.0.1:25413
HY2_OBFS_TYPE=salamander
HY2_OBFS_PASSWORD=CHANGE_ME
HY2_BANDWIDTH_UP_Mbps=50
HY2_BANDWIDTH_DOWN_Mbps=50
HY2_IGNORE_CLIENT_BANDWIDTH=false
HY2_CONFIG_PATH=/etc/hysteria/config.yaml
HUI_ENABLED=true
HUI_FORK_REF=packaged
HUI_BUILD_ID=build-20260413-001
HUI_BIND_HOST=127.0.0.1
HUI_PORT=8081
HUI_INSTALL_DIR=/opt/hy2xs-admin
HUI_DATA_DIR=/var/lib/hy2xs-admin
HUI_LOG_DIR=/var/log/hy2xs-admin
+4 -2
View File
@@ -3,7 +3,7 @@ import type { InstallContext, InstallOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
import { step } from "../lib/log";
import { defaultRuntimeConfig, parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { preflight } from "../steps/preflight";
import { installDeps } from "../steps/deps";
import { prepareFilesystem } from "../steps/filesystem";
@@ -29,7 +29,9 @@ function secret(): string {
export async function install(options: InstallOptions): Promise<void> {
const hasConfig = await exists(options.configPath);
const config = hasConfig ? parseRuntimeEnv(await readText(options.configPath)) : defaultRuntimeConfig();
const sourceConfigPath = hasConfig ? options.configPath : `${options.packageDir}/config/hy2xs.env`;
const sourceConfigRaw = await readText(sourceConfigPath);
const config = parseRuntimeEnv(sourceConfigRaw);
const context: InstallContext = {
options,
+10 -40
View File
@@ -83,6 +83,13 @@ function normalizeTlsMode(value: string): TlsMode {
throw new Error(`invalid HY2XS_TLS_MODE: ${value}`);
}
function normalizeAcmeType(value: string): "http" | "tls" | "dns" {
if (value === "http" || value === "tls" || value === "dns") {
return value;
}
throw new Error(`invalid HY2XS_ACME_TYPE: ${value}`);
}
export function parseRuntimeEnv(content: string): RuntimeConfig {
const env = parseEnv(content);
@@ -91,6 +98,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
const hysteriaPort = parsePort("HY2XS_HYSTERIA_PORT", env.HY2XS_HYSTERIA_PORT, 443);
const trafficStatsPort = parsePort("HY2XS_HYSTERIA_TRAFFIC_STATS_PORT", env.HY2XS_HYSTERIA_TRAFFIC_STATS_PORT, 36712);
const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme");
const acmeType = normalizeAcmeType(env.HY2XS_ACME_TYPE || "http");
const config: RuntimeConfig = {
domain: env.HY2XS_DOMAIN || "",
@@ -106,6 +114,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
adminInitialPassword: env.HY2XS_ADMIN_INITIAL_PASSWORD || randomSecret(),
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
tlsMode,
acmeType,
acmeEmail: env.HY2XS_ACME_EMAIL || "",
tlsCertPath: env.HY2XS_TLS_CERT_PATH || "/etc/hysteria/server.crt",
tlsKeyPath: env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key",
@@ -140,46 +149,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
return config;
}
export function defaultRuntimeConfig(): RuntimeConfig {
return parseRuntimeEnv(
[
"HY2XS_IPV6_ENABLED=false",
"HY2XS_DOMAIN=",
"HY2XS_PUBLIC_HOST=127.0.0.1",
"HY2XS_PUBLIC_PORT=443",
"HY2XS_SSH_PORT=22",
"HY2XS_FIREWALL_ENABLED=true",
"HY2XS_FIREWALL_STAGED_APPLY=true",
"HY2XS_UI_BIND_HOST=127.0.0.1",
"HY2XS_UI_PORT=8080",
"HY2XS_ADMIN_USER=admin",
`HY2XS_ADMIN_INITIAL_PASSWORD=${randomSecret()}`,
"HY2XS_FORCE_PASSWORD_CHANGE=true",
"HY2XS_TLS_MODE=self_signed_dev",
"HY2XS_ACME_EMAIL=",
"HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt",
"HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key",
"HY2XS_HYSTERIA_BIND_HOST=0.0.0.0",
"HY2XS_HYSTERIA_PORT=443",
"HY2XS_HYSTERIA_AUTH_MODE=http",
"HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth",
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1",
"HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712",
`HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${randomSecret()}`,
"HY2XS_HYSTERIA_OBFS_TYPE=salamander",
`HY2XS_HYSTERIA_OBFS_PASSWORD=${randomSecret()}`,
"HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps",
"HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps",
"HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false",
"HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml",
"HY2XS_HYSTERIA_VERSION=latest",
"HY2XS_INSTALL_DIR=/opt/hy2xs-admin",
"HY2XS_DATA_DIR=/var/lib/hy2xs-admin",
"HY2XS_LOG_DIR=/var/log/hy2xs"
].join("\n")
);
}
export function validateRuntimeConfig(config: RuntimeConfig): void {
if (config.ipv6Enabled) {
throw new Error("HY2XS is IPv4-only: HY2XS_IPV6_ENABLED must be false");
@@ -228,6 +197,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
`HY2XS_TLS_MODE=${config.tlsMode}`,
`HY2XS_ACME_TYPE=${config.acmeType}`,
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
`HY2XS_TLS_CERT_PATH=${config.tlsCertPath}`,
`HY2XS_TLS_KEY_PATH=${config.tlsKeyPath}`,
+29
View File
@@ -48,3 +48,32 @@ export async function runVisible(command: TemplateStringsArray, ...args: unknown
throw new Error(`command failed (${exitCode}): ${rendered}`);
}
}
export async function runHidden(command: TemplateStringsArray, ...args: unknown[]): Promise<void> {
const rendered = renderCommand(command, args);
const process = Bun.spawn(["sh", "-c", rendered], {
stdout: "inherit",
stderr: "inherit"
});
const exitCode = await process.exited;
if (exitCode !== 0) {
throw new Error(`command failed (${exitCode})`);
}
}
export async function runSecret(command: TemplateStringsArray, ...args: unknown[]): Promise<string> {
const rendered = renderCommand(command, args);
const process = Bun.spawn(["sh", "-c", rendered], {
stdout: "pipe",
stderr: "pipe"
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(process.stdout).text(),
new Response(process.stderr).text(),
process.exited
]);
if (exitCode !== 0) {
throw new Error(`command failed (${exitCode}): ${stderr.trim()}`);
}
return stdout.trim();
}
+7 -2
View File
@@ -9,11 +9,17 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
return;
}
const acmeChallengePort = context.config.acmeType === "tls" ? 443 : 80;
const acmeRule = context.config.tlsMode === "acme"
? `tcp dport ${acmeChallengePort} accept`
: "# acme challenge port disabled";
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
SSH_PORT: context.config.sshPort,
HYSTERIA_PORT: context.config.hysteriaPort,
UI_PORT: context.config.uiPort,
UI_BIND_HOST: context.config.uiBindHost
UI_BIND_HOST: context.config.uiBindHost,
ACME_RULE: acmeRule
});
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`;
@@ -30,7 +36,6 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
await runVisible`systemctl enable --now nftables`;
await runVisible`ss -H -ltn | grep -q ':${context.config.sshPort} ' || (echo 'ssh port check failed' >&2; exit 1)`;
await runVisible`ss -H -lun | grep -q ':${context.config.hysteriaPort} ' || (echo 'hysteria udp port check failed' >&2; exit 1)`;
if (context.config.firewallStagedApply) {
await runVisible`systemctl stop hy2xs-fw-rollback || true`;
+1 -1
View File
@@ -31,7 +31,7 @@ export async function installHysteria(context: InstallContext): Promise<void> {
if (policy === "latest") {
await runVisible`bash ${scriptPath}`;
} else {
await runVisible`HYSTERIA_VERSION=${policy} bash ${scriptPath}`;
await runVisible`bash ${scriptPath} --version ${policy}`;
}
await runVisible`test -x /usr/local/bin/hysteria`;
+7 -4
View File
@@ -1,6 +1,6 @@
import type { InstallContext } from "../types/context";
import { info } from "../lib/log";
import { runVisible } from "../lib/process";
import { runHidden, runSecret, runVisible } from "../lib/process";
export async function smoke(context: InstallContext): Promise<void> {
if (context.options.skipStart) {
@@ -26,10 +26,13 @@ export async function smoke(context: InstallContext): Promise<void> {
}
await runVisible`ss -H -lun | grep -q '0.0.0.0:${context.config.hysteriaPort} '`;
await runVisible`! ss -H -ltnu | grep -q '\[::\]'`;
await runVisible`curl -fsS --max-time 5 http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth >/dev/null`;
await runHidden`curl -fsS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth >/dev/null`;
await runVisible`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
await runVisible`curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online | grep -Eq '401|403'`;
await runHidden`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
const deniedCode = await runSecret`curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`;
if (!/(401|403)/.test(deniedCode)) {
throw new Error(`unexpected trafficStats status for invalid secret: ${deniedCode}`);
}
await runVisible`nft -c -f /etc/nftables.conf`;
+1
View File
@@ -32,6 +32,7 @@ export type RuntimeConfig = {
adminInitialPassword: string;
forcePasswordChange: boolean;
tlsMode: TlsMode;
acmeType: "http" | "tls" | "dns";
acmeEmail: string;
tlsCertPath: string;
tlsKeyPath: string;
+34
View File
@@ -0,0 +1,34 @@
# HY2XS canonical production runtime config (packaged baseline)
HY2XS_IPV6_ENABLED=false
HY2XS_DOMAIN=
HY2XS_PUBLIC_HOST=127.0.0.1
HY2XS_PUBLIC_PORT=443
HY2XS_SSH_PORT=22
HY2XS_FIREWALL_ENABLED=true
HY2XS_FIREWALL_STAGED_APPLY=true
HY2XS_UI_BIND_HOST=127.0.0.1
HY2XS_UI_PORT=8080
HY2XS_ADMIN_USER=admin
HY2XS_ADMIN_INITIAL_PASSWORD=
HY2XS_FORCE_PASSWORD_CHANGE=true
HY2XS_TLS_MODE=self_signed_dev
HY2XS_ACME_EMAIL=
HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt
HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key
HY2XS_HYSTERIA_BIND_HOST=0.0.0.0
HY2XS_HYSTERIA_PORT=443
HY2XS_HYSTERIA_AUTH_MODE=http
HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=
HY2XS_HYSTERIA_OBFS_TYPE=salamander
HY2XS_HYSTERIA_OBFS_PASSWORD=
HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml
HY2XS_HYSTERIA_VERSION=latest
HY2XS_INSTALL_DIR=/opt/hy2xs-admin
HY2XS_DATA_DIR=/var/lib/hy2xs-admin
HY2XS_LOG_DIR=/var/log/hy2xs
-47
View File
@@ -1,47 +0,0 @@
# HY2XS post-install reference file.
# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом.
DEPLOY_TARGET_OS=debian12
DEPLOY_TIMESTAMP=2026-04-13T10:00:00Z
PACKAGE_NAME=hy2xs-install-package
PACKAGE_BUILD_ID=build-20260413-001
PACKAGE_VERSION=0.1.0
ORCH_SOURCE_STACK=bun-typescript
ORCH_BUILD_MODE=bun-compile
ORCH_BUILD_ID=build-20260413-001
ORCH_ENTRYPOINT=/usr/local/lib/hy2xs/hy2xs-orchestrator
DEPLOY_DOMAIN=example.com
PUBLIC_HOST=example.com
PUBLIC_PORT=443
SSH_PORT=22
HY2XS_FIREWALL_ENABLED=true
HY2XS_FIREWALL_STAGED_APPLY=true
HY2_SOURCE=official-upstream
HY2_VERSION=v2.8.1
HY2_TLS_MODE=acme
HY2_ACME_EMAIL=admin@example.com
HY2_TLS_CERT_PATH=
HY2_TLS_KEY_PATH=
HY2_LISTEN_HOST=0.0.0.0
HY2_PORT=443
HY2_AUTH_MODE=http
HY2_AUTH_URL=http://127.0.0.1:8081/hui/hysteria2/auth
HY2_TRAFFIC_STATS_LISTEN=127.0.0.1:25413
HY2_OBFS_TYPE=salamander
HY2_OBFS_PASSWORD=CHANGE_ME
HY2_BANDWIDTH_UP_Mbps=50
HY2_BANDWIDTH_DOWN_Mbps=50
HY2_IGNORE_CLIENT_BANDWIDTH=false
HY2_CONFIG_PATH=/etc/hysteria/config.yaml
HUI_ENABLED=true
HUI_FORK_REF=packaged
HUI_BUILD_ID=build-20260413-001
HUI_BIND_HOST=127.0.0.1
HUI_PORT=8081
HUI_INSTALL_DIR=/opt/hy2xs-admin
HUI_DATA_DIR=/var/lib/hy2xs-admin
HUI_LOG_DIR=/var/log/hy2xs-admin
+7 -1
View File
@@ -3,6 +3,8 @@ set -eu
PACKAGE_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ORCHESTRATOR="$PACKAGE_DIR/orchestrator/hy2xs-orchestrator"
ORCHESTRATOR_INSTALL_PATH="/usr/local/lib/hy2xs/hy2xs-orchestrator"
ORCHESTRATOR_SYMLINK="/usr/local/bin/hy2xs-orchestrator"
log() {
printf '[hy2xs-install] %s\n' "$*"
@@ -21,6 +23,10 @@ if [ ! -x "$ORCHESTRATOR" ]; then
fail "Missing executable orchestrator artifact: $ORCHESTRATOR"
fi
install -d -m 0755 /usr/local/lib/hy2xs
install -m 0755 "$ORCHESTRATOR" "$ORCHESTRATOR_INSTALL_PATH"
ln -sf "$ORCHESTRATOR_INSTALL_PATH" "$ORCHESTRATOR_SYMLINK"
log "package directory: $PACKAGE_DIR"
log "starting install-only orchestrator"
exec "$ORCHESTRATOR" install --package-dir "$PACKAGE_DIR" "$@"
exec "$ORCHESTRATOR_INSTALL_PATH" install --package-dir "$PACKAGE_DIR" "$@"
+1
View File
@@ -5,6 +5,7 @@ table ip hy2xs {
iif lo accept
ct state established,related accept
tcp dport {{SSH_PORT}} accept
{{ACME_RULE}}
udp dport {{HYSTERIA_PORT}} accept
tcp dport {{UI_PORT}} ip saddr {{UI_BIND_HOST}} accept
icmp type echo-request accept