From 3fccd5c442655061f9d268bf24bedae94d8baff9 Mon Sep 17 00:00:00 2001 From: Crimson Date: Sun, 26 Apr 2026 07:27:06 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20production-hardening=20=D0=BF=D0=BE=20fix1?= =?UTF-8?q?:=20env/reconfigure,=20IPv4-only,=20TLS,=20secrets,=20firewall,?= =?UTF-8?q?=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 +- apps/cmd/cmd.go | 1 - apps/cmd/reset.go | 9 +- apps/cmd/server.go | 26 +- apps/cmd/version.go | 1 - apps/controller/account.go | 24 +- apps/controller/config.go | 75 +-- apps/controller/hysteria2.go | 1 - apps/controller/log.go | 1 - apps/controller/monitor.go | 1 - apps/controller/validator.go | 1 - apps/dao/account.go | 1 - apps/dao/config.go | 1 - apps/dao/sqlite.go | 63 +- apps/frontend/embed.go | 1 - apps/frontend/package.json | 3 - apps/frontend/pnpm-lock.yaml | 60 +- apps/go.mod | 4 +- apps/go.sum | 22 - apps/main.go | 1 - apps/middleware/admin.go | 1 - apps/middleware/cron.go | 1 - apps/middleware/filter.go | 1 - apps/middleware/jwt.go | 1 - apps/middleware/log.go | 1 - apps/middleware/rate_limiter.go | 1 - apps/model/bo/account.go | 1 - apps/model/bo/hysteria2.go | 1 - apps/model/bo/hysteria2_api.go | 1 - apps/model/bo/subscribe.go | 1 - apps/model/constant/client.go | 1 - apps/model/constant/code.go | 1 - apps/model/constant/config.go | 26 +- apps/model/constant/error.go | 1 - apps/model/constant/system.go | 1 - apps/model/dto/account.go | 1 - apps/model/dto/config.go | 1 - apps/model/dto/dto.go | 1 - apps/model/dto/hysteria2.go | 1 - apps/model/dto/log.go | 1 - apps/model/dto/server.go | 1 - apps/model/entity/account.go | 8 +- apps/model/entity/config.go | 1 - apps/model/entity/entity.go | 1 - apps/model/vo/account.go | 1 - apps/model/vo/config.go | 1 - apps/model/vo/hysteria2.go | 1 - apps/model/vo/jwt.go | 6 +- apps/model/vo/log.go | 1 - apps/model/vo/monitor.go | 1 - apps/model/vo/result.go | 1 - apps/model/vo/vo.go | 1 - apps/proxy/hysteria2.go | 1 - apps/proxy/hysteria2_api.go | 1 - apps/proxy/process.go | 1 - apps/router/account.go | 1 - apps/router/auth.go | 1 - apps/router/config.go | 1 - apps/router/hysteria2.go | 1 - apps/router/log.go | 1 - apps/router/monitor.go | 1 - apps/router/router.go | 1 - apps/service/account.go | 29 +- apps/service/config.go | 15 +- apps/service/cron.go | 17 +- apps/service/forward.go | 1 - apps/service/hysteria2.go | 20 +- apps/service/hysteria2_api.go | 57 +- apps/service/jwt.go | 1 - apps/service/monitor.go | 1 - apps/service/server.go | 1 - apps/util/arr.go | 1 - apps/util/encrypt.go | 26 + apps/util/encrypt_test.go | 1 - apps/util/export.go | 1 - apps/util/file.go | 1 - apps/util/github.go | 58 -- apps/util/hysteria2.go | 57 -- apps/util/linux.go | 1 - apps/util/map.go | 1 - apps/util/rand.go | 1 - apps/util/string.go | 1 - docs/03-server-hysteria2.md | 10 +- docs/04-admin-panel-h-ui-fork.md | 3 + docs/05-client-and-access-scope.md | 11 +- docs/07-systemd-and-firewall.md | 24 +- docs/08-orchestrator-spec.md | 30 +- docs/09-post-install-env.md | 31 +- docs/11-testing-and-acceptance.md | 17 +- docs/examples/post-install.env.example | 18 +- fix1.txt | 705 +++++++++++++++++++++ orchestrator/src/cli.ts | 101 ++- orchestrator/src/commands/install.ts | 16 +- orchestrator/src/commands/reconfigure.ts | 76 +++ orchestrator/src/config/env.ts | 253 ++++++++ orchestrator/src/steps/config.ts | 41 +- orchestrator/src/steps/env.ts | 28 +- orchestrator/src/steps/filesystem.ts | 10 +- orchestrator/src/steps/firewall.ts | 29 +- orchestrator/src/steps/preflight.ts | 51 +- orchestrator/src/steps/smoke.ts | 14 +- orchestrator/src/steps/systemd.ts | 7 +- orchestrator/src/steps/ui.ts | 6 +- orchestrator/src/types/context.ts | 62 +- package/examples/post-install.env.example | 59 +- package/systemd/hy2xs-admin.service | 16 +- package/templates/env/post-install.env.tpl | 17 +- package/templates/hysteria/config.yaml.tpl | 11 +- package/templates/nftables/hy2xs.nft.tpl | 6 +- 109 files changed, 1773 insertions(+), 569 deletions(-) delete mode 100644 apps/util/github.go create mode 100644 fix1.txt create mode 100644 orchestrator/src/commands/reconfigure.ts create mode 100644 orchestrator/src/config/env.ts diff --git a/README.md b/README.md index 6a8fe49..5c4b5aa 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # HY2XS -HY2XS — двухслойный install package для развёртывания на чистом Debian 12 amd64. +HY2XS — production installer/runtime-manager для развёртывания Hysteria2 + HY2XS admin на чистом Debian 12 amd64. Состав baseline: - production builder на Debian 12 amd64 создаёт один переносимый архив; - install-only оркестратор на Bun + TypeScript поставляется на target как готовый compiled artifact; - HY2XS admin поставляется в составе пакета как bundled fork; -- vanilla Hysteria2 скачивается с official upstream во время установки. +- vanilla Hysteria2 скачивается с official upstream во время установки; +- runtime-конфиг управляется через `/etc/hy2xs/hy2xs.env` и команду `reconfigure`. -В baseline намеренно не входят target-side build, update, rollback, uninstall, Telegram bot delivery, billing и внешний контур выдачи access/profile. +В baseline намеренно не входят target-side build, rollback/uninstall orchestration, Telegram bot delivery, port hopping и generic server-manager функции. ## Структура репозитория @@ -51,18 +52,25 @@ https://git.ext.flamy.studio/flamy_dev/HY2XS_flamy.git Публиковать изменения нужно только после прохождения baseline acceptance, сборки оркестратора и smoke checks итогового пакета. -## Установка +## Установка и конфигурация На чистом Debian 12 target нужно распаковать архив и запустить от root: ```sh -./install.sh --non-interactive --domain example.com +./install.sh --package-dir . --config /etc/hy2xs/hy2xs.env --non-interactive ``` -Полезные baseline-флаги: +После установки применяются команды оркестратора: ```sh -./install.sh --non-interactive --domain example.com --port 443 --ssh-port 22 --ui-port 8080 +hy2xs-orchestrator reconfigure --package-dir /opt/hy2xs-package --config /etc/hy2xs/hy2xs.env --dry-run +hy2xs-orchestrator reconfigure --package-dir /opt/hy2xs-package --config /etc/hy2xs/hy2xs.env --apply ``` -После установки создаётся `/etc/hysteria/post-install.env` с фактическими параметрами deploy-состояния. +Ключевые инварианты: + +- только IPv4 (`0.0.0.0:` для Hysteria, `127.0.0.1:` для UI по умолчанию); +- UI запускается не от root (`hy2xs-admin`); +- секреты и чувствительные конфиги: `0600`; +- snapshot deploy-фактов: `/etc/hysteria/post-install.env`; +- editable runtime layer: `/etc/hy2xs/hy2xs.env`. diff --git a/apps/cmd/cmd.go b/apps/cmd/cmd.go index f44c3e7..cb90da0 100644 --- a/apps/cmd/cmd.go +++ b/apps/cmd/cmd.go @@ -48,4 +48,3 @@ func Execute() { os.Exit(1) } } - diff --git a/apps/cmd/reset.go b/apps/cmd/reset.go index ec8bbca..f5c7508 100644 --- a/apps/cmd/reset.go +++ b/apps/cmd/reset.go @@ -36,8 +36,12 @@ func runReset(cmd *cobra.Command, args []string) { } if err = dao.UpdateAccount([]int64{1}, map[string]interface{}{ "username": username, - "pass": util.SHA224String(password), - "con_pass": fmt.Sprintf("%s.%s", username, password)}); err != nil { + "pass": func() string { + hash, _ := util.HashPassword(password) + return hash + }(), + "force_password_change": 1, + "con_pass": fmt.Sprintf("%s.%s", username, password)}); err != nil { fmt.Println(err.Error()) os.Exit(1) } @@ -49,4 +53,3 @@ func runReset(cmd *cobra.Command, args []string) { fmt.Println(fmt.Sprintf("HY2XS admin Login Password: %s", password)) fmt.Println(fmt.Sprintf("HY2XS admin Connection Password: %s", fmt.Sprintf("%s.%s", username, password))) } - diff --git a/apps/cmd/server.go b/apps/cmd/server.go index 44ad1e3..81c21b8 100644 --- a/apps/cmd/server.go +++ b/apps/cmd/server.go @@ -11,10 +11,23 @@ import ( "hy2xs-admin/router" "hy2xs-admin/service" "hy2xs-admin/util" + "net" "net/http" "os" ) +func resolveUiBindHost() (string, error) { + host := os.Getenv("HY2XS_UI_BIND_HOST") + if host == "" { + host = "127.0.0.1" + } + ip := net.ParseIP(host) + if ip == nil || ip.To4() == nil { + return "", errors.New("HY2XS_UI_BIND_HOST must be IPv4") + } + return host, nil +} + func runServer(port string) error { defer releaseResource() @@ -32,12 +45,6 @@ func runServer(port string) error { if err := service.InitHysteria2(); err != nil { return err } - if err := service.InitTableAndChain(); err != nil { - logrus.Errorf(err.Error()) - } - if err := service.InitPortHopping(); err != nil { - logrus.Errorf(err.Error()) - } config, err := dao.GetConfig("key = ?", constant.HUIWebContext) if err != nil { return err @@ -50,8 +57,12 @@ func runServer(port string) error { if err != nil { return err } + bindHost, err := resolveUiBindHost() + if err != nil { + return err + } - service.InitServer(fmt.Sprintf(":%d", serverPort), r) + service.InitServer(fmt.Sprintf("%s:%d", bindHost, serverPort), r) if err := service.StartServer(crtPath, keyPath); err != nil && err != http.ErrServerClosed { logrus.Errorf("start server err: %v", err) return errors.New("start server err") @@ -83,4 +94,3 @@ func initFile() error { } return nil } - diff --git a/apps/cmd/version.go b/apps/cmd/version.go index fdec920..f265e02 100644 --- a/apps/cmd/version.go +++ b/apps/cmd/version.go @@ -20,4 +20,3 @@ func init() { func runVersion(cmd *cobra.Command, args []string) { fmt.Println("HY2XS admin version", constant.Version) } - diff --git a/apps/controller/account.go b/apps/controller/account.go index fd9fef0..55a6ca4 100644 --- a/apps/controller/account.go +++ b/apps/controller/account.go @@ -26,14 +26,15 @@ func Login(c *gin.Context) { return } - token, err := service.Login(*loginDto.Username, util.SHA224String(*loginDto.Pass)) + token, forcePasswordChange, err := service.Login(*loginDto.Username, *loginDto.Pass) if err != nil { vo.Fail(err.Error(), c) return } jwtVo := vo.JwtVo{ - TokenType: constant.TokenType, - AccessToken: token, + TokenType: constant.TokenType, + AccessToken: token, + ForcePasswordChange: forcePasswordChange, } vo.Success(jwtVo, c) } @@ -100,7 +101,11 @@ func SaveAccount(c *gin.Context) { return } - passEncrypt := util.SHA224String(*accountSaveDto.Pass) + passEncrypt, err := util.HashPassword(*accountSaveDto.Pass) + if err != nil { + vo.Fail(err.Error(), c) + return + } conPass := fmt.Sprintf("%s.%s", *accountSaveDto.Username, *accountSaveDto.ConPass) account := entity.Account{ Username: accountSaveDto.Username, @@ -167,8 +172,12 @@ func UpdateAccount(c *gin.Context) { var passEncrypt *string if accountUpdateDto.Pass != nil && *accountUpdateDto.Pass != "" { - passEncryptSha224 := util.SHA224String(*accountUpdateDto.Pass) - passEncrypt = &passEncryptSha224 + passEncryptHash, hashErr := util.HashPassword(*accountUpdateDto.Pass) + if hashErr != nil { + vo.Fail(hashErr.Error(), c) + return + } + passEncrypt = &passEncryptHash } account := entity.Account{ @@ -331,6 +340,5 @@ func VerifyDefaultPass(c *gin.Context) { vo.Fail(err.Error(), c) return } - vo.Success(account.Pass != nil && *account.Pass == "02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd", c) + vo.Success(account.Pass != nil && !util.IsBcryptHash(*account.Pass), c) } - diff --git a/apps/controller/config.go b/apps/controller/config.go index 8d828a0..72a22d0 100644 --- a/apps/controller/config.go +++ b/apps/controller/config.go @@ -17,7 +17,6 @@ import ( "io" "os" "path/filepath" - "regexp" "strconv" "strings" "time" @@ -35,7 +34,6 @@ func UpdateConfigs(c *gin.Context) { return } - needResetPortHopping := false needRestart := false for _, item := range configsUpdateDto.ConfigUpdateDtos { @@ -81,19 +79,8 @@ func UpdateConfigs(c *gin.Context) { } if key == constant.Hysteria2ConfigPortHopping { - re := regexp.MustCompile(`^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$`) - if value != "" && !re.MatchString(value) { - vo.Fail(fmt.Sprintf("port hopping: %s is invalid", value), c) - return - } - hysteria2ConfigPortHopping, err := service.GetConfig(constant.Hysteria2ConfigPortHopping) - if err != nil { - vo.Fail(err.Error(), c) - return - } - if *hysteria2ConfigPortHopping.Value != value { - needResetPortHopping = true - } + vo.Fail("port hopping out of scope in HY2XS production", c) + return } if key == constant.ResetTrafficCron { @@ -113,13 +100,6 @@ func UpdateConfigs(c *gin.Context) { } } - if needResetPortHopping { - if err := service.InitPortHopping(); err != nil { - vo.Fail(err.Error(), c) - return - } - } - if needRestart { go func() { _ = service.StopServer() @@ -197,33 +177,11 @@ func UpdateHysteria2Config(c *gin.Context) { return } - hysteria2Config, err := service.GetHysteria2Config() - if err != nil { - vo.Fail(err.Error(), c) - return - } - - needResetPortHopping := false - if hysteria2Config.Listen != nil && - *hysteria2Config.Listen != "" && - hysteria2ServerConfig.Listen != nil && - *hysteria2ServerConfig.Listen != "" && - *hysteria2ServerConfig.Listen != *hysteria2Config.Listen { - needResetPortHopping = true - } - if err = service.UpdateHysteria2Config(hysteria2ServerConfig); err != nil { vo.Fail(err.Error(), c) return } - if needResetPortHopping { - if err := service.InitPortHopping(); err != nil { - vo.Fail(err.Error(), c) - return - } - } - running := service.Hysteria2IsRunning() if running { if err = service.RestartHysteria2(); err != nil { @@ -242,24 +200,24 @@ func ExportHysteria2Config(c *gin.Context) { } // Значения по умолчанию - config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret}) + config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret}) if err != nil { vo.Fail(err.Error(), c) return } var hUIWebPort string - var jwtSecret string + var trafficStatsSecret string for _, item := range config { if *item.Key == constant.HUIWebPort { hUIWebPort = *item.Value - } else if *item.Key == constant.JwtSecret { - jwtSecret = *item.Value + } else if *item.Key == constant.Hysteria2TrafficStatsSecret { + trafficStatsSecret = *item.Value } } - if hUIWebPort == "" || jwtSecret == "" { - logrus.Errorf("hUIWebPort or jwtSecret is nil") + if hUIWebPort == "" || trafficStatsSecret == "" { + logrus.Errorf("hUIWebPort or trafficStatsSecret is nil") vo.Fail(constant.SysError, c) return } @@ -279,7 +237,7 @@ func ExportHysteria2Config(c *gin.Context) { http.Insecure = &authHttpInsecure auth.HTTP = &http hysteria2ServerConfig.Auth = &auth - hysteria2ServerConfig.TrafficStats.Secret = &jwtSecret + hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405")) filePath := constant.ExportPathDir + fileName @@ -325,24 +283,24 @@ func ImportHysteria2Config(c *gin.Context) { } // Значения по умолчанию - config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret}) + config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret}) if err != nil { vo.Fail(err.Error(), c) return } var hUIWebPort string - var jwtSecret string + var trafficStatsSecret string for _, item := range config { if *item.Key == constant.HUIWebPort { hUIWebPort = *item.Value - } else if *item.Key == constant.JwtSecret { - jwtSecret = *item.Value + } else if *item.Key == constant.Hysteria2TrafficStatsSecret { + trafficStatsSecret = *item.Value } } - if hUIWebPort == "" || jwtSecret == "" { - logrus.Errorf("hUIWebPort or jwtSecret is nil") + if hUIWebPort == "" || trafficStatsSecret == "" { + logrus.Errorf("hUIWebPort or trafficStatsSecret is nil") vo.Fail(constant.SysError, c) return } @@ -362,7 +320,7 @@ func ImportHysteria2Config(c *gin.Context) { http.Insecure = &authHttpInsecure auth.HTTP = &http hysteria2ServerConfig.Auth = &auth - hysteria2ServerConfig.TrafficStats.Secret = &jwtSecret + hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret if err = service.SetHysteria2Config(hysteria2ServerConfig); err != nil { vo.Fail(err.Error(), c) @@ -501,4 +459,3 @@ func UploadCertFile(c *gin.Context) { } vo.Success(certPath, c) } - diff --git a/apps/controller/hysteria2.go b/apps/controller/hysteria2.go index 4a7c7d8..eab7e15 100644 --- a/apps/controller/hysteria2.go +++ b/apps/controller/hysteria2.go @@ -150,4 +150,3 @@ func Hysteria2Subscribe(c *gin.Context) { c.String(200, configStr) } - diff --git a/apps/controller/log.go b/apps/controller/log.go index 9ffb2d1..66f4585 100644 --- a/apps/controller/log.go +++ b/apps/controller/log.go @@ -114,4 +114,3 @@ func ExportLog(c *gin.Context) { c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) c.File(filePath) } - diff --git a/apps/controller/monitor.go b/apps/controller/monitor.go index 398ed50..e3e6502 100644 --- a/apps/controller/monitor.go +++ b/apps/controller/monitor.go @@ -23,4 +23,3 @@ func MonitorHysteria2(c *gin.Context) { } vo.Success(hysteria2MonitorVo, c) } - diff --git a/apps/controller/validator.go b/apps/controller/validator.go index d0fd9a6..ce36e56 100644 --- a/apps/controller/validator.go +++ b/apps/controller/validator.go @@ -39,4 +39,3 @@ func validateField[T interface{}](c *gin.Context, field T) (T, error) { } return field, nil } - diff --git a/apps/dao/account.go b/apps/dao/account.go index 0986b1f..37752fe 100644 --- a/apps/dao/account.go +++ b/apps/dao/account.go @@ -117,4 +117,3 @@ func ListAccount(query interface{}, args ...interface{}) ([]entity.Account, erro } return accounts, nil } - diff --git a/apps/dao/config.go b/apps/dao/config.go index daa5782..98155c6 100644 --- a/apps/dao/config.go +++ b/apps/dao/config.go @@ -64,4 +64,3 @@ func UpsertConfig(configs []entity.Config) error { } return nil } - diff --git a/apps/dao/sqlite.go b/apps/dao/sqlite.go index 0d868ac..1f5cc89 100644 --- a/apps/dao/sqlite.go +++ b/apps/dao/sqlite.go @@ -9,6 +9,8 @@ import ( "gorm.io/gorm/logger" "gorm.io/gorm/schema" "hy2xs-admin/model/constant" + "hy2xs-admin/model/entity" + "hy2xs-admin/util" "log" "os" "strings" @@ -69,6 +71,66 @@ func InitSql(port string) error { } } } + + if err := ensureSecureBootstrapAdmin(); err != nil { + return err + } + if err := ensureTrafficStatsSecret(); err != nil { + return err + } + 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 + } + if admin.Pass == nil { + return nil + } + if !util.IsBcryptHash(*admin.Pass) { + password, pwdErr := util.RandomString(18) + if pwdErr != nil { + return pwdErr + } + hash, hashErr := util.HashPassword(password) + if hashErr != nil { + return hashErr + } + conPass, 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 + } + } + return nil +} + +func ensureTrafficStatsSecret() error { + if _, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret); err == nil { + return nil + } + secret, secErr := util.RandomString(32) + if secErr != nil { + return secErr + } + key := constant.Hysteria2TrafficStatsSecret + remark := "Hysteria2 trafficStats API secret" + if _, saveErr := SaveConfig(entity.Config{Key: &key, Value: &secret, Remark: &remark}); saveErr != nil { + return saveErr + } return nil } @@ -117,4 +179,3 @@ func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB { return db.Offset(int((num - 1) * size)).Limit(int(size)) } } - diff --git a/apps/frontend/embed.go b/apps/frontend/embed.go index 76cbaff..d93c3da 100644 --- a/apps/frontend/embed.go +++ b/apps/frontend/embed.go @@ -73,4 +73,3 @@ func replaceRelativePaths(htmlContent string, basePath string) string { `, basePath) return strings.Replace(htmlContent, "", injection+"", 1) } - diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 8a90a05..ed012ab 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,13 +16,10 @@ "@vueuse/core": "^9.1.1", "axios": "^1.3.4", "copy-to-clipboard": "^3.3.3", - "echarts": "^5.2.2", "element-plus": "^2.3.1", "nprogress": "^0.2.0", "path-browserify": "^1.0.1", - "path-to-regexp": "^6.2.0", "pinia": "^2.0.33", - "screenfull": "^6.0.0", "vue": "^3.2.45", "vue-i18n": "9", "vue-router": "^4.1.6", diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index b7cbdde..c6942e2 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 - echarts: - specifier: ^5.2.2 - version: 5.2.2 element-plus: specifier: ^2.3.1 version: 2.3.1(vue@3.2.45) @@ -32,15 +29,9 @@ importers: path-browserify: specifier: ^1.0.1 version: 1.0.1 - path-to-regexp: - specifier: ^6.2.0 - version: 6.2.0 pinia: specifier: ^2.0.33 version: 2.0.33(typescript@4.9.3)(vue@3.2.45) - screenfull: - specifier: ^6.0.0 - version: 6.0.0 vue: specifier: ^3.2.45 version: 3.2.45 @@ -89,7 +80,7 @@ importers: version: 9.9.0(eslint@8.34.0) fast-glob: specifier: ^3.2.11 - version: 3.2.11 + version: 3.3.1 postcss: specifier: ^8.4.21 version: 8.4.21 @@ -559,28 +550,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.11.24': resolution: {integrity: sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@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] - libc: [glibc] '@swc/core-linux-x64-musl@1.11.24': resolution: {integrity: sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.11.24': resolution: {integrity: sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==} @@ -1273,9 +1260,6 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - echarts@5.2.2: - resolution: {integrity: sha512-yxuBfeIH5c+0FsoRP60w4De6omXhA06c7eUYBsC1ykB6Ys2yK5fSteIYWvkJ4xJVLQgCvAdO8C4mN6MLeJpBaw==} - electron-to-chromium@1.4.529: resolution: {integrity: sha512-6uyPyXTo8lkv8SWAmjKFbG42U073TXlzD4R8rW3EzuznhFS2olCIAfjjQtV2dV2ar/vRF55KUd3zQYnCB0dd3A==} @@ -1446,10 +1430,6 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.2.11: - resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} - engines: {node: '>=8.6.0'} - fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -2221,9 +2201,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@6.2.0: - resolution: {integrity: sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2466,10 +2443,6 @@ packages: engines: {node: '>=12.0.0'} hasBin: true - screenfull@6.0.0: - resolution: {integrity: sha512-LGY0nhNQkC4FX4DT4pZdJ5cZH5EOz9Gfh9KcVMl779pS677k4IV1Wv7sY/CwC9VKFT21fYgCh7zkTVVefi5XKA==} - engines: {node: ^14.13.1 || >=16.0.0} - scule@1.0.0: resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} @@ -2796,9 +2769,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.3.0: - resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} - tsutils@3.21.0: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -3055,9 +3025,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zrender@5.2.1: - resolution: {integrity: sha512-M3bPGZuyLTNBC6LiNKXJwSCtglMp8XUEqEBG+2MdICDI3d1s500Y4P0CzldQGsqpRVB7fkvf3BKQQRxsEaTlsw==} - snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -4313,11 +4280,6 @@ snapshots: duplexer@0.1.2: {} - echarts@5.2.2: - dependencies: - tslib: 2.3.0 - zrender: 5.2.1 - electron-to-chromium@1.4.529: {} element-plus@2.3.1(vue@3.2.45): @@ -4566,14 +4528,6 @@ snapshots: fast-diff@1.3.0: {} - fast-glob@3.2.11: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4715,7 +4669,7 @@ snapshots: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.11 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -5312,8 +5266,6 @@ snapshots: path-parse@1.0.7: {} - path-to-regexp@6.2.0: {} - path-type@4.0.0: {} pathe@0.2.0: {} @@ -5542,8 +5494,6 @@ snapshots: immutable: 4.3.4 source-map-js: 1.0.2 - screenfull@6.0.0: {} - scule@1.0.0: {} semver@5.7.2: {} @@ -5922,8 +5872,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.3.0: {} - tsutils@3.21.0(typescript@4.9.3): dependencies: tslib: 1.14.1 @@ -6226,7 +6174,3 @@ snapshots: yargs-parser@20.2.9: {} yocto-queue@0.1.0: {} - - zrender@5.2.1: - dependencies: - tslib: 2.3.0 diff --git a/apps/go.mod b/apps/go.mod index 03342e5..fd950da 100644 --- a/apps/go.mod +++ b/apps/go.mod @@ -8,12 +8,12 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.14.0 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/go-github/v39 v39.2.0 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.18.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.9 @@ -32,7 +32,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -55,7 +54,6 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/apps/go.sum b/apps/go.sum index ade1758..7f28e59 100644 --- a/apps/go.sum +++ b/apps/go.sum @@ -38,18 +38,10 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= -github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= @@ -129,35 +121,21 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/apps/main.go b/apps/main.go index 0743871..e87ba4a 100644 --- a/apps/main.go +++ b/apps/main.go @@ -5,4 +5,3 @@ import "hy2xs-admin/cmd" func main() { cmd.Execute() } - diff --git a/apps/middleware/admin.go b/apps/middleware/admin.go index 64c0aa1..27ce1af 100644 --- a/apps/middleware/admin.go +++ b/apps/middleware/admin.go @@ -24,4 +24,3 @@ func AdminHandler() gin.HandlerFunc { c.Next() } } - diff --git a/apps/middleware/cron.go b/apps/middleware/cron.go index cc770c5..84297a7 100644 --- a/apps/middleware/cron.go +++ b/apps/middleware/cron.go @@ -31,4 +31,3 @@ func InitCron() error { c.Start() return nil } - diff --git a/apps/middleware/filter.go b/apps/middleware/filter.go index 1748723..2cc7a51 100644 --- a/apps/middleware/filter.go +++ b/apps/middleware/filter.go @@ -23,4 +23,3 @@ func FilterHandler() gin.HandlerFunc { c.Next() } } - diff --git a/apps/middleware/jwt.go b/apps/middleware/jwt.go index 66bf478..0047d70 100644 --- a/apps/middleware/jwt.go +++ b/apps/middleware/jwt.go @@ -36,4 +36,3 @@ func JWTHandler() gin.HandlerFunc { c.Next() } } - diff --git a/apps/middleware/log.go b/apps/middleware/log.go index 6eb7fe2..a4b385a 100644 --- a/apps/middleware/log.go +++ b/apps/middleware/log.go @@ -41,4 +41,3 @@ func LogHandler() gin.HandlerFunc { c.Next() } } - diff --git a/apps/middleware/rate_limiter.go b/apps/middleware/rate_limiter.go index 7a047b6..eeb7c77 100644 --- a/apps/middleware/rate_limiter.go +++ b/apps/middleware/rate_limiter.go @@ -24,4 +24,3 @@ func RateLimiterHandler() gin.HandlerFunc { c.Next() } } - diff --git a/apps/model/bo/account.go b/apps/model/bo/account.go index e9229c8..b04b911 100644 --- a/apps/model/bo/account.go +++ b/apps/model/bo/account.go @@ -28,4 +28,3 @@ type AccountExport struct { ConAt int64 `json:"conAt"` Remark string `json:"remark"` } - diff --git a/apps/model/bo/hysteria2.go b/apps/model/bo/hysteria2.go index 502f304..9005662 100644 --- a/apps/model/bo/hysteria2.go +++ b/apps/model/bo/hysteria2.go @@ -203,4 +203,3 @@ type serverConfigMasquerade struct { ListenHTTPS *string `yaml:"listenHTTPS,omitempty" json:"listenHTTPS" validate:"omitempty"` ForceHTTPS *bool `yaml:"forceHTTPS,omitempty" json:"forceHTTPS" validate:"omitempty"` } - diff --git a/apps/model/bo/hysteria2_api.go b/apps/model/bo/hysteria2_api.go index 477f308..6bfc578 100644 --- a/apps/model/bo/hysteria2_api.go +++ b/apps/model/bo/hysteria2_api.go @@ -4,4 +4,3 @@ type Hysteria2UserTraffic struct { Tx int64 `json:"tx"` // upload Rx int64 `json:"rx"` // download } - diff --git a/apps/model/bo/subscribe.go b/apps/model/bo/subscribe.go index 6d4a740..7b286d5 100644 --- a/apps/model/bo/subscribe.go +++ b/apps/model/bo/subscribe.go @@ -25,4 +25,3 @@ type ClashConfig struct { Proxies []interface{} `yaml:"proxies"` ProxyGroups []ProxyGroup `yaml:"proxy-groups"` } - diff --git a/apps/model/constant/client.go b/apps/model/constant/client.go index d80931f..a7b3697 100644 --- a/apps/model/constant/client.go +++ b/apps/model/constant/client.go @@ -6,4 +6,3 @@ const ( V2rayN = "v2rayn" NekoBox = "nekobox" ) - diff --git a/apps/model/constant/code.go b/apps/model/constant/code.go index e336639..e42e6a4 100644 --- a/apps/model/constant/code.go +++ b/apps/model/constant/code.go @@ -7,4 +7,3 @@ const ( CodeForbiddenError int = 50403 CodeInvalidError int = 50001 ) - diff --git a/apps/model/constant/config.go b/apps/model/constant/config.go index 0bd7d29..2719657 100644 --- a/apps/model/constant/config.go +++ b/apps/model/constant/config.go @@ -1,17 +1,17 @@ package constant const ( - HUIWebPort = "H_UI_WEB_PORT" - HUIWebContext = "H_UI_WEB_CONTEXT" - HUICrtPath = "H_UI_CRT_PATH" - HUIKeyPath = "H_UI_KEY_PATH" - JwtSecret = "JWT_SECRET" - Hysteria2Enable = "HYSTERIA2_ENABLE" - Hysteria2Config = "HYSTERIA2_CONFIG" - Hysteria2TrafficTime = "HYSTERIA2_TRAFFIC_TIME" - Hysteria2ConfigRemark = "HYSTERIA2_CONFIG_REMARK" - Hysteria2ConfigPortHopping = "HYSTERIA2_CONFIG_PORT_HOPPING" - ResetTrafficCron = "RESET_TRAFFIC_CRON" - ClashExtension = "CLASH_EXTENSION" + HUIWebPort = "H_UI_WEB_PORT" + HUIWebContext = "H_UI_WEB_CONTEXT" + HUICrtPath = "H_UI_CRT_PATH" + HUIKeyPath = "H_UI_KEY_PATH" + JwtSecret = "JWT_SECRET" + Hysteria2TrafficStatsSecret = "HYSTERIA2_TRAFFIC_STATS_SECRET" + Hysteria2Enable = "HYSTERIA2_ENABLE" + Hysteria2Config = "HYSTERIA2_CONFIG" + Hysteria2TrafficTime = "HYSTERIA2_TRAFFIC_TIME" + Hysteria2ConfigRemark = "HYSTERIA2_CONFIG_REMARK" + Hysteria2ConfigPortHopping = "HYSTERIA2_CONFIG_PORT_HOPPING" + ResetTrafficCron = "RESET_TRAFFIC_CRON" + ClashExtension = "CLASH_EXTENSION" ) - diff --git a/apps/model/constant/error.go b/apps/model/constant/error.go index 56a072f..468de5e 100644 --- a/apps/model/constant/error.go +++ b/apps/model/constant/error.go @@ -12,4 +12,3 @@ const ( WrongPassword string = "wrong password" ConfigNotExist string = "config not exist" ) - diff --git a/apps/model/constant/system.go b/apps/model/constant/system.go index 33810b7..f41ac13 100644 --- a/apps/model/constant/system.go +++ b/apps/model/constant/system.go @@ -18,4 +18,3 @@ const ( Version = "v0.0.22" ) - diff --git a/apps/model/dto/account.go b/apps/model/dto/account.go index e8f2e37..50b2915 100644 --- a/apps/model/dto/account.go +++ b/apps/model/dto/account.go @@ -34,4 +34,3 @@ type AccountUpdateDto struct { Deleted *int64 `json:"deleted" form:"deleted" validate:"omitempty,oneof=0 1"` Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=32"` } - diff --git a/apps/model/dto/config.go b/apps/model/dto/config.go index a139463..0646770 100644 --- a/apps/model/dto/config.go +++ b/apps/model/dto/config.go @@ -16,4 +16,3 @@ type ConfigUpdateDto struct { type ConfigsUpdateDto struct { ConfigUpdateDtos []ConfigUpdateDto `json:"configUpdateDtos" form:"configUpdateDtos" validate:"required"` } - diff --git a/apps/model/dto/dto.go b/apps/model/dto/dto.go index 88c0c7f..958fedd 100644 --- a/apps/model/dto/dto.go +++ b/apps/model/dto/dto.go @@ -10,4 +10,3 @@ type BaseDto struct { type IdDto struct { Id *int64 `json:"id" form:"id" validate:"required,gt=0"` // Первичный ключ } - diff --git a/apps/model/dto/hysteria2.go b/apps/model/dto/hysteria2.go index e41d8b2..834e89f 100644 --- a/apps/model/dto/hysteria2.go +++ b/apps/model/dto/hysteria2.go @@ -25,4 +25,3 @@ type Hysteria2UrlDto struct { AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"` Hostname *string `json:"hostname" form:"hostname" validate:"required,min=1,max=255"` } - diff --git a/apps/model/dto/log.go b/apps/model/dto/log.go index 58d9694..980843e 100644 --- a/apps/model/dto/log.go +++ b/apps/model/dto/log.go @@ -7,4 +7,3 @@ type LogDto struct { type LogExportDto struct { Option *int `json:"option" form:"option" validate:"required,oneof=0 1"` } - diff --git a/apps/model/dto/server.go b/apps/model/dto/server.go index 5e59346..66b93b8 100644 --- a/apps/model/dto/server.go +++ b/apps/model/dto/server.go @@ -3,4 +3,3 @@ package dto type ServerDto struct { Port *int64 `json:"port" form:"port" validate:"required,min=1,max=65535"` } - diff --git a/apps/model/entity/account.go b/apps/model/entity/account.go index 1a7d0b7..fe3da3d 100644 --- a/apps/model/entity/account.go +++ b/apps/model/entity/account.go @@ -14,8 +14,8 @@ type Account struct { Deleted *int64 `gorm:"column:deleted;default:0" json:"deleted"` BaseEntity `gorm:"embedded"` - LoginAt *int64 `gorm:"column:login_at;default:0" json:"loginAt"` - ConAt *int64 `gorm:"column:con_at;default:0" json:"conAt"` - Remark *string `gorm:"column:remark;default:''" json:"remark"` + LoginAt *int64 `gorm:"column:login_at;default:0" json:"loginAt"` + ConAt *int64 `gorm:"column:con_at;default:0" json:"conAt"` + Remark *string `gorm:"column:remark;default:''" json:"remark"` + ForcePasswordChange *int64 `gorm:"column:force_password_change;default:0" json:"forcePasswordChange"` } - diff --git a/apps/model/entity/config.go b/apps/model/entity/config.go index aac7b87..e4ee747 100644 --- a/apps/model/entity/config.go +++ b/apps/model/entity/config.go @@ -6,4 +6,3 @@ type Config struct { Remark *string `gorm:"column:remark;default:''" json:"remark"` BaseEntity `gorm:"embedded"` } - diff --git a/apps/model/entity/entity.go b/apps/model/entity/entity.go index eb07537..897be17 100644 --- a/apps/model/entity/entity.go +++ b/apps/model/entity/entity.go @@ -7,4 +7,3 @@ type BaseEntity struct { CreateTime *time.Time `gorm:"column:create_time;default:null" json:"createTime"` UpdateTime *time.Time `gorm:"column:update_time;default:null" json:"updateTime"` } - diff --git a/apps/model/vo/account.go b/apps/model/vo/account.go index 1cd3f08..415a5e6 100644 --- a/apps/model/vo/account.go +++ b/apps/model/vo/account.go @@ -29,4 +29,3 @@ type AccountInfoVo struct { Username string `json:"username"` Roles []string `json:"roles"` } - diff --git a/apps/model/vo/config.go b/apps/model/vo/config.go index c61da54..3f1019c 100644 --- a/apps/model/vo/config.go +++ b/apps/model/vo/config.go @@ -5,4 +5,3 @@ type ConfigVo struct { Value string `json:"value"` Remark string `json:"remark"` } - diff --git a/apps/model/vo/hysteria2.go b/apps/model/vo/hysteria2.go index 2bc0c9b..e37e074 100644 --- a/apps/model/vo/hysteria2.go +++ b/apps/model/vo/hysteria2.go @@ -38,4 +38,3 @@ type Hysteria2AcmePathVo struct { CrtPath string `json:"crtPath"` KeyPath string `json:"keyPath"` } - diff --git a/apps/model/vo/jwt.go b/apps/model/vo/jwt.go index caea165..8d3e73d 100644 --- a/apps/model/vo/jwt.go +++ b/apps/model/vo/jwt.go @@ -1,7 +1,7 @@ package vo type JwtVo struct { - TokenType string `json:"tokenType"` - AccessToken string `json:"accessToken"` + TokenType string `json:"tokenType"` + AccessToken string `json:"accessToken"` + ForcePasswordChange bool `json:"forcePasswordChange"` } - diff --git a/apps/model/vo/log.go b/apps/model/vo/log.go index 2220665..f4fc456 100644 --- a/apps/model/vo/log.go +++ b/apps/model/vo/log.go @@ -21,4 +21,3 @@ type LogHysteria2Vo struct { Msg string `json:"msg"` Time string `json:"time"` } - diff --git a/apps/model/vo/monitor.go b/apps/model/vo/monitor.go index c0259fd..d741d72 100644 --- a/apps/model/vo/monitor.go +++ b/apps/model/vo/monitor.go @@ -13,4 +13,3 @@ type Hysteria2MonitorVo struct { Version string `json:"version"` // Версия Running bool `json:"running"` // Статус выполнения } - diff --git a/apps/model/vo/result.go b/apps/model/vo/result.go index 49c0b87..3e9e331 100644 --- a/apps/model/vo/result.go +++ b/apps/model/vo/result.go @@ -44,4 +44,3 @@ func Fail(message string, c *gin.Context) { Data: nil, }) } - diff --git a/apps/model/vo/vo.go b/apps/model/vo/vo.go index 4f5c816..6fa534a 100644 --- a/apps/model/vo/vo.go +++ b/apps/model/vo/vo.go @@ -6,4 +6,3 @@ type BaseVo struct { Id int64 `json:"id"` CreateTime time.Time `json:"createTime"` } - diff --git a/apps/proxy/hysteria2.go b/apps/proxy/hysteria2.go index b91a0a6..a8be8d1 100644 --- a/apps/proxy/hysteria2.go +++ b/apps/proxy/hysteria2.go @@ -56,4 +56,3 @@ func (h *Hysteria2Process) Release() error { } return nil } - diff --git a/apps/proxy/hysteria2_api.go b/apps/proxy/hysteria2_api.go index 283d914..ded58ba 100644 --- a/apps/proxy/hysteria2_api.go +++ b/apps/proxy/hysteria2_api.go @@ -134,4 +134,3 @@ func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) { } return onlineUsers, nil } - diff --git a/apps/proxy/process.go b/apps/proxy/process.go index 30453c6..d2b27f1 100644 --- a/apps/proxy/process.go +++ b/apps/proxy/process.go @@ -227,4 +227,3 @@ func (p *process) handleLogs(stdout, stderr io.ReadCloser) { } } } - diff --git a/apps/router/account.go b/apps/router/account.go index 639e838..c83130c 100644 --- a/apps/router/account.go +++ b/apps/router/account.go @@ -21,4 +21,3 @@ func initAccountAdminRouter(accountApi *gin.RouterGroup) { account.GET("/verifyDefaultPass", controller.VerifyDefaultPass) } } - diff --git a/apps/router/auth.go b/apps/router/auth.go index c1ab846..569a477 100644 --- a/apps/router/auth.go +++ b/apps/router/auth.go @@ -11,4 +11,3 @@ func initAuthRouter(authApi *gin.RouterGroup) { auth.POST("/login", controller.Login) } } - diff --git a/apps/router/config.go b/apps/router/config.go index 3d6ee0d..7a8e6e4 100644 --- a/apps/router/config.go +++ b/apps/router/config.go @@ -22,4 +22,3 @@ func initConfigRouter(configApi *gin.RouterGroup) { config.POST("/uploadCertFile", controller.UploadCertFile) } } - diff --git a/apps/router/hysteria2.go b/apps/router/hysteria2.go index 8576758..ce2ceb6 100644 --- a/apps/router/hysteria2.go +++ b/apps/router/hysteria2.go @@ -24,4 +24,3 @@ func initHysteria2Router(hysteria2Api *gin.RouterGroup) { hysteria2.GET("/hysteria2Url", controller.Hysteria2Url) } } - diff --git a/apps/router/log.go b/apps/router/log.go index 50bc08a..452ddc9 100644 --- a/apps/router/log.go +++ b/apps/router/log.go @@ -13,4 +13,3 @@ func initLogRouter(accountApi *gin.RouterGroup) { account.POST("/exportLog", controller.ExportLog) } } - diff --git a/apps/router/monitor.go b/apps/router/monitor.go index 24ce4df..96792ee 100644 --- a/apps/router/monitor.go +++ b/apps/router/monitor.go @@ -12,4 +12,3 @@ func initMonitorRouter(accountApi *gin.RouterGroup) { account.GET("/monitorHysteria2", controller.MonitorHysteria2) } } - diff --git a/apps/router/router.go b/apps/router/router.go index 03b068b..69cdeaf 100644 --- a/apps/router/router.go +++ b/apps/router/router.go @@ -39,4 +39,3 @@ func Router(router *gin.Engine, huiWebContext *string) { } } } - diff --git a/apps/service/account.go b/apps/service/account.go index 796d44c..5ee9aa6 100644 --- a/apps/service/account.go +++ b/apps/service/account.go @@ -10,20 +10,40 @@ import ( "hy2xs-admin/model/dto" "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" + "hy2xs-admin/util" ) -func Login(username string, pass string) (string, error) { - account, err := dao.GetAccount("username = ? and pass = ? and role = 'admin' and deleted = 0", username, pass) +func Login(username string, plainPassword string) (string, bool, error) { + account, err := dao.GetAccount("username = ? and role = 'admin' and deleted = 0", username) if err != nil { - return "", err + return "", false, err } + + verified, legacy := util.VerifyPassword(plainPassword, *account.Pass) + if !verified { + return "", false, errors.New(constant.WrongPassword) + } + + if legacy { + hash, hashErr := util.HashPassword(plainPassword) + if hashErr == nil { + _ = dao.UpdateAccount([]int64{*account.Id}, map[string]interface{}{"pass": hash}) + } + } + accountBo := bo.AccountBo{ Id: *account.Id, Username: *account.Username, Roles: []string{*account.Role}, Deleted: *account.Deleted, } - return GenToken(accountBo) + token, tokenErr := GenToken(accountBo) + if tokenErr != nil { + return "", false, tokenErr + } + + requirePasswordChange := legacy + return token, requirePasswordChange, nil } func PageAccount(accountPageDto dto.AccountPageDto) ([]entity.Account, int64, error) { @@ -156,4 +176,3 @@ func GetAccountInfo(c *gin.Context) (vo.AccountInfoVo, error) { Roles: myClaims.AccountBo.Roles, }, nil } - diff --git a/apps/service/config.go b/apps/service/config.go index b86d8d1..8f4fe8c 100644 --- a/apps/service/config.go +++ b/apps/service/config.go @@ -74,23 +74,23 @@ func GetHysteria2Config() (bo.Hysteria2ServerConfig, error) { func UpdateHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error { // Значения по умолчанию - config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret}) + config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret}) if err != nil { return err } var hUIWebPort string - var jwtSecret string + var trafficStatsSecret string for _, item := range config { if *item.Key == constant.HUIWebPort { hUIWebPort = *item.Value - } else if *item.Key == constant.JwtSecret { - jwtSecret = *item.Value + } else if *item.Key == constant.Hysteria2TrafficStatsSecret { + trafficStatsSecret = *item.Value } } - if hUIWebPort == "" || jwtSecret == "" { - logrus.Errorf("hUIWebPort or jwtSecret is nil") + if hUIWebPort == "" || trafficStatsSecret == "" { + logrus.Errorf("hUIWebPort or trafficStatsSecret is nil") return errors.New(constant.SysError) } @@ -108,7 +108,7 @@ func UpdateHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error http.Insecure = &authHttpInsecure auth.HTTP = &http hysteria2ServerConfig.Auth = &auth - hysteria2ServerConfig.TrafficStats.Secret = &jwtSecret + hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret yamlConfig, err := yaml.Marshal(&hysteria2ServerConfig) if err != nil { @@ -195,4 +195,3 @@ func GetAuthHttpUrl() (string, error) { } return fmt.Sprintf("%s://127.0.0.1:%d%s/hui/hysteria2/auth", protocol, port, webContext), nil } - diff --git a/apps/service/cron.go b/apps/service/cron.go index 13e0685..5aaf25b 100644 --- a/apps/service/cron.go +++ b/apps/service/cron.go @@ -27,16 +27,16 @@ func CronHandleAccount() { return } - jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret) + trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { return } // Сохранение данных трафика - go saveAccountTraffic(apiPort, *jwtSecretConfig.Value) + go saveAccountTraffic(apiPort, *trafficSecretConfig.Value) // Принудительное отключение - go kickAccount(apiPort, *jwtSecretConfig.Value) + go kickAccount(apiPort, *trafficSecretConfig.Value) } }() } @@ -58,7 +58,7 @@ func CronResetTraffic() { } } -func saveAccountTraffic(apiPort int64, jwtSecret string) { +func saveAccountTraffic(apiPort int64, trafficStatsSecret string) { if !trafficMutex.TryLock() { return } @@ -74,7 +74,7 @@ func saveAccountTraffic(apiPort int64, jwtSecret string) { return } - users, err := proxy.NewHysteria2Api(apiPort).ListUsers(true, jwtSecret) + users, err := proxy.NewHysteria2Api(apiPort).ListUsers(true, trafficStatsSecret) if err != nil { return } @@ -96,13 +96,13 @@ func saveAccountTraffic(apiPort int64, jwtSecret string) { } } -func kickAccount(apiPort int64, jwtSecret string) { +func kickAccount(apiPort int64, trafficStatsSecret string) { if !kickMutex.TryLock() { return } defer kickMutex.Unlock() - users, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(jwtSecret) + users, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(trafficStatsSecret) if err != nil { return } @@ -130,7 +130,7 @@ func kickAccount(apiPort int64, jwtSecret string) { kickUsernames[j] = *item.Username j++ } - if err = proxy.NewHysteria2Api(apiPort).KickUsers(kickUsernames, jwtSecret); err != nil { + if err = proxy.NewHysteria2Api(apiPort).KickUsers(kickUsernames, trafficStatsSecret); err != nil { return } }(usernameList) @@ -138,4 +138,3 @@ func kickAccount(apiPort int64, jwtSecret string) { wg.Wait() } } - diff --git a/apps/service/forward.go b/apps/service/forward.go index 359141f..6c42da9 100644 --- a/apps/service/forward.go +++ b/apps/service/forward.go @@ -232,4 +232,3 @@ func iptablesRules(protocol string) ([]string, error) { rules := strings.Split(output, "\n") return rules, nil } - diff --git a/apps/service/hysteria2.go b/apps/service/hysteria2.go index 50555cb..9fc54a1 100644 --- a/apps/service/hysteria2.go +++ b/apps/service/hysteria2.go @@ -65,16 +65,33 @@ func setHysteria2ConfigYAML() error { logrus.Errorf("marshal hysteria2 config err: %v", err) return errors.New("marshal hysteria2 config err") } - file, err := os.OpenFile(constant.Hysteria2ConfigPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + tmpPath := fmt.Sprintf("%s.tmp", constant.Hysteria2ConfigPath) + file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) if err != nil { logrus.Errorf("create hysteria2 server config file err: %v", err) return errors.New("create hysteria2 server config file err") } + defer file.Close() _, err = file.WriteString(string(hysteria2Config)) if err != nil { logrus.Errorf("write hysteria2 config.json file err: %v", err) return errors.New("hysteria2 config.json file write err") } + if syncErr := file.Sync(); syncErr != nil { + return syncErr + } + if closeErr := file.Close(); closeErr != nil { + return closeErr + } + if renameErr := os.Rename(tmpPath, constant.Hysteria2ConfigPath); renameErr != nil { + return renameErr + } + if chmodErr := os.Chmod(constant.Hysteria2ConfigPath, 0600); chmodErr != nil { + return chmodErr + } + if chownErr := os.Chown(constant.Hysteria2ConfigPath, 0, 0); chownErr != nil { + return chownErr + } return nil } @@ -147,4 +164,3 @@ func Hysteria2AcmePath() (vo.Hysteria2AcmePathVo, error) { } return vo.Hysteria2AcmePathVo{}, errors.New("cert not found") } - diff --git a/apps/service/hysteria2_api.go b/apps/service/hysteria2_api.go index d6ec7f3..37a3fe5 100644 --- a/apps/service/hysteria2_api.go +++ b/apps/service/hysteria2_api.go @@ -8,11 +8,28 @@ import ( "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/proxy" + "net" "net/url" + "strconv" "strings" "time" ) +func parseListenPort(listen string) (int, error) { + host, port, err := net.SplitHostPort(listen) + if err != nil { + return 0, err + } + if host == "" || port == "" { + return 0, errors.New("invalid listen address") + } + value, convErr := strconv.Atoi(port) + if convErr != nil || value < 1 || value > 65535 { + return 0, errors.New("invalid listen port") + } + return value, nil +} + func Hysteria2Auth(conPass string) (int64, string, error) { if !Hysteria2IsRunning() { return 0, "", errors.New("hysteria2 is not running") @@ -45,11 +62,11 @@ func Hysteria2Online() (map[string]int64, error) { if err != nil { return nil, errors.New("get hysteria2 apiPort err") } - jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret) + trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { return nil, err } - onlineUsers, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(*jwtSecretConfig.Value) + onlineUsers, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(*trafficSecretConfig.Value) if err != nil { return nil, err } @@ -76,11 +93,11 @@ func Hysteria2Kick(ids []int64, kickUtilTime int64) error { if err != nil { return errors.New("get hysteria2 apiPort err") } - jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret) + trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { return err } - if err = proxy.NewHysteria2Api(apiPort).KickUsers(keys, *jwtSecretConfig.Value); err != nil { + if err = proxy.NewHysteria2Api(apiPort).KickUsers(keys, *trafficSecretConfig.Value); err != nil { return err } return nil @@ -125,13 +142,13 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string, hysteria2Name = *hysteria2ConfigRemark.Value } - hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping) + userInfo := "" + configStr := "" + listenPort, err := parseListenPort(*hysteria2Config.Listen) if err != nil { return "", "", err } - - userInfo := "" - configStr := "" + publicHost := strings.Split(host, ":")[0] if clientType == constant.Shadowrocket || clientType == constant.Clash { userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", *account.Upload, @@ -142,9 +159,8 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string, hysteria2 := bo.Hysteria2{ Name: hysteria2Name, Type: "hysteria2", - Server: strings.Split(host, ":")[0], - Port: strings.Split(*hysteria2Config.Listen, ":")[1], - Ports: *hysteria2ConfigPortHopping.Value, + Server: publicHost, + Port: strconv.Itoa(listenPort), Password: conPass, } @@ -226,6 +242,13 @@ func Hysteria2Url(accountId int64, hostname string) (string, error) { if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" { return "", errors.New("hysteria2 config is empty") } + port, err := parseListenPort(*hysteria2Config.Listen) + if err != nil { + return "", err + } + if hostname == "" || hostname == "0.0.0.0" { + return "", errors.New("invalid public host") + } account, err := dao.GetAccount("id = ?", accountId) if err != nil { @@ -259,15 +282,6 @@ func Hysteria2Url(accountId int64, hostname string) (string, error) { urlConfig += fmt.Sprintf("&downmbps=%s", url.PathEscape(*hysteria2Config.Bandwidth.Down)) } - hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping) - if err != nil { - return "", err - } - if *hysteria2ConfigPortHopping.Value != "" { - // shadowrocket - urlConfig += fmt.Sprintf("&mport=%s", *hysteria2ConfigPortHopping.Value) - } - hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark) if err != nil { return "", err @@ -278,6 +292,5 @@ func Hysteria2Url(accountId int64, hostname string) (string, error) { if urlConfig != "" { urlConfig = "/?" + strings.TrimPrefix(urlConfig, "&") } - return fmt.Sprintf("hysteria2://%s@%s%s", *account.ConPass, hostname, *hysteria2Config.Listen) + urlConfig, nil + return fmt.Sprintf("hysteria2://%s@%s:%d", *account.ConPass, hostname, port) + urlConfig, nil } - diff --git a/apps/service/jwt.go b/apps/service/jwt.go index 7594991..7ec344c 100644 --- a/apps/service/jwt.go +++ b/apps/service/jwt.go @@ -58,4 +58,3 @@ func GetToken(c *gin.Context) string { } return strings.SplitN(tokenStr, " ", 2)[1] } - diff --git a/apps/service/monitor.go b/apps/service/monitor.go index 99bff06..345334b 100644 --- a/apps/service/monitor.go +++ b/apps/service/monitor.go @@ -62,4 +62,3 @@ func MonitorHysteria2() (vo.Hysteria2MonitorVo, error) { hysteria2MonitorVo.Running = running return hysteria2MonitorVo, nil } - diff --git a/apps/service/server.go b/apps/service/server.go index 650630c..7c22617 100644 --- a/apps/service/server.go +++ b/apps/service/server.go @@ -67,4 +67,3 @@ func GetServerPortAndCert() (int64, string, string, error) { return port, crtPath, keyPath, nil } - diff --git a/apps/util/arr.go b/apps/util/arr.go index 5d33fb8..a3c7cf0 100644 --- a/apps/util/arr.go +++ b/apps/util/arr.go @@ -30,4 +30,3 @@ func SplitArr[T any](arr []T, num int) [][]T { return segments } - diff --git a/apps/util/encrypt.go b/apps/util/encrypt.go index dcb6595..f0c34c1 100644 --- a/apps/util/encrypt.go +++ b/apps/util/encrypt.go @@ -2,7 +2,11 @@ package util import ( "crypto/sha256" + "errors" "fmt" + "strings" + + "golang.org/x/crypto/bcrypt" ) func SHA224String(password string) string { @@ -16,3 +20,25 @@ func SHA224String(password string) string { return str } +func HashPassword(password string) (string, error) { + if len(strings.TrimSpace(password)) < 6 { + return "", errors.New("password too short") + } + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func IsBcryptHash(hash string) bool { + return strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") +} + +func VerifyPassword(password string, storedHash string) (ok bool, legacy bool) { + if IsBcryptHash(storedHash) { + err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) + return err == nil, false + } + return SHA224String(password) == storedHash, true +} diff --git a/apps/util/encrypt_test.go b/apps/util/encrypt_test.go index 1db2cba..fb0f98d 100644 --- a/apps/util/encrypt_test.go +++ b/apps/util/encrypt_test.go @@ -5,4 +5,3 @@ import "testing" func TestSHA224String(t *testing.T) { println(SHA224String("sysadmin")) } - diff --git a/apps/util/export.go b/apps/util/export.go index 09b2e38..74da5e5 100644 --- a/apps/util/export.go +++ b/apps/util/export.go @@ -38,4 +38,3 @@ func ExportFile(filePath string, data any, t int) error { } return nil } - diff --git a/apps/util/file.go b/apps/util/file.go index 399bec8..a6a79cc 100644 --- a/apps/util/file.go +++ b/apps/util/file.go @@ -86,4 +86,3 @@ func FindFile(dir, filename string) (string, error) { } return result, nil } - diff --git a/apps/util/github.go b/apps/util/github.go deleted file mode 100644 index b43436c..0000000 --- a/apps/util/github.go +++ /dev/null @@ -1,58 +0,0 @@ -package util - -import ( - "context" - "fmt" - "github.com/google/go-github/v39/github" -) - -var githubClient *github.Client - -func init() { - githubClient = github.NewClient(nil) -} - -func GetReleaseAssetURL(owner, repo, version, fileName string) (string, error) { - ctx := context.Background() - - var release *github.RepositoryRelease - var err error - if version != "" { - release, _, err = githubClient.Repositories.GetReleaseByTag(ctx, owner, repo, version) - if err != nil { - return "", fmt.Errorf("failed to get release for version %s: %v", version, err) - } - } else { - releases, _, err := githubClient.Repositories.ListReleases(ctx, owner, repo, nil) - if err != nil { - return "", fmt.Errorf("failed to list releases: %v", err) - } - if len(releases) == 0 { - return "", fmt.Errorf("no releases found") - } - release = releases[0] - } - - assets, _, err := githubClient.Repositories.ListReleaseAssets(ctx, owner, repo, release.GetID(), nil) - if err != nil { - return "", fmt.Errorf("failed to list release assets: %v", err) - } - - for _, asset := range assets { - if asset.GetName() == fileName { - return asset.GetBrowserDownloadURL(), nil - } - } - - return "", fmt.Errorf("file '%s' not found in release '%s'", fileName, release.GetTagName()) -} - -func ListRelease(owner, repo string) ([]*github.RepositoryRelease, error) { - ctx := context.Background() - releases, _, err := githubClient.Repositories.ListReleases(ctx, owner, repo, nil) - if err != nil { - return nil, fmt.Errorf("failed to list releases: %v", err) - } - return releases, nil -} - diff --git a/apps/util/hysteria2.go b/apps/util/hysteria2.go index 1a41fb3..bf224a4 100644 --- a/apps/util/hysteria2.go +++ b/apps/util/hysteria2.go @@ -1,66 +1,9 @@ package util import ( - "fmt" "hy2xs-admin/model/constant" - "io" - "net/http" - "os" - "runtime" ) func GetHysteria2BinPath() string { return constant.Hysteria2BinPath } - -func GetHysteria2BinName() string { - hysteria2FileName := fmt.Sprintf("hysteria-%s-%s", runtime.GOOS, runtime.GOARCH) - if runtime.GOOS == "windows" { - hysteria2FileName += ".exe" - } - return hysteria2FileName -} - -func DownloadHysteria2(version string) error { - hysteria2BinName := GetHysteria2BinName() - hysteria2BinPath := GetHysteria2BinPath() - - // Download the latest version of Hysteria2 - url, err := GetReleaseAssetURL("apernet", "hysteria", version, hysteria2BinName) - if err != nil { - return err - } - - resp, err := http.Get(url) - defer resp.Body.Close() - if err != nil { - return fmt.Errorf("failed to download file: %v", err) - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download file, status code: %d", resp.StatusCode) - } - - if Exists(hysteria2BinPath) { - if err = os.Remove(hysteria2BinPath); err != nil { - return fmt.Errorf("failed to remove existing file: %v", err) - } - } - - file, err := os.Create(hysteria2BinPath) - defer file.Close() - if err != nil { - return fmt.Errorf("failed to create file %s: %v", hysteria2BinPath, err) - } - - _, err = io.Copy(file, resp.Body) - if err != nil { - return fmt.Errorf("failed to write to file: %v", err) - } - - if err = os.Chmod(hysteria2BinPath, 0755); err != nil { - return fmt.Errorf("failed to change file permissions: %v", err) - } - return nil -} - diff --git a/apps/util/linux.go b/apps/util/linux.go index 4e205a5..8db725d 100644 --- a/apps/util/linux.go +++ b/apps/util/linux.go @@ -98,4 +98,3 @@ func VerifyPort(port string) error { } return nil } - diff --git a/apps/util/map.go b/apps/util/map.go index 7169d42..9b86a32 100644 --- a/apps/util/map.go +++ b/apps/util/map.go @@ -21,4 +21,3 @@ func SplitMap[T any](inputMap map[string]T, chunkSize int) []map[string]T { return segments } - diff --git a/apps/util/rand.go b/apps/util/rand.go index 754f710..00bc91d 100644 --- a/apps/util/rand.go +++ b/apps/util/rand.go @@ -17,4 +17,3 @@ func RandomString(length int) (string, error) { return string(bytes), nil } - diff --git a/apps/util/string.go b/apps/util/string.go index 5ebbafd..2a145a1 100644 --- a/apps/util/string.go +++ b/apps/util/string.go @@ -34,4 +34,3 @@ func CompareVersion(version1, version2 string) int { // The version number is exactly the same return 0 } - diff --git a/docs/03-server-hysteria2.md b/docs/03-server-hysteria2.md index b0c765c..c3dfa11 100644 --- a/docs/03-server-hysteria2.md +++ b/docs/03-server-hysteria2.md @@ -59,11 +59,18 @@ Hysteria2 — основной транспортный компонент се ## TLS Требования: -- нормальный домен +- production default: `acme` +- поддерживаемые режимы: `acme | file | self_signed_dev` +- `self_signed_dev` только для dev/lab - корректный `server_name` / SNI на клиентах - одна понятная TLS policy - без смешивания нескольких несовместимых схем по умолчанию +Инварианты: +- `acme` -> только `acme` block в конфиге; +- `file` -> только `tls.cert`/`tls.key` block; +- `self_signed_dev` -> только dev сценарии. + ## Auth policy Для baseline выбирается одна предсказуемая auth-модель. @@ -88,6 +95,7 @@ Hysteria2 — основной транспортный компонент се - `/etc/hysteria/config.yaml` - `/var/lib/hysteria/` +- `/etc/hy2xs/hy2xs.env` - `/etc/hysteria/post-install.env` ## Серверные инварианты diff --git a/docs/04-admin-panel-h-ui-fork.md b/docs/04-admin-panel-h-ui-fork.md index 02ead90..fcff131 100644 --- a/docs/04-admin-panel-h-ui-fork.md +++ b/docs/04-admin-panel-h-ui-fork.md @@ -57,6 +57,7 @@ Bundled H UI должна: - поставляться внутри итогового пакета - иметь свой install dir - иметь свой data dir +- иметь rootless systemd unit (`hy2xs-admin`) - запускаться отдельным systemd unit - не требовать target-side build @@ -76,6 +77,7 @@ Bundled H UI должна: - Hysteria2 устанавливается install-оркестратором с official upstream - Hysteria2 запускается отдельным `hysteria-server.service` - HY2XS admin работает как operator UI и HTTP auth/traffic layer +- HY2XS admin не запускается от root - смена версии Hysteria2 через UI отключена в baseline - список upstream releases не является частью operator UI baseline @@ -85,6 +87,7 @@ Bundled H UI должна: - склеивать unit Hysteria и unit H UI в один сервис - раздувать оркестратор из-за особенностей UI - использовать HY2XS admin как updater бинаря Hysteria2 +- использовать `JWT_SECRET` как `trafficStats.secret` для Hysteria API ## Что фиксировать в `post-install.env` diff --git a/docs/05-client-and-access-scope.md b/docs/05-client-and-access-scope.md index fd4d20c..77802d5 100644 --- a/docs/05-client-and-access-scope.md +++ b/docs/05-client-and-access-scope.md @@ -9,7 +9,7 @@ В baseline этого пакета docs входит только следующее: - установка Hysteria2 - установка HY2XS admin -- базовая настройка systemd / firewall / `post-install.env` +- базовая настройка systemd / firewall / `hy2xs.env` / `post-install.env` - подготовка рабочего серверного окружения ## Что не входит в baseline @@ -31,6 +31,15 @@ Но это не делает access layer частью install baseline. +## Публичный endpoint + +В клиентской части используется только `public_host/public_port`. + +Инварианты: +- `listen` и `public endpoint` разделены; +- в клиентских URL не используется `0.0.0.0`; +- проект остаётся IPv4-only. + ## Почему это важно Если смешать install baseline и delivery layer, документация начинает неверно обещать лишнее: diff --git a/docs/07-systemd-and-firewall.md b/docs/07-systemd-and-firewall.md index 7d97e04..f2c2962 100644 --- a/docs/07-systemd-and-firewall.md +++ b/docs/07-systemd-and-firewall.md @@ -21,10 +21,19 @@ Базовые требования: - отдельный unit `hy2xs-admin.service` +- запуск от `User=hy2xs-admin`, не от root - отдельный install dir - отдельный data dir - отдельный жизненный цикл от Hysteria +Рекомендуемый hardening: +- `NoNewPrivileges=true` +- `PrivateTmp=true` +- `ProtectHome=true` +- `ProtectSystem=strict` +- `ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs` +- `RestrictAddressFamilies=AF_INET AF_UNIX` + Важно: - HY2XS admin не должен запускаться как часть unit Hysteria - unit-файлы не должны быть склеены @@ -36,14 +45,21 @@ - TCP-порт SSH - established/related traffic +IPv4-only policy: +- использовать `table ip`, а не `table inet`; +- IPv6 правила не добавлять; +- UI-порт разрешать только с локального bind-host. + После staged-проверки можно включать default policy `drop`. ## Порядок применения -1. Добавить allow-правила. -2. Проверить, что SSH-сессия не теряется. -3. Проверить listen Hysteria-порта. -4. Только потом затягивать policy. +1. Подготовить candidate-файл (`/etc/nftables.d/hy2xs.nft`). +2. Проверить `nft -c -f`. +3. Создать rollback timer. +4. Применить candidate и проверить SSH/Hysteria/UI. +5. При успехе отменить rollback timer. +6. При провале — rollback. ## Что не делаем diff --git a/docs/08-orchestrator-spec.md b/docs/08-orchestrator-spec.md index 97c655b..7676030 100644 --- a/docs/08-orchestrator-spec.md +++ b/docs/08-orchestrator-spec.md @@ -18,14 +18,15 @@ ## Главная роль оркестратора -Оркестратор работает **только на target machine** и умеет только: -- выполнить первичную установку +Оркестратор работает **только на target machine** и умеет: +- выполнить первичную установку (`install`) +- выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`) - разложить bundled UI - скачать Hysteria2 из official upstream -- создать базовые конфиги +- создать/обновить конфиги - создать unit-файлы -- применить baseline firewall -- создать `post-install.env` +- применить staged firewall +- создать `post-install.env` и runtime env-файл ## Оркестратор не умеет @@ -112,15 +113,16 @@ ## CLI baseline -Допустимые флаги: -- `--non-interactive` -- `--domain` -- `--port` -- `--ssh-port` -- `--skip-firewall` -- `--skip-start` -- `--ui-port` -- `--ui-bind-host` +Команды: +- `install --package-dir [--config /etc/hy2xs/hy2xs.env]` +- `reconfigure --package-dir --config /etc/hy2xs/hy2xs.env --dry-run` +- `reconfigure --package-dir --config /etc/hy2xs/hy2xs.env --apply` + +Инварианты: +- только IPv4 bind/listen; +- TLS modes: `acme | file | self_signed_dev`; +- `trafficStats.secret` отдельный от `JWT_SECRET`; +- при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail. ## Что не реализовывать diff --git a/docs/09-post-install-env.md b/docs/09-post-install-env.md index 0b709d0..1eb2a1b 100644 --- a/docs/09-post-install-env.md +++ b/docs/09-post-install-env.md @@ -1,12 +1,20 @@ -# Post-install env +# Runtime env и post-install snapshot ## Цель документа -Зафиксировать `post-install.env` как компактный deploy reference file после первичной установки. +Зафиксировать двухслойную модель: + +- editable runtime env: `/etc/hy2xs/hy2xs.env` +- generated deploy snapshot: `/etc/hysteria/post-install.env` ## Зачем нужен файл -После первичной установки оператору нужна одна точка, где видно: +После первичной установки оператору нужны: + +1) runtime-файл, который оркестратор читает и валидирует; +2) snapshot-файл фактического deploy-состояния. + +В snapshot видно: - какой пакет был установлен - какой build артефакт использован - какой стек оркестратора применён @@ -24,12 +32,15 @@ - не заменяет runtime-конфиги - не превращает target в builder -## Рекомендуемый путь +## Рекомендуемые пути ```bash +/etc/hy2xs/hy2xs.env /etc/hysteria/post-install.env ``` +Оба файла должны иметь права `0600`. + ## Минимальный набор переменных ### Deploy / package @@ -73,18 +84,18 @@ - `HUI_INSTALL_DIR` - `HUI_DATA_DIR` -## Как работать с файлом +## Как работать с файлами Правильная модель: -1. оркестратор создаёт файл при первичной установке -2. оператор использует файл как reference/source-of-truth -3. при необходимости оператор вручную переносит изменения в реальные рабочие конфиги -4. затем оператор применяет изменения документированным способом +1. оркестратор создаёт `hy2xs.env` и `post-install.env` при установке; +2. оператор редактирует только `hy2xs.env`; +3. оператор запускает `reconfigure --dry-run`, затем `reconfigure --apply`; +4. оркестратор обновляет runtime и перезаписывает snapshot. ## Что нельзя делать - сваливать туда временный мусор -- считать, что edit env автоматически меняет runtime +- считать, что edit env автоматически меняет runtime без `reconfigure --apply` - использовать файл как замену настоящей конфигурации сервисов ## Пример diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index 54ad7ca..2cd87a6 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -25,21 +25,26 @@ 3. bundled HY2XS admin раскладывается локально из пакета 4. создаются нужные каталоги 5. создаются systemd unit-файлы -6. создаётся `post-install.env` -7. baseline firewall применяется корректно +6. создаются `hy2xs.env` и `post-install.env` с правами `0600` +7. baseline firewall применяется корректно через staged mode 8. SSH остаётся доступным +9. `reconfigure --dry-run` выводит план изменений +10. `reconfigure --apply` применяет изменения и проходит smoke ## C. Runtime tests 1. `hysteria-server` active 2. `hy2xs-admin` active -3. UDP-порт слушается -4. HY2XS admin открывается по ожидаемому admin path +3. Hysteria слушает только IPv4 (`0.0.0.0:`) +4. HY2XS admin слушает ожидаемый `HY2XS_UI_BIND_HOST:` 5. тестовый совместимый клиент подключается 6. идёт реальный трафик 7. лимит 50/50 Mbps соблюдается при согласованной клиентской конфигурации 8. reboot не ломает baseline 9. Hysteria2 управляется systemd unit, а не внутренним updater'ом admin panel +10. нет IPv6 listen (`[::]`) для Hysteria/HY2XS admin +11. `trafficStats.secret` не равен `JWT_SECRET` +12. bootstrap admin secret существует и имеет `0600` ## D. Negative tests @@ -51,6 +56,8 @@ 6. Hysteria upstream недоступен 7. firewall применился частично 8. install flow прерван посередине +9. попытка использовать `HY2XS_IPV6_ENABLED=true` +10. `HY2XS_PUBLIC_HOST=0.0.0.0` ## Acceptance criteria @@ -64,3 +71,5 @@ 6. оркестратор зафиксирован как Bun/TypeScript stack и поставляется как готовый install-артефакт 7. оркестратор не требует update / rollback / uninstall логики 8. Telegram/access layer не требуется для прохождения install acceptance +9. отсутствует production path для port hopping +10. UI не запускается от root diff --git a/docs/examples/post-install.env.example b/docs/examples/post-install.env.example index b0168fd..b7a6361 100644 --- a/docs/examples/post-install.env.example +++ b/docs/examples/post-install.env.example @@ -9,16 +9,27 @@ PACKAGE_VERSION=0.1.0 ORCH_SOURCE_STACK=bun-typescript ORCH_BUILD_MODE=bun-compile -ORCH_BUILD_ID=orch-build-20260413-001 +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 @@ -27,9 +38,10 @@ HY2_IGNORE_CLIENT_BANDWIDTH=false HY2_CONFIG_PATH=/etc/hysteria/config.yaml HUI_ENABLED=true -HUI_FORK_REF=main -HUI_BUILD_ID=hy2xs-admin-build-20260413-001 +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 diff --git a/fix1.txt b/fix1.txt new file mode 100644 index 0000000..d7f0091 --- /dev/null +++ b/fix1.txt @@ -0,0 +1,705 @@ +Ты работаешь с проектом HY2XS_flamy / HY2XS. + +Контекст проекта: +HY2XS — это production installer/orchestrator для установки Hysteria2 + встроенной HY2XS admin panel на Debian 12 VPS. Оркестратор написан на Bun + TypeScript, admin panel является fork/refactor h-ui и собирается заранее, на target-сервере сборки быть не должно. Hysteria2 должен оставаться upstream vanilla binary. Проект не должен превращаться в Telegram-бота, CRM или generic server manager. Это узкий installer/runtime manager для Hysteria2 + HY2XS admin. + +Главная задача: +Провести production-hardening pass без костылей и без legacy. Исправить код, шаблоны, systemd, firewall, env/config layer, документацию и acceptance checks так, чтобы проект соответствовал официальным подходам Hysteria2/systemd и собственным docs проекта. + +ВАЖНО: +- Не делай косметические правки вместо архитектурных. +- Не добавляй временные обходы. +- Не оставляй legacy h-ui функциональность, если она не входит в scope HY2XS. +- Не меняй архитектуру на “собирать на target-сервере”. +- Не форкай Hysteria2. +- Не делай IPv6 support. В этом проекте серверы только IPv4. IPv6 должен быть явно out of scope. +- Все bind/listen должны быть IPv4-only. +- Все секреты должны быть chmod 600/root-only или доступны только нужному сервисному пользователю. +- После правок обнови документацию, примеры и acceptance criteria. + +Перед правками: +1. Изучи текущую структуру проекта: + - README.md + - docs/*.md + - hy2xs_implementation_plan-no_git/*.txt + - orchestrator/src/** + - package/templates/** + - package/systemd/** + - package/examples/** + - apps/** + - apps/frontend/** + - tools/build/** +2. Сверь спорные места с официальными docs: + - Hysteria2 server config + - Hysteria2 ACME/TLS + - Hysteria2 HTTP auth + - Hysteria2 trafficStats API + - Hysteria2 install script behavior + - systemd hardening options + - nftables safe apply practices + - Bun standalone compile behavior +3. Найди рассинхроны между docs и реализацией. +4. Исправь реализацию под production, а не просто подгони docs под текущий код. + +Ключевые проблемы, которые нужно исправить: + +1. Реализовать нормальный env/config layer + +Сейчас /etc/hysteria/post-install.env существует как reference-файл, но фактически не является полноценным слоем конфигурации. + +Нужно: +- Ввести единый runtime/deploy env-файл, который оркестратор умеет читать, валидировать и применять. +- Рекомендуемый путь: + - /etc/hy2xs/hy2xs.env как основной editable config layer + - /etc/hysteria/post-install.env как generated deployment snapshot/reference + Либо можно оставить один /etc/hysteria/post-install.env, но только если он реально становится читаемым оркестратором config layer. Предпочтительно разделить editable config и generated snapshot. +- Файл должен иметь права 0600. +- Файл должен содержать основные production-параметры: + - HY2XS_DOMAIN + - HY2XS_PUBLIC_HOST + - HY2XS_PUBLIC_PORT + - HY2XS_IPV6_ENABLED=false + - HY2XS_SSH_PORT + - HY2XS_FIREWALL_ENABLED + - HY2XS_FIREWALL_STAGED_APPLY + - HY2XS_UI_BIND_HOST=127.0.0.1 + - HY2XS_UI_PORT + - HY2XS_ADMIN_USER + - HY2XS_ADMIN_INITIAL_PASSWORD или путь к bootstrap secret file + - HY2XS_FORCE_PASSWORD_CHANGE=true + - HY2XS_TLS_MODE=acme|file|self_signed_dev + - HY2XS_ACME_EMAIL + - HY2XS_TLS_CERT_PATH + - HY2XS_TLS_KEY_PATH + - 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:/hui/hysteria2/auth + - HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1 + - HY2XS_HYSTERIA_TRAFFIC_STATS_PORT + - HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET + - HY2XS_HYSTERIA_OBFS_TYPE=salamander + - HY2XS_HYSTERIA_OBFS_PASSWORD + - HY2XS_HYSTERIA_BANDWIDTH_UP + - HY2XS_HYSTERIA_BANDWIDTH_DOWN + - HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false + - HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml + - HY2XS_HYSTERIA_VERSION or latest policy + - HY2XS_INSTALL_DIR=/opt/hy2xs-admin + - HY2XS_DATA_DIR=/var/lib/hy2xs-admin + - HY2XS_LOG_DIR=/var/log/hy2xs +- Добавить в orchestrator: + - загрузку env-файла; + - строгую валидацию; + - нормализацию типов; + - запрет несовместимых комбинаций; + - dry-run вывод; + - явную команду reconfigure. +- Изменение env-файла не должно автоматически менять runtime. +- Применение только через явную команду: + - hy2xs-orchestrator install --config /path/to/hy2xs.env + - hy2xs-orchestrator reconfigure --config /etc/hy2xs/hy2xs.env --dry-run + - hy2xs-orchestrator reconfigure --config /etc/hy2xs/hy2xs.env --apply +- Если reconfigure меняет port/listen/firewall/TLS, он должен: + - валидировать новый config; + - сделать backup текущих config/unit/firewall; + - применить staged; + - выполнить smoke-check; + - откатить изменения при провале. + +Файлы для правок: +- orchestrator/src/cli.ts +- orchestrator/src/types/context.ts +- orchestrator/src/commands/install.ts +- добавить orchestrator/src/commands/reconfigure.ts +- добавить orchestrator/src/config/env.ts или похожий модуль +- orchestrator/src/steps/env.ts +- package/templates/env/* +- package/examples/* +- docs/09-post-install-env.md +- docs/08-orchestrator-spec.md +- docs/11-testing-and-acceptance.md + +2. Зафиксировать IPv4-only модель + +Требование: +Проект работает только с IPv4. IPv6 не нужен и не должен случайно включаться. + +Нужно: +- В Hysteria config не использовать listen вида :443. +- Использовать только: + listen: 0.0.0.0: +- HY2XS admin должен слушать только IPv4 bind host: + 127.0.0.1: по умолчанию. +- Не использовать [::], ::, dual-stack listen. +- В docs явно указать: + - IPv6 unsupported/out of scope; + - все публичные входящие соединения идут через IPv4; + - домен должен иметь A-record; + - AAAA-record не используется и не требуется; + - если у домена есть AAAA, оператор должен понимать, что HY2XS его не обслуживает. +- В preflight добавить проверку: + - домен резолвится в IPv4 A-record; + - если есть AAAA, вывести warning, но не включать IPv6; + - public_host/client URL строится только по IPv4-домену/IPv4 endpoint. +- В firewall использовать IPv4-only nftables rules. Не использовать inet table как способ “случайно покрыть IPv6”, если проект IPv4-only. +- Если на сервере IPv6 включен, HY2XS всё равно не должен слушать IPv6. + +Файлы: +- package/templates/hysteria/config.yaml.tpl +- package/templates/nftables/hy2xs.nft.tpl +- orchestrator/src/steps/preflight.ts +- orchestrator/src/steps/config.ts +- orchestrator/src/steps/firewall.ts +- docs/03-server-hysteria2.md +- docs/05-client-and-access-scope.md +- docs/07-systemd-and-firewall.md +- docs/12-operations-and-troubleshooting.md + +3. Исправить Hysteria2 listen/public endpoint split + +Сейчас есть риск, что client URL строится из listen, что неверно. + +Нужно разделить: +- bind_host: 0.0.0.0 +- bind_port: 443 +- public_host: domain or IPv4 +- public_port: 443 + +Hysteria server config использует bind_host/bind_port. +Client URL/subscription использует public_host/public_port. + +Нельзя строить client URL из поля listen. +Нельзя использовать 0.0.0.0 в client URL. + +Проверь и исправь: +- apps/service/hysteria2_api.go +- apps/model/bo/hysteria2.go +- apps/model/bo/hysteria2_api.go +- apps/model/dto/hysteria2.go +- apps/model/vo/hysteria2.go +- все места, где public URL строится через Listen. +- package/templates/hysteria/config.yaml.tpl +- docs. + +Acceptance: +- При listen: 0.0.0.0:443 клиентский URL должен быть hysteria2://password@example.com:443/... +- В URL не должно быть 0.0.0.0. +- При public_port=443 порт можно явно оставить или нормализовать по принятому формату, но поведение должно быть стабильным и протестированным. + +4. Исправить TLS/ACME flow + +Сейчас self-signed сертификат фактически используется как default. Это не production-ready. + +Нужно: +- Добавить TLS modes: + - acme — production default + - file — production для собственного cert/key + - self_signed_dev — только dev/lab, запрещён для non-interactive production без явного флага +- В production default использовать ACME, если задан domain и acme_email. +- Для acme mode Hysteria config должен использовать acme block, а не tls cert/key. +- Для file mode использовать tls.cert/tls.key. +- Для self_signed_dev: + - явно генерировать self-signed cert; + - явно маркировать install как dev/lab; + - client config должен включать insecure только для этого режима. +- Не смешивать acme и tls file в одном config. +- Добавить в env: + - HY2XS_TLS_MODE + - HY2XS_ACME_EMAIL + - HY2XS_TLS_CERT_PATH + - HY2XS_TLS_KEY_PATH +- Проверить, что generated config соответствует официальной схеме Hysteria2. + +Файлы: +- orchestrator/src/steps/config.ts +- package/templates/hysteria/config.yaml.tpl +- package/templates/env/* +- docs/03-server-hysteria2.md +- docs/05-client-and-access-scope.md +- docs/11-testing-and-acceptance.md + +Acceptance: +- acme mode генерирует только acme block. +- file mode генерирует только tls cert/key block. +- self_signed_dev генерирует tls cert/key + client insecure. +- production install без domain/acme_email должен завершиться ошибкой или потребовать явный self_signed_dev. + +5. Исправить TrafficStats API secret + +Сейчас Hysteria config получает HYSTERIA_API_SECRET, но HY2XS admin использует JWT_SECRET для trafficStats API. Это неверно. + +Нужно: +- Ввести отдельную настройку: + HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET +- Один и тот же secret должен использоваться: + - в Hysteria config: + trafficStats.secret + - в HY2XS admin при вызовах: + /traffic + /online + kick user / disconnect if exists +- Не использовать JWT_SECRET для Hysteria API. +- JWT_SECRET должен отвечать только за авторизацию HY2XS admin. +- Добавить smoke-check: + - запрос к trafficStats API с правильным secret проходит; + - запрос с неправильным secret не проходит. +- Обновить docs. + +Файлы: +- apps/service/hysteria2_api.go +- apps/proxy/hysteria2_api.go +- apps/model/constant/config.go +- apps/service/config.go +- orchestrator/src/steps/config.ts +- package/templates/hysteria/config.yaml.tpl +- package/templates/env/* +- docs/03-server-hysteria2.md +- docs/04-admin-panel-h-ui-fork.md +- docs/11-testing-and-acceptance.md + +6. Убрать default admin sysadmin/sysadmin + +Сейчас seed создаёт admin sysadmin/sysadmin. Это production blocker. + +Нужно: +- Убрать дефолтный predictable admin password. +- Во время install генерировать bootstrap admin password. +- Записывать его в root-only файл: + /etc/hy2xs/bootstrap-admin.secret + chmod 600 +- Или выводить один раз в конце установки + сохранять в защищённый файл. +- Admin username должен задаваться через env: + HY2XS_ADMIN_USER +- При первом логине требовать смену пароля: + HY2XS_FORCE_PASSWORD_CHANGE=true +- Не использовать con_pass = admin password. +- con_pass для Hysteria user должен быть отдельным случайным secret. +- Перейти с SHA224 на нормальный password hashing: + - bcrypt или argon2id. +- Сделать backward-compatible migration: + - старые SHA224 пароли распознавать; + - после успешного login перехешировать в новый формат; + - новые пароли сохранять только в новом формате. +- Обновить docs. + +Файлы: +- apps/dao/sqlite.go +- apps/util/encrypt.go +- apps/service/account.go +- apps/model/entity/account.go +- apps/model/dto/account.go +- apps/controller/account.go +- orchestrator/src/commands/install.ts +- orchestrator/src/steps/env.ts +- package/templates/env/* +- docs/04-admin-panel-h-ui-fork.md +- docs/09-post-install-env.md +- docs/11-testing-and-acceptance.md + +Acceptance: +- После fresh install нет sysadmin/sysadmin. +- Login невозможен старым дефолтным паролем. +- Bootstrap password уникален для каждой установки. +- Password hash не является быстрым SHA digest. +- force password change работает. + +7. Исправить UI bind host + +Сейчас CLI имеет --ui-bind-host, systemd передаёт HY2XS_UI_BIND_HOST, но Go server фактически слушает :port на всех интерфейсах. + +Нужно: +- Go server должен читать HY2XS_UI_BIND_HOST. +- Default: 127.0.0.1. +- Addr должен быть: + 127.0.0.1: + или другой явно заданный IPv4. +- Запретить пустой host как bind all. +- Запретить :: и IPv6 bind. +- Если оператор явно задаёт 0.0.0.0, требовать warning/explicit flag, либо запретить в production. +- systemd unit должен передавать HY2XS_UI_BIND_HOST. +- smoke-check должен проверять, что UI не слушает 0.0.0.0, если bind host 127.0.0.1. + +Файлы: +- apps/cmd/server.go +- apps/service/server.go +- package/systemd/hy2xs-admin.service +- orchestrator/src/steps/systemd.ts +- orchestrator/src/steps/smoke.ts +- docs/04-admin-panel-h-ui-fork.md +- docs/07-systemd-and-firewall.md + +Acceptance: +- ss -ltnp показывает 127.0.0.1:, а не 0.0.0.0:. +- UI недоступен с внешнего интерфейса без reverse proxy/tunnel. +- --ui-bind-host реально влияет на bind. + +8. Запускать HY2XS admin не от root + +Сейчас systemd unit не задаёт User/Group, значит admin работает от root. + +Нужно: +- Создать системного пользователя: + hy2xs-admin +- Запускать HY2XS admin от User=hy2xs-admin Group=hy2xs-admin. +- Настроить директории: + - /opt/hy2xs-admin read-only для binary/assets + - /var/lib/hy2xs-admin writable data + - /var/log/hy2xs writable logs +- В systemd добавить hardening: + - NoNewPrivileges=true + - PrivateTmp=true + - ProtectHome=true + - ProtectSystem=strict + - ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs + - RestrictAddressFamilies=AF_INET AF_UNIX + - CapabilityBoundingSet= empty where possible + - AmbientCapabilities= empty where possible +- Если UI должен перезапускать Hysteria или читать privileged files, не запускать весь UI от root. + Вместо этого: + - либо убрать такие операции из UI; + - либо сделать отдельный узкий root helper; + - либо использовать строго ограниченный sudoers/polkit rule. +- Не оставлять широкие root-права. + +Файлы: +- package/systemd/hy2xs-admin.service +- orchestrator/src/steps/filesystem.ts +- orchestrator/src/steps/systemd.ts +- apps/proxy/process.go +- apps/service/hysteria2.go +- apps/service/config.go +- docs/04-admin-panel-h-ui-fork.md +- docs/07-systemd-and-firewall.md + +Acceptance: +- systemctl show hy2xs-admin показывает User=hy2xs-admin. +- Процесс не root. +- UI продолжает работать. +- Нет записи в /etc/hysteria/config.yaml от имени неподходящего процесса без контролируемого helper/reconfigure. + +9. Исправить права /etc/hysteria/config.yaml + +Сейчас orchestrator пишет config chmod 0600, но UI может перезаписать его chmod 0644. + +Нужно: +- Всегда писать /etc/hysteria/config.yaml с правами 0600. +- owner должен быть корректным для Hysteria service. +- Если UI больше не должен напрямую писать config, убрать такую возможность или перенести в orchestrator reconfigure. +- Писать config atomic: + - temporary file + - fsync if needed + - chmod/chown + - rename + - validate + - restart/reload +- Не допускать world-readable secrets. + +Файлы: +- orchestrator/src/steps/config.ts +- apps/service/hysteria2.go +- apps/service/config.go +- docs/03-server-hysteria2.md +- docs/07-systemd-and-firewall.md + +Acceptance: +- stat -c "%a %U %G" /etc/hysteria/config.yaml показывает 600 и ожидаемого owner/group. +- В config есть secrets, но файл не world-readable. +- UI не ослабляет права после сохранения. + +10. Переделать firewall apply на безопасный staged mode + +Сейчас firewall может резко применить policy drop и отрезать SSH. + +Нужно: +- Не перезаписывать слепо весь /etc/nftables.conf без staged rollback. +- Использовать include-файл, например: + /etc/nftables.d/hy2xs.nft +- Перед apply выполнять: + nft -c -f +- Сделать staged apply: + - backup current rules/config + - install rollback timer через systemd-run или эквивалент + - apply candidate + - проверить SSH port rule + - проверить Hysteria UDP port + - проверить UI local bind + - если checks passed — cancel rollback + - если failed — rollback +- IPv4-only: + - table ip hy2xs + - не использовать IPv6 rules как supported path. +- Allow: + - lo + - established/related + - SSH TCP port from IPv4 + - Hysteria UDP port from IPv4 + - optionally ICMP + - UI TCP только с 127.0.0.1 +- Default drop только после staged safety. +- Если --skip-firewall, не открывать UI публично. + +Файлы: +- orchestrator/src/steps/firewall.ts +- package/templates/nftables/hy2xs.nft.tpl +- docs/07-systemd-and-firewall.md +- docs/11-testing-and-acceptance.md + +Acceptance: +- nft -c проходит. +- Неправильный SSH port не применяет irreversible lockout. +- UI не доступен с внешнего интерфейса. +- Hysteria UDP port открыт. +- IPv6 не считается поддерживаемым. + +11. Удалить port hopping legacy + +Docs говорят no port hopping, но code всё ещё инициализирует: +- service.InitTableAndChain() +- service.InitPortHopping() + +Нужно: +- Убрать port hopping из production path. +- Удалить или отключить: + - apps/service/forward.go + - связанные nft/iptables вызовы + - связанные routes/UI элементы + - связанные config fields +- Если удалить полностью нельзя за один pass, закрыть build tag’ом или runtime flag default false, но production binary не должен инициализировать port hopping. +- Не создавать nft table/chain при старте HY2XS admin. +- Обновить docs. + +Файлы: +- apps/cmd/server.go +- apps/service/forward.go +- apps/router/* +- apps/frontend/src/views/* +- docs/04-admin-panel-h-ui-fork.md +- docs/10-access-layer-out-of-scope.md + +Acceptance: +- grep по InitPortHopping не показывает production вызов. +- При старте UI не создаёт nft/iptables rules. +- В UI нет port hopping управления. + +12. Убрать legacy updater/GitHub logic из UI + +HY2XS admin не должен быть updater’ом h-ui/original repo. + +Нужно: +- Найти и удалить legacy code, связанный с: + - original h-ui repo + - GitHub releases + - self-update UI + - update checker + - install/update Hysteria из UI, если это зона orchestrator +- В Go dependency audit: + - github.com/google/go-github/v39 сейчас используется только apps/util/github.go. + - Если self-update/GitHub release logic удаляется, удалить util/github.go и go-github из go.mod/go.sum. +- Не оставлять неиспользуемые функции “на потом”. + +Файлы: +- apps/util/github.go +- apps/go.mod +- apps/go.sum +- apps/controller/config.go +- apps/service/config.go +- frontend config/settings views +- docs/04-admin-panel-h-ui-fork.md +- docs/10-access-layer-out-of-scope.md + +Acceptance: +- go mod tidy удаляет google/go-github, если больше не нужен. +- UI не показывает updater legacy. +- Docs говорят, что update/upgrade — зона orchestrator/package, не admin panel. + +13. Проверить и почистить frontend dependencies + +В apps/frontend/package.json есть вероятные рудименты. + +Проверь фактическое использование и удали неиспользуемое: +- echarts — в текущем коде не найдено использование. +- screenfull — в текущем коде не найдено использование. +- path-to-regexp — в текущем коде не найдено использование. +- fast-glob — в vite.config.ts не используется. +- @types/path-browserify — нужен только если path-browserify остаётся. +- vite-plugin-dynamic-base — используется в vite.config.ts, но проверить, нужен ли HY2XS dynamic base. Если UI всегда от root path, убрать. +- unocss — используется в vite config, но проверить, реально ли есть UnoCSS classes. Если нет — убрать. +- unplugin-icons / @iconify-json/ep — оставить только если реально нужны auto icons. +- vuedraggable — используется в ImputMultiple, оставить если компонент нужен. +- path-browserify — используется в Sidebar/TagsView, оставить или заменить на простую path join utility без browser polyfill. +- @vueuse/core — используется, оставить или заменить точечно, если хочется уменьшить bundle. +- nprogress — используется, оставить. +- copy-to-clipboard — используется, оставить или заменить navigator.clipboard, если поддержка позволяет. + +После удаления: +- обновить pnpm-lock.yaml; +- убедиться, что frontend build проходит; +- удалить сгенерированные auto-import/component d.ts, если они не должны быть в repo, либо обновить их корректно; +- не оставлять импортов на удалённые пакеты. + +Файлы: +- apps/frontend/package.json +- apps/frontend/pnpm-lock.yaml +- apps/frontend/vite.config.ts +- apps/frontend/src/** +- docs/04-admin-panel-h-ui-fork.md + +Acceptance: +- pnpm install --frozen-lockfile проходит. +- pnpm build:prod проходит. +- grep не находит удалённые пакеты в src/vite config. +- UI работает без legacy dependencies. + +14. Исправить Hysteria install flow + +Сейчас installer использует official script через curl/sh и не pin’ит версию. + +Нужно: +- Добавить возможность pin версии: + HY2XS_HYSTERIA_VERSION=vX.Y.Z или latest. +- Если latest — явно записать фактически установленную версию в post-install snapshot. +- Использовать official installer корректно. +- Если используется curl | bash/script, сделать безопаснее: + - скачать script в temp file; + - проверить download success; + - по возможности checksum/signature/version; + - запускать bash, если official docs требуют bash. +- После установки проверить: + - binary exists; + - hysteria version; + - expected service conflict отсутствует или осознанно заменён. +- Если official installer создаёт свой service, HY2XS должен явно документировать и контролировать replacement. + +Файлы: +- orchestrator/src/steps/hysteria.ts +- orchestrator/src/steps/smoke.ts +- docs/02-build-layer-and-package.md +- docs/03-server-hysteria2.md +- docs/08-orchestrator-spec.md + +15. Улучшить smoke/acceptance checks + +Текущий smoke слишком слабый. + +Добавить проверки: +- systemd units active: + - hysteria-server + - hy2xs-admin +- Hysteria UDP port слушает IPv4. +- UI слушает только expected IPv4 bind host. +- UI не слушает 0.0.0.0 при bind 127.0.0.1. +- Auth endpoint отвечает корректно: + - /hui/hysteria2/auth для тестового/существующего пользователя. +- TrafficStats API: + - correct secret works; + - wrong secret denied. +- Hysteria config syntactically valid. +- TLS mode соответствует env: + - acme mode содержит acme и не содержит tls cert/key. + - file mode содержит tls cert/key и не содержит acme. + - self_signed_dev clearly marked. +- Firewall rules installed and nft -c passes. +- File permissions: + - /etc/hy2xs/hy2xs.env 0600 + - /etc/hysteria/post-install.env 0600 + - /etc/hysteria/config.yaml 0600 + - bootstrap-admin.secret 0600 +- No default sysadmin/sysadmin. +- No IPv6 listen: + - ss output must not show [::] for HY2XS/Hysteria. +- No port hopping init. +- Reboot persistence documented or testable. + +Файлы: +- orchestrator/src/steps/smoke.ts +- docs/11-testing-and-acceptance.md +- package/install.sh + +16. Обновить docs и убрать рассинхрон + +Обновить: +- README.md +- docs/01-architecture-baseline.md +- docs/02-build-layer-and-package.md +- docs/03-server-hysteria2.md +- docs/04-admin-panel-h-ui-fork.md +- docs/05-client-and-access-scope.md +- docs/06-speed-limits-and-congestion.md +- docs/07-systemd-and-firewall.md +- docs/08-orchestrator-spec.md +- docs/09-post-install-env.md +- docs/10-access-layer-out-of-scope.md +- docs/11-testing-and-acceptance.md +- docs/12-operations-and-troubleshooting.md +- hy2xs_implementation_plan-no_git/*.txt если они остаются как актуальные документы. + +Docs должны фиксировать: +- HY2XS = Bun/TypeScript orchestrator + bundled HY2XS admin + upstream Hysteria2. +- No Telegram bot. +- No port hopping. +- No target-side build. +- No IPv6 support. +- IPv4-only bind/listen. +- Production TLS default = ACME. +- self-signed only dev/lab. +- env/config layer and reconfigure flow. +- admin panel не updater и не generic server manager. +- admin panel не работает от root. +- firewall staged apply. +- secret/file permission policy. +- acceptance checks. + +Также исправить README: +- Убедиться, что build command указывает реальный путь. +- Если фактический build script tools/build/build.sh, README не должен ссылаться на builder/build.sh. +- Все команды должны соответствовать текущей структуре. + +17. Общие требования к качеству + +После правок выполнить: +- gofmt по Go-коду. +- go mod tidy. +- frontend lockfile update. +- TypeScript check для orchestrator. +- Build orchestrator. +- Build frontend. +- Build Go admin. +- Smoke если возможно в среде. +- grep-проверки: + - нет sysadmin/sysadmin как production seed. + - нет InitPortHopping production вызова. + - нет listen: :{{HYSTERIA_PORT}}. + - нет UI bind ":8080" без host. + - нет JWT_SECRET как Hysteria trafficStats secret. + - нет chmod 0644 для /etc/hysteria/config.yaml. + - нет IPv6 listen [::] / "::" в runtime defaults. + - нет h-ui legacy updater/original repo references, если они вне scope. + +Не делай: +- Не оставляй TODO вместо исправления P0. +- Не добавляй reverse proxy как обязательную часть. +- Не добавляй Docker requirement. +- Не добавляй Telegram/bot features. +- Не добавляй IPv6. +- Не оставляй root admin panel. +- Не делай “если не получилось, chmod 777”. +- Не ослабляй firewall ради прохождения smoke. +- Не используй JWT_SECRET для Hysteria API. +- Не используй 0.0.0.0 в client URL. +- Не делай self-signed production default. + +Ожидаемый результат: +1. Полный diff с исправлением production blockers. +2. Обновлённые docs. +3. Обновлённые templates/examples. +4. Удалённые legacy dependencies/code. +5. Понятный reconfigure/env flow. +6. IPv4-only модель зафиксирована в коде и docs. +7. Расширенные smoke/acceptance checks. +8. Краткий отчёт: + - что исправлено; + - какие файлы изменены; + - какие legacy части удалены; + - какие команды проверки запускались; + - что осталось как tech debt, если что-то невозможно закрыть за один pass. \ No newline at end of file diff --git a/orchestrator/src/cli.ts b/orchestrator/src/cli.ts index 854d3ab..ecaed26 100644 --- a/orchestrator/src/cli.ts +++ b/orchestrator/src/cli.ts @@ -1,8 +1,11 @@ import { install } from "./commands/install"; -import type { InstallOptions } from "./types/context"; +import { reconfigure } from "./commands/reconfigure"; +import type { InstallOptions, ReconfigureOptions } from "./types/context"; function usage(): never { - console.error("Usage: hy2xs-orchestrator install --package-dir [--domain ] [--port ] [--ssh-port ] [--skip-firewall] [--skip-start] [--ui-port ] [--ui-bind-host ] [--non-interactive]"); + console.error("Usage:"); + console.error(" hy2xs-orchestrator install --package-dir [--config ] [--skip-firewall] [--skip-start] [--non-interactive]"); + console.error(" hy2xs-orchestrator reconfigure --package-dir [--config ] [--dry-run|--apply] [--skip-firewall] [--skip-start]"); process.exit(2); } @@ -18,14 +21,10 @@ function takeValue(args: string[], index: number, flag: string): string { function parseInstallOptions(args: string[]): InstallOptions { const options: InstallOptions = { packageDir: "", + configPath: "/etc/hy2xs/hy2xs.env", nonInteractive: false, - domain: "", - port: 443, - sshPort: 22, skipFirewall: false, - skipStart: false, - uiPort: 8080, - uiBindHost: "127.0.0.1" + skipStart: false }; for (let i = 0; i < args.length; i += 1) { @@ -38,30 +37,14 @@ function parseInstallOptions(args: string[]): InstallOptions { case "--non-interactive": options.nonInteractive = true; break; - case "--domain": - options.domain = takeValue(args, i, arg); - i += 1; - break; - case "--port": - options.port = Number(takeValue(args, i, arg)); - i += 1; - break; - case "--ssh-port": - options.sshPort = Number(takeValue(args, i, arg)); - i += 1; - break; case "--skip-firewall": options.skipFirewall = true; break; case "--skip-start": options.skipStart = true; break; - case "--ui-port": - options.uiPort = Number(takeValue(args, i, arg)); - i += 1; - break; - case "--ui-bind-host": - options.uiBindHost = takeValue(args, i, arg); + case "--config": + options.configPath = takeValue(args, i, arg); i += 1; break; default: @@ -75,23 +58,73 @@ function parseInstallOptions(args: string[]): InstallOptions { usage(); } - for (const [name, value] of Object.entries({ port: options.port, sshPort: options.sshPort, uiPort: options.uiPort })) { - if (!Number.isInteger(value) || value < 1 || value > 65535) { - console.error(`Invalid ${name}: ${value}`); - usage(); + return options; +} + +function parseReconfigureOptions(args: string[]): ReconfigureOptions { + const options: ReconfigureOptions = { + packageDir: "", + configPath: "/etc/hy2xs/hy2xs.env", + nonInteractive: false, + dryRun: false, + apply: false, + skipFirewall: false, + skipStart: false + }; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + switch (arg) { + case "--package-dir": + options.packageDir = takeValue(args, i, arg); + i += 1; + break; + case "--config": + options.configPath = takeValue(args, i, arg); + i += 1; + break; + case "--dry-run": + options.dryRun = true; + break; + case "--apply": + options.apply = true; + break; + case "--skip-firewall": + options.skipFirewall = true; + break; + case "--skip-start": + options.skipStart = true; + break; + default: + console.error(`Unknown argument: ${arg}`); + usage(); } } + if (!options.packageDir) { + console.error("Missing --package-dir"); + usage(); + } + + if (options.dryRun === options.apply) { + console.error("Specify exactly one of --dry-run or --apply"); + usage(); + } + return options; } async function main(): Promise { const [command, ...args] = Bun.argv.slice(2); - if (command !== "install") { - usage(); + if (command === "install") { + await install(parseInstallOptions(args)); + return; } - - await install(parseInstallOptions(args)); + if (command === "reconfigure") { + await reconfigure(parseReconfigureOptions(args)); + return; + } + usage(); } main().catch((error: unknown) => { diff --git a/orchestrator/src/commands/install.ts b/orchestrator/src/commands/install.ts index 554079f..665acb3 100644 --- a/orchestrator/src/commands/install.ts +++ b/orchestrator/src/commands/install.ts @@ -1,7 +1,9 @@ import { randomBytes } from "node:crypto"; import type { InstallContext, InstallOptions } from "../types/context"; -import { readText } from "../lib/fs"; +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 { preflight } from "../steps/preflight"; import { installDeps } from "../steps/deps"; import { prepareFilesystem } from "../steps/filesystem"; @@ -26,15 +28,16 @@ function secret(): string { } export async function install(options: InstallOptions): Promise { + const hasConfig = await exists(options.configPath); + const config = hasConfig ? parseRuntimeEnv(await readText(options.configPath)) : defaultRuntimeConfig(); + const context: InstallContext = { options, + config, packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), hysteriaAuthPassword: secret(), - hysteriaObfsPassword: secret(), - hysteriaApiSecret: secret(), - hysteriaApiPort: 36712, hysteriaVersion: "unknown" }; @@ -44,6 +47,11 @@ export async function install(options: InstallOptions): Promise { await installDeps(context); step("filesystem"); await prepareFilesystem(context); + if (!hasConfig) { + step("write runtime env"); + await runVisible`mkdir -p /etc/hy2xs`; + await writeText(options.configPath, renderRuntimeEnv(config), 0o600); + } step("bundled UI"); await deployUi(context); step("Hysteria2 upstream install"); diff --git a/orchestrator/src/commands/reconfigure.ts b/orchestrator/src/commands/reconfigure.ts new file mode 100644 index 0000000..b443629 --- /dev/null +++ b/orchestrator/src/commands/reconfigure.ts @@ -0,0 +1,76 @@ +import type { ReconfigureContext, ReconfigureOptions } from "../types/context"; +import { readText, writeText } from "../lib/fs"; +import { info, step } from "../lib/log"; +import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env"; +import { preflight } from "../steps/preflight"; +import { generateConfig } from "../steps/config"; +import { deploySystemd } from "../steps/systemd"; +import { applyFirewall } from "../steps/firewall"; +import { writePostInstallEnv } from "../steps/env"; +import { smoke } from "../steps/smoke"; +import { runVisible } from "../lib/process"; + +async function backupCurrentState(): Promise { + await runVisible`mkdir -p /etc/hy2xs/backups`; + await runVisible`cp -a /etc/hysteria/config.yaml /etc/hy2xs/backups/config.yaml.bak 2>/dev/null || true`; + await runVisible`cp -a /etc/systemd/system/hy2xs-admin.service /etc/hy2xs/backups/hy2xs-admin.service.bak 2>/dev/null || true`; + await runVisible`cp -a /etc/systemd/system/hysteria-server.service /etc/hy2xs/backups/hysteria-server.service.bak 2>/dev/null || true`; + await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/hy2xs/backups/hy2xs.nft.bak 2>/dev/null || true`; +} + +async function rollbackCurrentState(): Promise { + await runVisible`cp -a /etc/hy2xs/backups/config.yaml.bak /etc/hysteria/config.yaml 2>/dev/null || true`; + await runVisible`cp -a /etc/hy2xs/backups/hy2xs-admin.service.bak /etc/systemd/system/hy2xs-admin.service 2>/dev/null || true`; + await runVisible`cp -a /etc/hy2xs/backups/hysteria-server.service.bak /etc/systemd/system/hysteria-server.service 2>/dev/null || true`; + await runVisible`cp -a /etc/hy2xs/backups/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true`; + await runVisible`systemctl daemon-reload`; + await runVisible`systemctl restart hysteria-server hy2xs-admin || true`; +} + +export async function reconfigure(options: ReconfigureOptions): Promise { + const configRaw = await readText(options.configPath); + const config = parseRuntimeEnv(configRaw); + + const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = { + options, + config, + packageVersion: "reconfigure", + packageBuildId: "reconfigure", + installDate: new Date().toISOString(), + hysteriaAuthPassword: "managed-by-ui-auth", + hysteriaVersion: "unknown" + }; + + step("preflight"); + await preflight(context); + + if (options.dryRun) { + info("reconfigure dry-run: validated config and execution graph"); + info(`config file: ${options.configPath}`); + info(`ui bind: ${config.uiBindHost}:${config.uiPort}`); + info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`); + info(`public endpoint: ${config.publicHost}:${config.publicPort}`); + return; + } + + step("backup"); + await backupCurrentState(); + + try { + step("config generation"); + await generateConfig(context); + step("systemd units"); + await deploySystemd(context); + step("firewall"); + await applyFirewall(context); + step("write env artifacts"); + await writeText(options.configPath, renderRuntimeEnv(config), 0o600); + await writePostInstallEnv(context); + step("smoke checks"); + await smoke(context); + } catch (error) { + info("reconfigure failed, rollback in progress"); + await rollbackCurrentState(); + throw error; + } +} diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts new file mode 100644 index 0000000..53f0a9a --- /dev/null +++ b/orchestrator/src/config/env.ts @@ -0,0 +1,253 @@ +import { randomBytes } from "node:crypto"; +import type { RuntimeConfig, TlsMode } from "../types/context"; + +type EnvMap = Record; + +function randomSecret(): string { + return randomBytes(24).toString("base64url"); +} + +function parseEnv(content: string): EnvMap { + const result: EnvMap = {}; + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + const separator = line.indexOf("="); + if (separator < 1) { + throw new Error(`invalid env line: ${rawLine}`); + } + const key = line.slice(0, separator).trim(); + const value = line.slice(separator + 1).trim(); + result[key] = value; + } + return result; +} + +function parsePort(name: string, raw: string, fallback: number): number { + const value = raw ? Number(raw) : fallback; + if (!Number.isInteger(value) || value < 1 || value > 65535) { + throw new Error(`invalid ${name}: ${raw}`); + } + return value; +} + +function parseBool(name: string, raw: string, fallback: boolean): boolean { + if (!raw) { + return fallback; + } + if (raw === "true") { + return true; + } + if (raw === "false") { + return false; + } + throw new Error(`invalid ${name}: ${raw}`); +} + +function requireValue(name: string, value: string): string { + if (!value || !value.trim()) { + throw new Error(`missing required ${name}`); + } + return value.trim(); +} + +function normalizeIpv4Host(name: string, value: string): string { + if (value === "0.0.0.0" || value === "127.0.0.1") { + return value; + } + if (/^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(value)) { + return value; + } + if (value.includes(":")) { + throw new Error(`${name} must be IPv4-only`); + } + throw new Error(`invalid ${name}: ${value}`); +} + +function normalizePublicHost(value: string): string { + if (!value) { + throw new Error("missing required HY2XS_PUBLIC_HOST"); + } + if (value.includes(":")) { + throw new Error("HY2XS_PUBLIC_HOST must not contain IPv6"); + } + return value; +} + +function normalizeTlsMode(value: string): TlsMode { + if (value === "acme" || value === "file" || value === "self_signed_dev") { + return value; + } + throw new Error(`invalid HY2XS_TLS_MODE: ${value}`); +} + +export function parseRuntimeEnv(content: string): RuntimeConfig { + const env = parseEnv(content); + + const uiPort = parsePort("HY2XS_UI_PORT", env.HY2XS_UI_PORT, 8080); + const uiBindHost = normalizeIpv4Host("HY2XS_UI_BIND_HOST", env.HY2XS_UI_BIND_HOST || "127.0.0.1"); + 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 config: RuntimeConfig = { + domain: env.HY2XS_DOMAIN || "", + publicHost: normalizePublicHost(env.HY2XS_PUBLIC_HOST || env.HY2XS_DOMAIN || ""), + publicPort: parsePort("HY2XS_PUBLIC_PORT", env.HY2XS_PUBLIC_PORT, hysteriaPort), + ipv6Enabled: false, + sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22), + firewallEnabled: parseBool("HY2XS_FIREWALL_ENABLED", env.HY2XS_FIREWALL_ENABLED, true), + firewallStagedApply: parseBool("HY2XS_FIREWALL_STAGED_APPLY", env.HY2XS_FIREWALL_STAGED_APPLY, true), + uiBindHost, + uiPort, + adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"), + adminInitialPassword: env.HY2XS_ADMIN_INITIAL_PASSWORD || randomSecret(), + forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true), + tlsMode, + 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", + hysteriaBindHost: normalizeIpv4Host("HY2XS_HYSTERIA_BIND_HOST", env.HY2XS_HYSTERIA_BIND_HOST || "0.0.0.0"), + hysteriaPort, + hysteriaAuthMode: "http", + hysteriaAuthUrl: env.HY2XS_HYSTERIA_AUTH_URL || `http://127.0.0.1:${uiPort}/hui/hysteria2/auth`, + hysteriaTrafficStatsHost: normalizeIpv4Host( + "HY2XS_HYSTERIA_TRAFFIC_STATS_HOST", + env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1" + ), + hysteriaTrafficStatsPort: trafficStatsPort, + hysteriaTrafficStatsSecret: env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET || randomSecret(), + hysteriaObfsType: "salamander", + hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", env.HY2XS_HYSTERIA_OBFS_PASSWORD || randomSecret()), + hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps", + hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps", + hysteriaIgnoreClientBandwidth: parseBool( + "HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH", + env.HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH, + false + ), + hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml", + hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "latest", + installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin", + dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin", + logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs", + bootstrapAdminSecretPath: "/etc/hy2xs/bootstrap-admin.secret" + }; + + validateRuntimeConfig(config); + 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"); + } + if (config.uiBindHost.includes(":")) { + throw new Error("HY2XS_UI_BIND_HOST must be IPv4-only"); + } + if (config.hysteriaBindHost.includes(":")) { + throw new Error("HY2XS_HYSTERIA_BIND_HOST must be IPv4-only"); + } + if (config.hysteriaBindHost !== "0.0.0.0") { + throw new Error("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 in production profile"); + } + + if (config.tlsMode === "acme") { + if (!config.domain) { + throw new Error("HY2XS_DOMAIN is required for TLS mode acme"); + } + if (!config.acmeEmail) { + throw new Error("HY2XS_ACME_EMAIL is required for TLS mode acme"); + } + } + if (config.tlsMode === "file") { + requireValue("HY2XS_TLS_CERT_PATH", config.tlsCertPath); + requireValue("HY2XS_TLS_KEY_PATH", config.tlsKeyPath); + } + + if (config.publicHost === "0.0.0.0") { + throw new Error("HY2XS_PUBLIC_HOST must be a routable domain or IPv4, not 0.0.0.0"); + } +} + +export function renderRuntimeEnv(config: RuntimeConfig): string { + const lines = [ + "# HY2XS runtime config (editable)", + "HY2XS_IPV6_ENABLED=false", + `HY2XS_DOMAIN=${config.domain}`, + `HY2XS_PUBLIC_HOST=${config.publicHost}`, + `HY2XS_PUBLIC_PORT=${config.publicPort}`, + `HY2XS_SSH_PORT=${config.sshPort}`, + `HY2XS_FIREWALL_ENABLED=${config.firewallEnabled}`, + `HY2XS_FIREWALL_STAGED_APPLY=${config.firewallStagedApply}`, + `HY2XS_UI_BIND_HOST=${config.uiBindHost}`, + `HY2XS_UI_PORT=${config.uiPort}`, + `HY2XS_ADMIN_USER=${config.adminUser}`, + `HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`, + `HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`, + `HY2XS_TLS_MODE=${config.tlsMode}`, + `HY2XS_ACME_EMAIL=${config.acmeEmail}`, + `HY2XS_TLS_CERT_PATH=${config.tlsCertPath}`, + `HY2XS_TLS_KEY_PATH=${config.tlsKeyPath}`, + `HY2XS_HYSTERIA_BIND_HOST=${config.hysteriaBindHost}`, + `HY2XS_HYSTERIA_PORT=${config.hysteriaPort}`, + "HY2XS_HYSTERIA_AUTH_MODE=http", + `HY2XS_HYSTERIA_AUTH_URL=${config.hysteriaAuthUrl}`, + `HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=${config.hysteriaTrafficStatsHost}`, + `HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=${config.hysteriaTrafficStatsPort}`, + `HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${config.hysteriaTrafficStatsSecret}`, + "HY2XS_HYSTERIA_OBFS_TYPE=salamander", + `HY2XS_HYSTERIA_OBFS_PASSWORD=${config.hysteriaObfsPassword}`, + `HY2XS_HYSTERIA_BANDWIDTH_UP=${config.hysteriaBandwidthUp}`, + `HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`, + `HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`, + `HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`, + `HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`, + `HY2XS_INSTALL_DIR=${config.installDir}`, + `HY2XS_DATA_DIR=${config.dataDir}`, + `HY2XS_LOG_DIR=${config.logDir}` + ]; + return `${lines.join("\n")}\n`; +} diff --git a/orchestrator/src/steps/config.ts b/orchestrator/src/steps/config.ts index b05b7c3..c2c9153 100644 --- a/orchestrator/src/steps/config.ts +++ b/orchestrator/src/steps/config.ts @@ -3,19 +3,40 @@ import { readText, renderTemplate, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; export async function generateConfig(context: InstallContext): Promise { + const tlsAcmeBlock = context.config.tlsMode === "acme" + ? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0` + : ""; + const tlsFileBlock = context.config.tlsMode === "file" || context.config.tlsMode === "self_signed_dev" + ? `tls:\n cert: ${context.config.tlsCertPath}\n key: ${context.config.tlsKeyPath}` + : ""; + const template = await readText(`${context.options.packageDir}/templates/hysteria/config.yaml.tpl`); const rendered = renderTemplate(template, { - HYSTERIA_PORT: context.options.port, + HYSTERIA_BIND_HOST: context.config.hysteriaBindHost, + HYSTERIA_PORT: context.config.hysteriaPort, HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword, - HYSTERIA_OBFS_PASSWORD: context.hysteriaObfsPassword, - HYSTERIA_API_PORT: context.hysteriaApiPort, - HYSTERIA_API_SECRET: context.hysteriaApiSecret, - UI_PORT: context.options.uiPort, - BANDWIDTH_UP: "50 mbps", - BANDWIDTH_DOWN: "50 mbps" + HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword, + HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost, + HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort, + HYSTERIA_API_SECRET: context.config.hysteriaTrafficStatsSecret, + UI_PORT: context.config.uiPort, + BANDWIDTH_UP: context.config.hysteriaBandwidthUp, + BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown, + TLS_ACME_BLOCK: tlsAcmeBlock, + TLS_FILE_BLOCK: tlsFileBlock, + AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false" }); - await writeText("/etc/hysteria/config.yaml", rendered, 0o600); - await runVisible`openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=${context.options.domain || "hy2xs.local"} -keyout /etc/hysteria/server.key -out /etc/hysteria/server.crt`; - await runVisible`chown hysteria:hysteria /etc/hysteria/config.yaml /etc/hysteria/server.key /etc/hysteria/server.crt`; + await writeText("/etc/hysteria/config.yaml.tmp", rendered, 0o600); + await runVisible`mv /etc/hysteria/config.yaml.tmp /etc/hysteria/config.yaml`; + + if (context.config.tlsMode === "self_signed_dev") { + await runVisible`openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=${context.config.domain || "hy2xs.local"} -keyout ${context.config.tlsKeyPath} -out ${context.config.tlsCertPath}`; + await runVisible`chmod 600 ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`; + } + + await runVisible`chown hysteria:hysteria /etc/hysteria/config.yaml`; + if (context.config.tlsMode !== "acme") { + await runVisible`chown hysteria:hysteria ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`; + } } diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index 8512e0f..979b94f 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -6,15 +6,29 @@ export async function writePostInstallEnv(context: InstallContext): Promise { +export async function prepareFilesystem(context: InstallContext): Promise { await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`; - await runVisible`mkdir -p /etc/hysteria /var/lib/hysteria /opt/hy2xs-admin /var/lib/hy2xs-admin /var/log/hy2xs /usr/local/lib/hy2xs`; + await runVisible`id -u hy2xs-admin >/dev/null 2>&1 || useradd --system --home ${context.config.dataDir} --shell /usr/sbin/nologin hy2xs-admin`; + await runVisible`mkdir -p /etc/hy2xs /etc/hysteria /var/lib/hysteria ${context.config.installDir} ${context.config.dataDir} ${context.config.logDir} /usr/local/lib/hy2xs /etc/nftables.d`; await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`; - await runVisible`chown -R root:root /var/lib/hy2xs-admin`; + await runVisible`chown -R root:root ${context.config.installDir}`; + await runVisible`chmod -R go-w ${context.config.installDir}`; + await runVisible`chown -R hy2xs-admin:hy2xs-admin ${context.config.dataDir} ${context.config.logDir}`; + await runVisible`chmod 700 /etc/hy2xs`; } diff --git a/orchestrator/src/steps/firewall.ts b/orchestrator/src/steps/firewall.ts index d3acaf7..e2d6d48 100644 --- a/orchestrator/src/steps/firewall.ts +++ b/orchestrator/src/steps/firewall.ts @@ -4,19 +4,36 @@ import { info } from "../lib/log"; import { runVisible } from "../lib/process"; export async function applyFirewall(context: InstallContext): Promise { - if (context.options.skipFirewall) { + if (context.options.skipFirewall || !context.config.firewallEnabled) { info("firewall skipped by flag"); return; } const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), { - SSH_PORT: context.options.sshPort, - HYSTERIA_PORT: context.options.port, - UI_PORT: context.options.uiPort + SSH_PORT: context.config.sshPort, + HYSTERIA_PORT: context.config.hysteriaPort, + UI_PORT: context.config.uiPort, + UI_BIND_HOST: context.config.uiBindHost }); - await runVisible`cp -a /etc/nftables.conf /etc/nftables.conf.hy2xs.bak 2>/dev/null || true`; - await writeText("/etc/nftables.conf", rendered, 0o644); + await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`; + await writeText("/etc/nftables.d/hy2xs.nft.candidate", rendered, 0o600); + await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`; + + if (context.config.firewallStagedApply) { + await runVisible`systemd-run --unit hy2xs-fw-rollback --on-active=45s /bin/sh -c 'cp -a /etc/nftables.d/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; nft -f /etc/nftables.conf >/dev/null 2>&1 || true'`; + } + + await runVisible`mv /etc/nftables.d/hy2xs.nft.candidate /etc/nftables.d/hy2xs.nft`; + await runVisible`grep -q 'include "/etc/nftables.d/hy2xs.nft"' /etc/nftables.conf || printf '\ninclude "/etc/nftables.d/hy2xs.nft"\n' >> /etc/nftables.conf`; await runVisible`nft -f /etc/nftables.conf`; 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`; + await runVisible`systemctl reset-failed hy2xs-fw-rollback || true`; + } } diff --git a/orchestrator/src/steps/preflight.ts b/orchestrator/src/steps/preflight.ts index 50696cb..cd503a7 100644 --- a/orchestrator/src/steps/preflight.ts +++ b/orchestrator/src/steps/preflight.ts @@ -1,6 +1,6 @@ import type { InstallContext } from "../types/context"; import { exists, readText } from "../lib/fs"; -import { fail } from "../lib/log"; +import { fail, info } from "../lib/log"; import { run } from "../lib/process"; async function isPortBusy(port: number): Promise { @@ -13,6 +13,8 @@ async function isPortBusy(port: number): Promise { } export async function preflight(context: InstallContext): Promise { + const isReconfigure = context.packageVersion === "reconfigure"; + if (process.getuid?.() !== 0) { fail("installer must run as root"); } @@ -26,28 +28,59 @@ export async function preflight(context: InstallContext): Promise { fail("bundled HY2XS admin is missing from install package"); } - if (await exists("/etc/hysteria/post-install.env")) { + if (!isReconfigure && (await exists("/etc/hysteria/post-install.env"))) { fail("existing HY2XS post-install.env found; update/repair is out of scope"); } - if (await exists("/opt/hy2xs-admin")) { + if (!isReconfigure && (await exists(context.config.installDir))) { fail("existing /opt/hy2xs-admin found; conflicting old state"); } - const ports = new Set([context.options.port, context.options.uiPort]); + const ports = new Set([context.config.hysteriaPort, context.config.uiPort]); if (ports.size !== 2) { fail("Hysteria port and UI port must be different"); } - if (context.options.domain && !/^[a-zA-Z0-9.-]+$/.test(context.options.domain)) { + if (context.config.domain && !/^[a-zA-Z0-9.-]+$/.test(context.config.domain)) { fail("domain contains unsupported characters"); } - if (await isPortBusy(context.options.port)) { - fail(`Hysteria UDP/TCP port already appears to be in use: ${context.options.port}`); + if (context.config.uiBindHost.includes(":")) { + fail("HY2XS UI bind host must be IPv4-only"); } - if (await isPortBusy(context.options.uiPort)) { - fail(`HY2XS admin port already appears to be in use: ${context.options.uiPort}`); + if (context.config.hysteriaBindHost !== "0.0.0.0") { + fail("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 for production profile"); + } + + if (context.config.tlsMode === "acme" && (!context.config.domain || !context.config.acmeEmail)) { + fail("acme mode requires HY2XS_DOMAIN and HY2XS_ACME_EMAIL"); + } + + if (context.config.domain) { + try { + const a = await run`getent ahostsv4 ${context.config.domain}`; + if (!a.trim()) { + fail(`domain has no A-record: ${context.config.domain}`); + } + } catch { + fail(`domain has no A-record: ${context.config.domain}`); + } + try { + const aaaa = await run`getent ahostsv6 ${context.config.domain}`; + if (aaaa.trim()) { + info(`warning: domain ${context.config.domain} has AAAA record; HY2XS remains IPv4-only`); + } + } catch { + // no AAAA is acceptable + } + } + + if (await isPortBusy(context.config.hysteriaPort)) { + fail(`Hysteria UDP/TCP port already appears to be in use: ${context.config.hysteriaPort}`); + } + + if (await isPortBusy(context.config.uiPort)) { + fail(`HY2XS admin port already appears to be in use: ${context.config.uiPort}`); } } diff --git a/orchestrator/src/steps/smoke.ts b/orchestrator/src/steps/smoke.ts index 1c54fda..1035657 100644 --- a/orchestrator/src/steps/smoke.ts +++ b/orchestrator/src/steps/smoke.ts @@ -13,7 +13,17 @@ export async function smoke(context: InstallContext): Promise { await runVisible`systemctl is-active --quiet hy2xs-admin`; await runVisible`/usr/local/bin/hysteria version`; await runVisible`test -s /etc/hysteria/config.yaml`; + await runVisible`test -s /etc/hy2xs/hy2xs.env`; await runVisible`test -s /etc/hysteria/post-install.env`; - await runVisible`ss -H -lntu | grep -q ':${context.options.uiPort} '`; - await runVisible`curl -fsS --max-time 5 http://127.0.0.1:${context.options.uiPort}/ >/dev/null`; + await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`; + await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`; + await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`; + await runVisible`test "$(stat -c '%a' ${context.config.bootstrapAdminSecretPath})" = '600'`; + await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`; + if (context.config.uiBindHost === "127.0.0.1") { + await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`; + } + 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}/ >/dev/null`; } diff --git a/orchestrator/src/steps/systemd.ts b/orchestrator/src/steps/systemd.ts index 6487729..49ea8e7 100644 --- a/orchestrator/src/steps/systemd.ts +++ b/orchestrator/src/steps/systemd.ts @@ -4,8 +4,11 @@ import { runVisible } from "../lib/process"; export async function deploySystemd(context: InstallContext): Promise { const values = { - UI_BIND_HOST: context.options.uiBindHost, - UI_PORT: context.options.uiPort + UI_BIND_HOST: context.config.uiBindHost, + UI_PORT: context.config.uiPort, + INSTALL_DIR: context.config.installDir, + DATA_DIR: context.config.dataDir, + LOG_DIR: context.config.logDir }; const hysteriaUnit = await readText(`${context.options.packageDir}/systemd/hysteria-server.service`); diff --git a/orchestrator/src/steps/ui.ts b/orchestrator/src/steps/ui.ts index a741b36..3906fb7 100644 --- a/orchestrator/src/steps/ui.ts +++ b/orchestrator/src/steps/ui.ts @@ -2,7 +2,7 @@ import type { InstallContext } from "../types/context"; import { runVisible } from "../lib/process"; export async function deployUi(context: InstallContext): Promise { - await runVisible`cp -a ${context.options.packageDir}/ui/hy2xs-admin/. /opt/hy2xs-admin/`; - await runVisible`chown -R root:root /opt/hy2xs-admin`; - await runVisible`chmod -R go-w /opt/hy2xs-admin`; + await runVisible`cp -a ${context.options.packageDir}/ui/hy2xs-admin/. ${context.config.installDir}/`; + await runVisible`chown -R root:root ${context.config.installDir}`; + await runVisible`chmod -R go-w ${context.config.installDir}`; } diff --git a/orchestrator/src/types/context.ts b/orchestrator/src/types/context.ts index c044540..5d864cd 100644 --- a/orchestrator/src/types/context.ts +++ b/orchestrator/src/types/context.ts @@ -1,23 +1,71 @@ export type InstallOptions = { packageDir: string; + configPath: string; nonInteractive: boolean; - domain: string; - port: number; - sshPort: number; skipFirewall: boolean; skipStart: boolean; - uiPort: number; +}; + +export type ReconfigureOptions = { + packageDir: string; + configPath: string; + nonInteractive: boolean; + dryRun: boolean; + apply: boolean; + skipFirewall: boolean; + skipStart: boolean; +}; + +export type TlsMode = "acme" | "file" | "self_signed_dev"; + +export type RuntimeConfig = { + domain: string; + publicHost: string; + publicPort: number; + ipv6Enabled: false; + sshPort: number; + firewallEnabled: boolean; + firewallStagedApply: boolean; uiBindHost: string; + uiPort: number; + adminUser: string; + adminInitialPassword: string; + forcePasswordChange: boolean; + tlsMode: TlsMode; + acmeEmail: string; + tlsCertPath: string; + tlsKeyPath: string; + hysteriaBindHost: string; + hysteriaPort: number; + hysteriaAuthMode: "http"; + hysteriaAuthUrl: string; + hysteriaTrafficStatsHost: string; + hysteriaTrafficStatsPort: number; + hysteriaTrafficStatsSecret: string; + hysteriaObfsType: "salamander"; + hysteriaObfsPassword: string; + hysteriaBandwidthUp: string; + hysteriaBandwidthDown: string; + hysteriaIgnoreClientBandwidth: boolean; + hysteriaConfigPath: string; + hysteriaVersionPolicy: string; + installDir: string; + dataDir: string; + logDir: string; + bootstrapAdminSecretPath: string; }; export type InstallContext = { options: InstallOptions; + config: RuntimeConfig; packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; - hysteriaObfsPassword: string; - hysteriaApiSecret: string; - hysteriaApiPort: number; hysteriaVersion: string; }; + +export type ReconfigureContext = { + options: ReconfigureOptions; + config: RuntimeConfig; +}; diff --git a/package/examples/post-install.env.example b/package/examples/post-install.env.example index e7e2f8a..064be63 100644 --- a/package/examples/post-install.env.example +++ b/package/examples/post-install.env.example @@ -1,14 +1,47 @@ -# post-install.env example -# Baseline reference file for a clean Debian 12 deployment. +# HY2XS post-install reference file. +# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом. -HY2XS_PACKAGE_VERSION=0.1.0 -HY2XS_PACKAGE_BUILD_ID=build-20260413-001 -HY2XS_ORCHESTRATOR_STACK=Bun+TypeScript -HY2XS_INSTALL_DATE=2026-04-13T10:00:00Z -HY2XS_DOMAIN=example.com -HY2XS_HYSTERIA_VERSION=v2.8.1 -HY2XS_HYSTERIA_PORT=443 -HY2XS_UI_BIND_HOST=127.0.0.1 -HY2XS_UI_PORT=8081 -HY2XS_ADMIN_PATH=/opt/hy2xs-admin -HY2XS_CONFIG_PATH=/etc/hysteria/config.yaml +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 diff --git a/package/systemd/hy2xs-admin.service b/package/systemd/hy2xs-admin.service index d04fae2..99aebf6 100644 --- a/package/systemd/hy2xs-admin.service +++ b/package/systemd/hy2xs-admin.service @@ -5,13 +5,23 @@ Wants=network-online.target [Service] Type=simple -WorkingDirectory=/opt/hy2xs-admin -Environment=HUI_DATA=/var/lib/hy2xs-admin/ +User=hy2xs-admin +Group=hy2xs-admin +WorkingDirectory={{INSTALL_DIR}} +Environment=HUI_DATA={{DATA_DIR}}/ Environment=HY2XS_UI_BIND_HOST={{UI_BIND_HOST}} -ExecStart=/opt/hy2xs-admin/hy2xs-admin -p {{UI_PORT}} +Environment=HY2XS_UI_PORT={{UI_PORT}} +ExecStart={{INSTALL_DIR}}/hy2xs-admin -p {{UI_PORT}} Restart=on-failure RestartSec=5s NoNewPrivileges=true +PrivateTmp=true +ProtectHome=true +ProtectSystem=strict +ReadWritePaths={{DATA_DIR}} {{LOG_DIR}} +RestrictAddressFamilies=AF_INET AF_UNIX +CapabilityBoundingSet= +AmbientCapabilities= [Install] WantedBy=multi-user.target diff --git a/package/templates/env/post-install.env.tpl b/package/templates/env/post-install.env.tpl index 694b801..25d9d5b 100644 --- a/package/templates/env/post-install.env.tpl +++ b/package/templates/env/post-install.env.tpl @@ -13,15 +13,23 @@ ORCH_BUILD_ID={{PACKAGE_BUILD_ID}} ORCH_ENTRYPOINT=/usr/local/lib/hy2xs/hy2xs-orchestrator DEPLOY_DOMAIN={{DOMAIN}} +PUBLIC_HOST={{PUBLIC_HOST}} +PUBLIC_PORT={{PUBLIC_PORT}} SSH_PORT={{SSH_PORT}} +HY2XS_FIREWALL_ENABLED={{FIREWALL_ENABLED}} +HY2XS_FIREWALL_STAGED_APPLY={{FIREWALL_STAGED_APPLY}} HY2_SOURCE=official-upstream HY2_VERSION={{HYSTERIA_VERSION}} -HY2_LISTEN_HOST=0.0.0.0 +HY2_TLS_MODE={{TLS_MODE}} +HY2_ACME_EMAIL={{ACME_EMAIL}} +HY2_TLS_CERT_PATH={{TLS_CERT_PATH}} +HY2_TLS_KEY_PATH={{TLS_KEY_PATH}} +HY2_LISTEN_HOST={{HYSTERIA_BIND_HOST}} HY2_PORT={{HYSTERIA_PORT}} HY2_AUTH_MODE=http HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth -HY2_TRAFFIC_STATS_LISTEN=127.0.0.1:{{HYSTERIA_API_PORT}} +HY2_TRAFFIC_STATS_LISTEN={{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}} HY2_OBFS_TYPE=salamander HY2_OBFS_PASSWORD={{HYSTERIA_OBFS_PASSWORD}} HY2_BANDWIDTH_UP_Mbps=50 @@ -34,5 +42,6 @@ HUI_FORK_REF=packaged HUI_BUILD_ID={{PACKAGE_BUILD_ID}} HUI_BIND_HOST={{UI_BIND_HOST}} HUI_PORT={{UI_PORT}} -HUI_INSTALL_DIR=/opt/hy2xs-admin -HUI_DATA_DIR=/var/lib/hy2xs-admin +HUI_INSTALL_DIR={{INSTALL_DIR}} +HUI_DATA_DIR={{DATA_DIR}} +HUI_LOG_DIR={{LOG_DIR}} diff --git a/package/templates/hysteria/config.yaml.tpl b/package/templates/hysteria/config.yaml.tpl index ec08f31..3990bbb 100644 --- a/package/templates/hysteria/config.yaml.tpl +++ b/package/templates/hysteria/config.yaml.tpl @@ -1,14 +1,13 @@ -listen: :{{HYSTERIA_PORT}} +listen: {{HYSTERIA_BIND_HOST}}:{{HYSTERIA_PORT}} -tls: - cert: /etc/hysteria/server.crt - key: /etc/hysteria/server.key +{{TLS_ACME_BLOCK}} +{{TLS_FILE_BLOCK}} auth: type: http http: url: http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth - insecure: true + insecure: {{AUTH_INSECURE}} obfs: type: salamander @@ -20,7 +19,7 @@ bandwidth: down: "{{BANDWIDTH_DOWN}}" trafficStats: - listen: 127.0.0.1:{{HYSTERIA_API_PORT}} + listen: {{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}} secret: "{{HYSTERIA_API_SECRET}}" quic: diff --git a/package/templates/nftables/hy2xs.nft.tpl b/package/templates/nftables/hy2xs.nft.tpl index 39c2bfd..5d822cd 100644 --- a/package/templates/nftables/hy2xs.nft.tpl +++ b/package/templates/nftables/hy2xs.nft.tpl @@ -1,4 +1,4 @@ -table inet hy2xs { +table ip hy2xs { chain input { type filter hook input priority 0; policy drop; @@ -6,9 +6,7 @@ table inet hy2xs { ct state established,related accept tcp dport {{SSH_PORT}} accept udp dport {{HYSTERIA_PORT}} accept - tcp dport {{UI_PORT}} ip saddr 127.0.0.1 accept + tcp dport {{UI_PORT}} ip saddr {{UI_BIND_HOST}} accept icmp type echo-request accept - ip6 nexthdr ipv6-icmp accept - ip protocol icmp accept } }