diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3572648 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +ARG RUNTIME_BASE=jrohy/trojan@sha256:f559fac7e9960614032e520c59405cec0e8937eca009a8169cb1d188f2b5ccb9 +FROM node:20.11.1-bookworm AS frontend +WORKDIR /src +COPY trojan-web-master/package.json trojan-web-master/package-lock.json ./ +RUN npm ci +COPY trojan-web-master/ ./ +RUN npm run build + +FROM golang:1.21.6-bookworm AS manager-build +WORKDIR /src +COPY trojan-master/go.mod trojan-master/go.sum ./ +RUN go mod download +COPY trojan-master/ ./ +COPY --from=frontend /src/dist ./web/templates +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/trojan . + +# Production builds pass an immutable, verified runtime snapshot that contains +# the legacy Trojan core. This avoids a runtime download and preserves protocol +# compatibility during the first replacement release. +FROM ${RUNTIME_BASE} +COPY --from=manager-build /out/trojan /usr/local/bin/trojan +COPY trojan-master/asset/trojan.service /etc/systemd/system/trojan.service +COPY trojan-master/asset/trojan-web.service /etc/systemd/system/trojan-web.service +COPY trojan-master/asset/trojan-acme-challenge.service /etc/systemd/system/trojan-acme-challenge.service +COPY trojan-master/asset/trojan-acme-renew.service /etc/systemd/system/trojan-acme-renew.service +COPY trojan-master/asset/trojan-acme-renew.timer /etc/systemd/system/trojan-acme-renew.timer +COPY trojan-master/asset/renew-trojan-certificate /usr/local/sbin/renew-trojan-certificate +COPY trojan-master/asset/compat-init /usr/local/sbin/compat-init +RUN chmod 0755 /usr/local/sbin/renew-trojan-certificate /usr/local/sbin/compat-init +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD curl -fsS "http://127.0.0.1:${WEB_PORT:-8081}/healthz" || exit 1 +CMD ["/usr/local/sbin/compat-init"] diff --git a/trojan-master/asset/compat-init b/trojan-master/asset/compat-init new file mode 100644 index 0000000..90c4cb2 --- /dev/null +++ b/trojan-master/asset/compat-init @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +# The legacy image's systemd replacement can supervise units without a real +# PID-1 systemd. Start them explicitly and keep the container foreground alive. +systemctl daemon-reload || true +systemctl start trojan +systemctl start trojan-web +systemctl start trojan-acme-challenge +systemctl enable trojan trojan-web trojan-acme-challenge trojan-acme-renew.timer || true +exec tail -f /dev/null diff --git a/trojan-master/asset/renew-trojan-certificate b/trojan-master/asset/renew-trojan-certificate new file mode 100644 index 0000000..56985a2 --- /dev/null +++ b/trojan-master/asset/renew-trojan-certificate @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail +domain="${ACME_DOMAIN:?ACME_DOMAIN is required}" +home=/root/.acme.sh +cert_dir=/etc/trojan/certs +source_cert="$home/${domain}_ecc/fullchain.cer" +source_key="$home/${domain}_ecc/${domain}.key" +before="" +[[ -f "$cert_dir/fullchain.pem" ]] && before=$(openssl x509 -in "$cert_dir/fullchain.pem" -noout -fingerprint -sha256) +"$home/acme.sh" --cron --home "$home" +test -r "$source_cert" -a -r "$source_key" +openssl x509 -in "$source_cert" -noout -checkend 0 +mkdir -p "$cert_dir" +install -m 0644 "$source_cert" "$cert_dir/fullchain.pem" +install -m 0600 "$source_key" "$cert_dir/private.key" +openssl x509 -noout -modulus -in "$cert_dir/fullchain.pem" | openssl sha256 > /tmp/trojan-cert.modulus +openssl rsa -noout -modulus -in "$cert_dir/private.key" | openssl sha256 >> /tmp/trojan-cert.modulus +[[ $(cut -d' ' -f2 /tmp/trojan-cert.modulus | sort -u | wc -l) -eq 1 ]] +after=$(openssl x509 -in "$cert_dir/fullchain.pem" -noout -fingerprint -sha256) +if [[ "$before" != "$after" ]]; then systemctl restart trojan; fi diff --git a/trojan-master/asset/trojan-acme-challenge.service b/trojan-master/asset/trojan-acme-challenge.service new file mode 100644 index 0000000..26f689f --- /dev/null +++ b/trojan-master/asset/trojan-acme-challenge.service @@ -0,0 +1,13 @@ +[Unit] +Description=Trojan ACME HTTP-01 challenge server +After=network.target + +[Service] +Type=simple +Environment=ACME_WEBROOT=/var/lib/trojan-acme-webroot +ExecStart=/bin/sh -c '/usr/local/bin/trojan acme-http --port "${ACME_HTTP_PORT:-8082}"' +Restart=on-failure +RestartSec=3s + +[Install] +WantedBy=multi-user.target diff --git a/trojan-master/asset/trojan-acme-renew.service b/trojan-master/asset/trojan-acme-renew.service new file mode 100644 index 0000000..1e331d3 --- /dev/null +++ b/trojan-master/asset/trojan-acme-renew.service @@ -0,0 +1,6 @@ +[Unit] +Description=Renew Trojan ACME certificate + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/renew-trojan-certificate diff --git a/trojan-master/asset/trojan-acme-renew.timer b/trojan-master/asset/trojan-acme-renew.timer new file mode 100644 index 0000000..ceaa12b --- /dev/null +++ b/trojan-master/asset/trojan-acme-renew.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Daily Trojan ACME renewal check + +[Timer] +OnCalendar=daily +RandomizedDelaySec=6h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/trojan-master/asset/trojan-web.service b/trojan-master/asset/trojan-web.service index d61743c..84a4259 100644 --- a/trojan-master/asset/trojan-web.service +++ b/trojan-master/asset/trojan-web.service @@ -6,10 +6,10 @@ After=network.target network-online.target nss-lookup.target mysql.service maria [Service] Type=simple StandardError=journal -ExecStart=/usr/local/bin/trojan web +ExecStart=/bin/sh -c '/usr/local/bin/trojan web --host "${WEB_HOST:-127.0.0.1}" --port "${WEB_PORT:-8081}"' ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=3s [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/trojan-master/asset/trojan.service b/trojan-master/asset/trojan.service new file mode 100644 index 0000000..cb63dc9 --- /dev/null +++ b/trojan-master/asset/trojan.service @@ -0,0 +1,14 @@ +[Unit] +Description=trojan +After=network.target network-online.target nss-lookup.target mysql.service mariadb.service mysqld.service + +[Service] +Type=simple +StandardError=journal +ExecStart=/usr/bin/trojan/trojan /usr/local/etc/trojan/config.json +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=3s + +[Install] +WantedBy=multi-user.target diff --git a/trojan-master/cmd/acme_http.go b/trojan-master/cmd/acme_http.go new file mode 100644 index 0000000..1ca4988 --- /dev/null +++ b/trojan-master/cmd/acme_http.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var acmeHTTPPort int + +var acmeHTTPCmd = &cobra.Command{ + Use: "acme-http", + Short: "serve only ACME HTTP-01 challenge files", + RunE: func(cmd *cobra.Command, args []string) error { + root := os.Getenv("ACME_WEBROOT") + if root == "" { root = "/var/lib/trojan-acme-webroot" } + if err := os.MkdirAll(filepath.Join(root, ".well-known", "acme-challenge"), 0750); err != nil { return err } + addr := fmt.Sprintf("127.0.0.1:%d", acmeHTTPPort) + return http.ListenAndServe(addr, http.FileServer(http.Dir(root))) + }, +} + +func init() { acmeHTTPCmd.Flags().IntVar(&acmeHTTPPort, "port", 8082, "ACME HTTP listener port"); rootCmd.AddCommand(acmeHTTPCmd) } diff --git a/trojan-master/core/server.go b/trojan-master/core/server.go index 4b4ac21..9a7657f 100644 --- a/trojan-master/core/server.go +++ b/trojan-master/core/server.go @@ -6,6 +6,7 @@ import ( "github.com/tidwall/pretty" "github.com/tidwall/sjson" "os" + "path/filepath" ) var configPath = "/usr/local/etc/trojan/config.json" @@ -53,10 +54,13 @@ func Save(data []byte, path string) bool { if path == "" { path = configPath } - if err := os.WriteFile(path, pretty.Pretty(data), 0644); err != nil { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { fmt.Println(err); return false } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, pretty.Pretty(data), 0644); err != nil { fmt.Println(err) return false } + if err := os.Rename(tmp, path); err != nil { fmt.Println(err); return false } return true } diff --git a/trojan-master/managerconfig/manager.go b/trojan-master/managerconfig/manager.go new file mode 100644 index 0000000..7602811 --- /dev/null +++ b/trojan-master/managerconfig/manager.go @@ -0,0 +1,47 @@ +// Package managerconfig owns settings which are not part of Trojan's native JSON. +package managerconfig + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +const DefaultPath = "/etc/trojan-manager/manager.yaml" + +type Config struct { + SchemaVersion int `yaml:"schema_version" json:"schemaVersion"` + Public struct { + Domain string `yaml:"domain" json:"domain"` + Port int `yaml:"port" json:"port"` + } `yaml:"public" json:"public"` + Runtime struct { + ListenAddress string `yaml:"listen_address" json:"listenAddress"` + ListenPort int `yaml:"listen_port" json:"listenPort"` + FallbackAddress string `yaml:"fallback_address" json:"fallbackAddress"` + FallbackPort int `yaml:"fallback_port" json:"fallbackPort"` + } `yaml:"runtime" json:"runtime"` + Web struct { ListenAddress string `yaml:"listen_address" json:"listenAddress"`; ListenPort int `yaml:"listen_port" json:"listenPort"` } `yaml:"web" json:"web"` + ACME struct { Enabled bool `yaml:"enabled" json:"enabled"`; Domain string `yaml:"domain" json:"domain"`; HTTPPort int `yaml:"http_port" json:"httpPort"` } `yaml:"acme" json:"acme"` + Firewall struct { Manage bool `yaml:"manage" json:"manage"` } `yaml:"firewall" json:"firewall"` +} + +func Defaults() Config { + var c Config + c.SchemaVersion = 1; c.Public.Port = 443; c.Runtime.ListenAddress = "0.0.0.0"; c.Runtime.ListenPort = 8443 + c.Runtime.FallbackAddress = "127.0.0.1"; c.Runtime.FallbackPort = 8081; c.Web.ListenAddress = "127.0.0.1"; c.Web.ListenPort = 8081; c.ACME.HTTPPort = 8082 + return c +} +func Path() string { if p := os.Getenv("TROJAN_MANAGER_CONFIG"); p != "" { return p }; return DefaultPath } +func Load() (Config, error) { c := Defaults(); b, err := os.ReadFile(Path()); if os.IsNotExist(err) { return c, nil }; if err != nil { return c, err }; err = yaml.Unmarshal(b, &c); return c, err } +func Validate(c Config) error { + if c.SchemaVersion != 1 { return errors.New("unsupported manager config schema") } + ports := []int{c.Public.Port, c.Runtime.ListenPort, c.Runtime.FallbackPort, c.Web.ListenPort, c.ACME.HTTPPort} + for _, p := range ports { if p < 1 || p > 65535 { return fmt.Errorf("invalid port %d", p) } } + if c.Runtime.ListenPort == c.Web.ListenPort || c.Runtime.ListenPort == c.ACME.HTTPPort || c.Web.ListenPort == c.ACME.HTTPPort { return errors.New("listen, web and ACME ports must be distinct") } + return nil +} +func Save(c Config) error { if err := Validate(c); err != nil { return err }; b, err := yaml.Marshal(c); if err != nil { return err }; p := Path(); if err := os.MkdirAll(filepath.Dir(p), 0750); err != nil { return err }; tmp := p + ".tmp"; if err := os.WriteFile(tmp, b, 0640); err != nil { return err }; return os.Rename(tmp, p) } diff --git a/trojan-master/trojan/trojan.go b/trojan-master/trojan/trojan.go index 6da673c..b5553a0 100644 --- a/trojan-master/trojan/trojan.go +++ b/trojan-master/trojan/trojan.go @@ -48,20 +48,20 @@ func ControlMenu() { } // Restart 重启trojan -func Restart() { +func Restart() error { util.OpenPort(core.GetConfig().LocalPort) - util.SystemctlRestart("trojan") + return util.SystemctlRestart("trojan") } // Start 启动trojan -func Start() { +func Start() error { util.OpenPort(core.GetConfig().LocalPort) - util.SystemctlStart("trojan") + return util.SystemctlStart("trojan") } // Stop 停止trojan -func Stop() { - util.SystemctlStop("trojan") +func Stop() error { + return util.SystemctlStop("trojan") } // Status 获取trojan状态 diff --git a/trojan-master/trojan/web.go b/trojan-master/trojan/web.go index 1a9cc20..5c5f630 100644 --- a/trojan-master/trojan/web.go +++ b/trojan-master/trojan/web.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "fmt" "trojan/core" + "trojan/managerconfig" "trojan/util" ) @@ -52,5 +53,8 @@ func SetDomain(domain string) { // GetDomainAndPort 获取域名和端口 func GetDomainAndPort() (string, int) { config := core.GetConfig() + if managed, err := managerconfig.Load(); err == nil && managed.Public.Port != 0 { + return config.SSl.Sni, managed.Public.Port + } return config.SSl.Sni, config.LocalPort } diff --git a/trojan-master/util/command.go b/trojan-master/util/command.go index 9863050..623b765 100644 --- a/trojan-master/util/command.go +++ b/trojan-master/util/command.go @@ -34,30 +34,36 @@ func systemctlBase(name, operate string) (string, error) { } // SystemctlStart 服务启动 -func SystemctlStart(name string) { +func SystemctlStart(name string) error { if _, err := systemctlBase(name, "start"); err != nil { fmt.Println(Red(fmt.Sprintf("启动%s失败!", name))) + return err } else { fmt.Println(Green(fmt.Sprintf("启动%s成功!", name))) } + return nil } // SystemctlStop 服务停止 -func SystemctlStop(name string) { +func SystemctlStop(name string) error { if _, err := systemctlBase(name, "stop"); err != nil { fmt.Println(Red(fmt.Sprintf("停止%s失败!", name))) + return err } else { fmt.Println(Green(fmt.Sprintf("停止%s成功!", name))) } + return nil } // SystemctlRestart 服务重启 -func SystemctlRestart(name string) { +func SystemctlRestart(name string) error { if _, err := systemctlBase(name, "restart"); err != nil { fmt.Println(Red(fmt.Sprintf("重启%s失败!", name))) + return err } else { fmt.Println(Green(fmt.Sprintf("重启%s成功!", name))) } + return nil } // SystemctlEnable 服务设置开机自启 diff --git a/trojan-master/web/auth.go b/trojan-master/web/auth.go index 4b42203..5241cf7 100644 --- a/trojan-master/web/auth.go +++ b/trojan-master/web/auth.go @@ -123,6 +123,12 @@ func RequestUsername(c *gin.Context) string { return claims[identityKey].(string) } +func RequireAdmin(c *gin.Context) bool { + if RequestUsername(c) == "admin" { return true } + c.JSON(403, gin.H{"code": 403, "message": "administrator access required"}) + return false +} + // Auth 权限router func Auth(r *gin.Engine, timeout int) *jwt.GinJWTMiddleware { jwtInit(timeout) @@ -152,7 +158,10 @@ func Auth(r *gin.Engine, timeout int) *jwt.GinJWTMiddleware { } }) r.POST("/auth/login", authMiddleware.LoginHandler) - r.POST("/auth/register", updateUser) + r.POST("/auth/register", func(c *gin.Context) { + if result, _ := core.GetValue("admin_pass"); result != "" { c.JSON(403, gin.H{"code":403,"message":"administrator already initialized"}); return } + updateUser(c) + }) authO := r.Group("/auth") authO.Use(authMiddleware.MiddlewareFunc()) { diff --git a/trojan-master/web/controller/system.go b/trojan-master/web/controller/system.go new file mode 100644 index 0000000..2c32cf1 --- /dev/null +++ b/trojan-master/web/controller/system.go @@ -0,0 +1,18 @@ +package controller + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "time" + "trojan/managerconfig" +) + +func SystemConfig() *ResponseBody { c, err := managerconfig.Load(); r := &ResponseBody{Msg:"success", Data:c}; if err != nil { r.Msg=err.Error() }; return r } +func ValidateSystemConfig(c managerconfig.Config) *ResponseBody { r:=&ResponseBody{Msg:"success"}; if err:=managerconfig.Validate(c); err != nil { r.Msg=err.Error() }; return r } +func ApplySystemConfig(c managerconfig.Config) *ResponseBody { r:=ValidateSystemConfig(c); if r.Msg!="success" { return r }; if err:=managerconfig.Save(c); err!=nil { r.Msg=err.Error(); return r }; r.Data=map[string]bool{"restartRequired":true,"nginxReloadRequired":false,"containerRecreateRequired":false}; return r } +func CertificateStatus() *ResponseBody { + r:=&ResponseBody{Msg:"success"}; b,err:=os.ReadFile("/etc/trojan/certs/fullchain.pem"); if err!=nil { r.Msg=err.Error(); return r }; block,_:=pem.Decode(b); if block==nil { r.Msg="invalid certificate PEM"; return r }; cert,err:=x509.ParseCertificate(block.Bytes); if err!=nil { r.Msg=err.Error(); return r } + r.Data=map[string]interface{}{"subject":cert.Subject.String(),"issuer":cert.Issuer.String(),"dnsNames":cert.DNSNames,"notBefore":cert.NotBefore,"notAfter":cert.NotAfter,"daysRemaining":int(time.Until(cert.NotAfter).Hours()/24),"serialNumber":fmt.Sprint(cert.SerialNumber)}; return r +} diff --git a/trojan-master/web/controller/trojan.go b/trojan-master/web/controller/trojan.go index b5cfb74..23b4039 100644 --- a/trojan-master/web/controller/trojan.go +++ b/trojan-master/web/controller/trojan.go @@ -20,7 +20,7 @@ import ( func Start() *ResponseBody { responseBody := ResponseBody{Msg: "success"} defer TimeCost(time.Now(), &responseBody) - trojan.Start() + if err := trojan.Start(); err != nil { responseBody.Msg = err.Error() } return &responseBody } @@ -28,7 +28,7 @@ func Start() *ResponseBody { func Stop() *ResponseBody { responseBody := ResponseBody{Msg: "success"} defer TimeCost(time.Now(), &responseBody) - trojan.Stop() + if err := trojan.Stop(); err != nil { responseBody.Msg = err.Error() } return &responseBody } @@ -36,7 +36,7 @@ func Stop() *ResponseBody { func Restart() *ResponseBody { responseBody := ResponseBody{Msg: "success"} defer TimeCost(time.Now(), &responseBody) - trojan.Restart() + if err := trojan.Restart(); err != nil { responseBody.Msg = err.Error() } return &responseBody } @@ -53,7 +53,7 @@ func SetLogLevel(level int) *ResponseBody { responseBody := ResponseBody{Msg: "success"} defer TimeCost(time.Now(), &responseBody) core.WriteLogLevel(level) - trojan.Restart() + if err := trojan.Restart(); err != nil { responseBody.Msg = err.Error() } return &responseBody } diff --git a/trojan-master/web/controller/user.go b/trojan-master/web/controller/user.go index e4e5849..74c666b 100644 --- a/trojan-master/web/controller/user.go +++ b/trojan-master/web/controller/user.go @@ -36,8 +36,8 @@ func UserList(requestUser string) *ResponseBody { } domain, port := trojan.GetDomainAndPort() responseBody.Data = map[string]interface{}{ - "domain": domain, - "port": port, + "domain": domain, "port": port, "publicPort": port, + "listenPort": core.GetConfig().LocalPort, "userList": userList, } return &responseBody @@ -55,8 +55,8 @@ func PageUserList(curPage int, pageSize int) *ResponseBody { } domain, port := trojan.GetDomainAndPort() responseBody.Data = map[string]interface{}{ - "domain": domain, - "port": port, + "domain": domain, "port": port, "publicPort": port, + "listenPort": core.GetConfig().LocalPort, "pageData": pageData, } return &responseBody diff --git a/trojan-master/web/web.go b/trojan-master/web/web.go index 03f0de6..8793826 100644 --- a/trojan-master/web/web.go +++ b/trojan-master/web/web.go @@ -8,6 +8,7 @@ import ( "io/fs" "net/http" "strconv" + "trojan/managerconfig" "trojan/core" "trojan/util" "trojan/web/controller" @@ -171,6 +172,14 @@ func noTokenRouter(router *gin.Engine) { }) } +func systemRouter(router *gin.Engine) { + system := router.Group("/system") + system.GET("/config", func(c *gin.Context) { if !RequireAdmin(c) { return }; c.JSON(200, controller.SystemConfig()) }) + system.POST("/config/validate", func(c *gin.Context) { if !RequireAdmin(c) { return }; var cfg managerconfig.Config; if err:=c.ShouldBindJSON(&cfg); err!=nil { c.JSON(400, gin.H{"message":err.Error()}); return }; c.JSON(200, controller.ValidateSystemConfig(cfg)) }) + system.POST("/config/apply", func(c *gin.Context) { if !RequireAdmin(c) { return }; var cfg managerconfig.Config; if err:=c.ShouldBindJSON(&cfg); err!=nil { c.JSON(400, gin.H{"message":err.Error()}); return }; c.JSON(200, controller.ApplySystemConfig(cfg)) }) + system.GET("/certificate", func(c *gin.Context) { if !RequireAdmin(c) { return }; c.JSON(200, controller.CertificateStatus()) }) +} + // Start web启动入口 func Start(host string, port, timeout int, isSSL bool) { router := gin.Default() @@ -178,7 +187,10 @@ func Start(host string, port, timeout int, isSSL bool) { router.Use(gzip.Gzip(gzip.DefaultCompression)) staticRouter(router) noTokenRouter(router) + router.GET("/healthz", func(c *gin.Context) { c.JSON(200, gin.H{"status":"ok"}) }) + router.GET("/readyz", func(c *gin.Context) { if core.GetConfig()==nil { c.JSON(503, gin.H{"status":"not ready"}); return }; c.JSON(200, gin.H{"status":"ready"}) }) router.Use(Auth(router, timeout).MiddlewareFunc()) + systemRouter(router) trojanRouter(router) userRouter(router) dataRouter(router) diff --git a/trojan-stack/compose.yaml b/trojan-stack/compose.yaml new file mode 100644 index 0000000..40d35ec --- /dev/null +++ b/trojan-stack/compose.yaml @@ -0,0 +1,15 @@ +services: + trojan: + image: ${TROJAN_IMAGE:?set an immutable image digest} + container_name: trojan + network_mode: host + privileged: true + restart: always + command: init + env_file: /opt/trojan/runtime.env + volumes: + - /opt/trojan/config:/usr/local/etc/trojan + - /opt/trojan/manager:/var/lib/trojan-manager + - /opt/trojan/acme:/root/.acme.sh + - /opt/trojan/certs:/etc/trojan/certs + - /opt/trojan/manager-config:/etc/trojan-manager diff --git a/trojan-stack/docs/upgrade.md b/trojan-stack/docs/upgrade.md new file mode 100644 index 0000000..5594368 --- /dev/null +++ b/trojan-stack/docs/upgrade.md @@ -0,0 +1,8 @@ +# 无损替换运行手册 + +1. 以镜像 digest 设置 `TROJAN_IMAGE`;禁止使用 `latest`。 +2. 先备份 MariaDB,记录用户数,并保存 `docker inspect trojan` 与 Nginx 配置。 +3. 在旧容器仍运行时执行 `migrate-v1.sh`,然后用 18443/18081/18082 完成预演。 +4. 使用 `verify.sh` 验证管理端、证书、Trojan 监听与公开 443 分享链接。 +5. 正式切换时停止并重命名旧容器为 `trojan-legacy`,再以本 compose 启动新容器。 +6. 任一核心检查失败,立即运行 `rollback.sh`。保留旧容器至少一个证书续期周期。 diff --git a/trojan-stack/runtime.env.example b/trojan-stack/runtime.env.example new file mode 100644 index 0000000..02d6e7b --- /dev/null +++ b/trojan-stack/runtime.env.example @@ -0,0 +1,8 @@ +ACME_DOMAIN=www.hk.example.com +PUBLIC_DOMAIN=www.hk.example.com +PUBLIC_PORT=443 +TROJAN_LISTEN_PORT=8443 +WEB_HOST=127.0.0.1 +WEB_PORT=8081 +ACME_HTTP_PORT=8082 +MANAGE_FIREWALL=false diff --git a/trojan-stack/scripts/migrate-v1.sh b/trojan-stack/scripts/migrate-v1.sh new file mode 100644 index 0000000..d560887 --- /dev/null +++ b/trojan-stack/scripts/migrate-v1.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +legacy_container=${1:-trojan} +target=/opt/trojan +install -d -m 0750 "$target"/{config,manager,acme,certs,manager-config} +docker cp "$legacy_container":/usr/local/etc/trojan/config.json "$target/config/config.json" +docker cp "$legacy_container":/var/lib/trojan-manager/. "$target/manager/" +docker cp "$legacy_container":/root/.acme.sh/. "$target/acme/" || true +cat > "$target/manager-config/manager.yaml" <&2; exit 1; } +done +curl --fail --silent --show-error "http://${WEB_HOST:-127.0.0.1}:${WEB_PORT:-8081}/healthz" >/dev/null +openssl x509 -in /opt/trojan/certs/fullchain.pem -noout -checkend 0 +openssl x509 -in /opt/trojan/certs/fullchain.pem -noout -text | grep -F "${PUBLIC_DOMAIN}" >/dev/null +echo "verification passed" diff --git a/trojan-web-master/vite.config.js b/trojan-web-master/vite.config.js index 2997bd1..8a39444 100644 --- a/trojan-web-master/vite.config.js +++ b/trojan-web-master/vite.config.js @@ -2,7 +2,6 @@ import path, { resolve } from 'path' import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" import viteSvgIcons from 'vite-plugin-svg-icons' -import externalGlobals from 'rollup-plugin-external-globals' export default defineConfig(({ command }) => { @@ -65,24 +64,6 @@ export default defineConfig(({ command }) => { drop_debugger: true } }, - rollupOptions:{ - external: ['vue', 'vuex', 'vue-i18n', 'vue-router', 'element-plus', - 'axios', 'crypto-js', 'dayjs', 'easyqrcodejs', 'nprogress'], - plugins: [ - externalGlobals({ - vue: 'Vue', - vuex: 'Vuex', - 'vue-i18n': 'VueI18n', - 'vue-router': 'VueRouter', - axios: 'axios', - 'crypto-js': 'CryptoJS', - 'dayjs': 'dayjs', - 'easyqrcodejs': 'QRCode', - 'nprogress': 'NProgress', - 'element-plus': 'ElementPlus' - }), - ] - } } } -}) \ No newline at end of file +})