Реализован production-hardening по fix1: env/reconfigure, IPv4-only, TLS, secrets, firewall, docs
This commit is contained in:
@@ -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:<port>` для Hysteria, `127.0.0.1:<ui_port>` для UI по умолчанию);
|
||||
- UI запускается не от root (`hy2xs-admin`);
|
||||
- секреты и чувствительные конфиги: `0600`;
|
||||
- snapshot deploy-фактов: `/etc/hysteria/post-install.env`;
|
||||
- editable runtime layer: `/etc/hy2xs/hy2xs.env`.
|
||||
|
||||
@@ -48,4 +48,3 @@ func Execute() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -36,7 +36,11 @@ func runReset(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
if err = dao.UpdateAccount([]int64{1}, map[string]interface{}{
|
||||
"username": username,
|
||||
"pass": util.SHA224String(password),
|
||||
"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)))
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,3 @@ func init() {
|
||||
func runVersion(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("HY2XS admin version", constant.Version)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ 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
|
||||
@@ -34,6 +34,7 @@ func Login(c *gin.Context) {
|
||||
jwtVo := vo.JwtVo{
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+15
-58
@@ -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,20 +79,9 @@ 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)
|
||||
vo.Fail("port hopping out of scope in HY2XS production", c)
|
||||
return
|
||||
}
|
||||
hysteria2ConfigPortHopping, err := service.GetConfig(constant.Hysteria2ConfigPortHopping)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if *hysteria2ConfigPortHopping.Value != value {
|
||||
needResetPortHopping = true
|
||||
}
|
||||
}
|
||||
|
||||
if key == constant.ResetTrafficCron {
|
||||
resetTrafficCron, err := service.GetConfig(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)
|
||||
}
|
||||
|
||||
|
||||
@@ -150,4 +150,3 @@ func Hysteria2Subscribe(c *gin.Context) {
|
||||
|
||||
c.String(200, configStr)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,4 +114,3 @@ func ExportLog(c *gin.Context) {
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,3 @@ func MonitorHysteria2(c *gin.Context) {
|
||||
}
|
||||
vo.Success(hysteria2MonitorVo, c)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,3 @@ func validateField[T interface{}](c *gin.Context, field T) (T, error) {
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -117,4 +117,3 @@ func ListAccount(query interface{}, args ...interface{}) ([]entity.Account, erro
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -64,4 +64,3 @@ func UpsertConfig(configs []entity.Config) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+62
-1
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,4 +73,3 @@ func replaceRelativePaths(htmlContent string, basePath string) string {
|
||||
</script>`, basePath)
|
||||
return strings.Replace(htmlContent, "</head>", injection+"</head>", 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+2
-58
@@ -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
|
||||
|
||||
+1
-3
@@ -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
|
||||
|
||||
-22
@@ -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=
|
||||
|
||||
@@ -5,4 +5,3 @@ import "hy2xs-admin/cmd"
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,3 @@ func AdminHandler() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,3 @@ func InitCron() error {
|
||||
c.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,3 @@ func FilterHandler() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,4 +36,3 @@ func JWTHandler() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,4 +41,3 @@ func LogHandler() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,3 @@ func RateLimiterHandler() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,3 @@ type AccountExport struct {
|
||||
ConAt int64 `json:"conAt"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@ type Hysteria2UserTraffic struct {
|
||||
Tx int64 `json:"tx"` // upload
|
||||
Rx int64 `json:"rx"` // download
|
||||
}
|
||||
|
||||
|
||||
@@ -25,4 +25,3 @@ type ClashConfig struct {
|
||||
Proxies []interface{} `yaml:"proxies"`
|
||||
ProxyGroups []ProxyGroup `yaml:"proxy-groups"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ const (
|
||||
V2rayN = "v2rayn"
|
||||
NekoBox = "nekobox"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ const (
|
||||
CodeForbiddenError int = 50403
|
||||
CodeInvalidError int = 50001
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
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"
|
||||
@@ -14,4 +15,3 @@ const (
|
||||
ResetTrafficCron = "RESET_TRAFFIC_CRON"
|
||||
ClashExtension = "CLASH_EXTENSION"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ const (
|
||||
WrongPassword string = "wrong password"
|
||||
ConfigNotExist string = "config not exist"
|
||||
)
|
||||
|
||||
|
||||
@@ -18,4 +18,3 @@ const (
|
||||
|
||||
Version = "v0.0.22"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -16,4 +16,3 @@ type ConfigUpdateDto struct {
|
||||
type ConfigsUpdateDto struct {
|
||||
ConfigUpdateDtos []ConfigUpdateDto `json:"configUpdateDtos" form:"configUpdateDtos" validate:"required"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ type BaseDto struct {
|
||||
type IdDto struct {
|
||||
Id *int64 `json:"id" form:"id" validate:"required,gt=0"` // Первичный ключ
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ type LogDto struct {
|
||||
type LogExportDto struct {
|
||||
Option *int `json:"option" form:"option" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ package dto
|
||||
type ServerDto struct {
|
||||
Port *int64 `json:"port" form:"port" validate:"required,min=1,max=65535"`
|
||||
}
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ type Account struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ type Config struct {
|
||||
Remark *string `gorm:"column:remark;default:''" json:"remark"`
|
||||
BaseEntity `gorm:"embedded"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,3 @@ type AccountInfoVo struct {
|
||||
Username string `json:"username"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@ type ConfigVo struct {
|
||||
Value string `json:"value"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
|
||||
@@ -38,4 +38,3 @@ type Hysteria2AcmePathVo struct {
|
||||
CrtPath string `json:"crtPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@ package vo
|
||||
type JwtVo struct {
|
||||
TokenType string `json:"tokenType"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
ForcePasswordChange bool `json:"forcePasswordChange"`
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,3 @@ type LogHysteria2Vo struct {
|
||||
Msg string `json:"msg"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ type Hysteria2MonitorVo struct {
|
||||
Version string `json:"version"` // Версия
|
||||
Running bool `json:"running"` // Статус выполнения
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,3 @@ func Fail(message string, c *gin.Context) {
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ type BaseVo struct {
|
||||
Id int64 `json:"id"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
}
|
||||
|
||||
|
||||
@@ -56,4 +56,3 @@ func (h *Hysteria2Process) Release() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -134,4 +134,3 @@ func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) {
|
||||
}
|
||||
return onlineUsers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -227,4 +227,3 @@ func (p *process) handleLogs(stdout, stderr io.ReadCloser) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,3 @@ func initAccountAdminRouter(accountApi *gin.RouterGroup) {
|
||||
account.GET("/verifyDefaultPass", controller.VerifyDefaultPass)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,4 +11,3 @@ func initAuthRouter(authApi *gin.RouterGroup) {
|
||||
auth.POST("/login", controller.Login)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,3 @@ func initConfigRouter(configApi *gin.RouterGroup) {
|
||||
config.POST("/uploadCertFile", controller.UploadCertFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,3 @@ func initHysteria2Router(hysteria2Api *gin.RouterGroup) {
|
||||
hysteria2.GET("/hysteria2Url", controller.Hysteria2Url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ func initLogRouter(accountApi *gin.RouterGroup) {
|
||||
account.POST("/exportLog", controller.ExportLog)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ func initMonitorRouter(accountApi *gin.RouterGroup) {
|
||||
account.GET("/monitorHysteria2", controller.MonitorHysteria2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,3 @@ func Router(router *gin.Engine, huiWebContext *string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-5
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,4 +232,3 @@ func iptablesRules(protocol string) ([]string, error) {
|
||||
rules := strings.Split(output, "\n")
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -58,4 +58,3 @@ func GetToken(c *gin.Context) string {
|
||||
}
|
||||
return strings.SplitN(tokenStr, " ", 2)[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -62,4 +62,3 @@ func MonitorHysteria2() (vo.Hysteria2MonitorVo, error) {
|
||||
hysteria2MonitorVo.Running = running
|
||||
return hysteria2MonitorVo, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,4 +67,3 @@ func GetServerPortAndCert() (int64, string, string, error) {
|
||||
|
||||
return port, crtPath, keyPath, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,3 @@ func SplitArr[T any](arr []T, num int) [][]T {
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,4 +5,3 @@ import "testing"
|
||||
func TestSHA224String(t *testing.T) {
|
||||
println(SHA224String("sysadmin"))
|
||||
}
|
||||
|
||||
|
||||
@@ -38,4 +38,3 @@ func ExportFile(filePath string, data any, t int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -86,4 +86,3 @@ func FindFile(dir, filename string) (string, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -98,4 +98,3 @@ func VerifyPort(port string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,3 @@ func SplitMap[T any](inputMap map[string]T, chunkSize int) []map[string]T {
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
|
||||
@@ -17,4 +17,3 @@ func RandomString(length int) (string, error) {
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -34,4 +34,3 @@ func CompareVersion(version1, version2 string) int {
|
||||
// The version number is exactly the same
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
## Серверные инварианты
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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, документация начинает неверно обещать лишнее:
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Что не делаем
|
||||
|
||||
|
||||
@@ -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 <path> [--config /etc/hy2xs/hy2xs.env]`
|
||||
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run`
|
||||
- `reconfigure --package-dir <path> --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.
|
||||
|
||||
## Что не реализовывать
|
||||
|
||||
|
||||
+21
-10
@@ -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`
|
||||
- использовать файл как замену настоящей конфигурации сервисов
|
||||
|
||||
## Пример
|
||||
|
||||
@@ -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:<udp_port>`)
|
||||
4. HY2XS admin слушает ожидаемый `HY2XS_UI_BIND_HOST:<ui_port>`
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:<ui_port>/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:<port>
|
||||
- HY2XS admin должен слушать только IPv4 bind host:
|
||||
127.0.0.1:<ui_port> по умолчанию.
|
||||
- Не использовать [::], ::, 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:<port>
|
||||
или другой явно заданный 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:<ui_port>, а не 0.0.0.0:<ui_port>.
|
||||
- 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 <candidate>
|
||||
- Сделать 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.
|
||||
+66
-33
@@ -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 <path> [--domain <name>] [--port <udp>] [--ssh-port <tcp>] [--skip-firewall] [--skip-start] [--ui-port <tcp>] [--ui-bind-host <host>] [--non-interactive]");
|
||||
console.error("Usage:");
|
||||
console.error(" hy2xs-orchestrator install --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start] [--non-interactive]");
|
||||
console.error(" hy2xs-orchestrator reconfigure --package-dir <path> [--config <path>] [--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}`);
|
||||
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<void> {
|
||||
const [command, ...args] = Bun.argv.slice(2);
|
||||
if (command !== "install") {
|
||||
usage();
|
||||
}
|
||||
|
||||
if (command === "install") {
|
||||
await install(parseInstallOptions(args));
|
||||
return;
|
||||
}
|
||||
if (command === "reconfigure") {
|
||||
await reconfigure(parseReconfigureOptions(args));
|
||||
return;
|
||||
}
|
||||
usage();
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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");
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { RuntimeConfig, TlsMode } from "../types/context";
|
||||
|
||||
type EnvMap = Record<string, string>;
|
||||
|
||||
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`;
|
||||
}
|
||||
@@ -3,19 +3,40 @@ import { readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function generateConfig(context: InstallContext): Promise<void> {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,29 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
|
||||
PACKAGE_VERSION: context.packageVersion,
|
||||
PACKAGE_BUILD_ID: context.packageBuildId,
|
||||
INSTALL_DATE: context.installDate,
|
||||
DOMAIN: context.options.domain,
|
||||
SSH_PORT: context.options.sshPort,
|
||||
DOMAIN: context.config.domain,
|
||||
PUBLIC_HOST: context.config.publicHost,
|
||||
PUBLIC_PORT: context.config.publicPort,
|
||||
SSH_PORT: context.config.sshPort,
|
||||
FIREWALL_ENABLED: context.config.firewallEnabled ? "true" : "false",
|
||||
FIREWALL_STAGED_APPLY: context.config.firewallStagedApply ? "true" : "false",
|
||||
HYSTERIA_VERSION: context.hysteriaVersion,
|
||||
HYSTERIA_PORT: context.options.port,
|
||||
HYSTERIA_OBFS_PASSWORD: context.hysteriaObfsPassword,
|
||||
HYSTERIA_API_PORT: context.hysteriaApiPort,
|
||||
UI_BIND_HOST: context.options.uiBindHost,
|
||||
UI_PORT: context.options.uiPort
|
||||
TLS_MODE: context.config.tlsMode,
|
||||
ACME_EMAIL: context.config.acmeEmail,
|
||||
TLS_CERT_PATH: context.config.tlsCertPath,
|
||||
TLS_KEY_PATH: context.config.tlsKeyPath,
|
||||
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
||||
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
||||
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
||||
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
|
||||
});
|
||||
|
||||
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
|
||||
await writeText(context.config.bootstrapAdminSecretPath, `${context.config.adminUser}:${context.config.adminInitialPassword}\n`, 0o600);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function prepareFilesystem(_context: InstallContext): Promise<void> {
|
||||
export async function prepareFilesystem(context: InstallContext): Promise<void> {
|
||||
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`;
|
||||
}
|
||||
|
||||
@@ -4,19 +4,36 @@ import { info } from "../lib/log";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function applyFirewall(context: InstallContext): Promise<void> {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
@@ -13,6 +13,8 @@ async function isPortBusy(port: number): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function preflight(context: InstallContext): Promise<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user