diff --git a/.env.example b/.env.example index ee262468b..ab000be2a 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ XUI_DB_FOLDER=x-ui XUI_LOG_FOLDER=x-ui XUI_BIN_FOLDER=x-ui XUI_INIT_WEB_BASE_PATH=/ +# XUI_PORT=8080 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b71d4264..f3ae9cd62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,7 @@ XUI_DB_FOLDER=x-ui XUI_LOG_FOLDER=x-ui XUI_BIN_FOLDER=x-ui XUI_INIT_WEB_BASE_PATH=/ +# XUI_PORT=8080 ``` Drop the xray binary (`xray-windows-amd64.exe` on Windows, `xray-linux-amd64` on Linux, etc.) plus the matching `geoip.dat` and `geosite.dat` files into `x-ui/`. The easiest source is a [released Xray-core build](https://github.com/XTLS/Xray-core/releases). On Windows, `wintun.dll` is also required for testing TUN inbounds. @@ -256,9 +257,16 @@ For deeper notes on the frontend toolchain see [`frontend/README.md`](frontend/R | `XUI_LOG_FOLDER` | platform default | Where `3xui.log` lives | | `XUI_BIN_FOLDER` | `bin` | Where the xray binary, geo files, and xray `config.json` live | | `XUI_INIT_WEB_BASE_PATH` | `/` | The initial URI path for the web panel | +| `XUI_PORT` | persisted `webPort` | Runtime-only web panel listener port override (`1` through `65535`) | | `XUI_DB_TYPE` | `sqlite` | Set to `postgres` to use PostgreSQL via `XUI_DB_DSN` | | `XUI_DB_DSN` | — | PostgreSQL DSN when `XUI_DB_TYPE=postgres` | +A valid `XUI_PORT` takes precedence over the database-backed `webPort` for the +current process without changing the stored setting. Unset, empty, whitespace-only, +malformed, or out-of-range values fall back to `webPort`; invalid configured values +also produce a warning. With Docker bridge networking, the published container port +must match the override, for example `XUI_PORT: "8080"` with `ports: ["8080:8080"]`. + ## Issues - Bug reports and feature requests: [GitHub Issues](https://github.com/MHSanaei/3x-ui/issues) diff --git a/docker-compose.yml b/docker-compose.yml index 9d75ba1cd..168f8be09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: XRAY_VMESS_AEAD_FORCED: "false" XUI_ENABLE_FAIL2BAN: "true" # XUI_INIT_WEB_BASE_PATH: "/" + # XUI_PORT: "8080" # To use PostgreSQL instead of the default SQLite, run: # docker compose --profile postgres up -d # and uncomment the two lines below. @@ -26,6 +27,7 @@ services: # XUI_DB_DSN: "postgres://xui:xui@postgres:5432/xui?sslmode=disable" tty: true ports: + # When XUI_PORT is set, publish the same container port (for example "8080:8080"). - "2053:2053" restart: unless-stopped @@ -39,4 +41,4 @@ services: POSTGRES_DB: xui volumes: - $PWD/pgdata/:/var/lib/postgresql/data - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/internal/config/config.go b/internal/config/config.go index cb1a539c4..48f39e522 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "testing" ) @@ -63,6 +64,23 @@ func IsSkipHSTS() bool { return os.Getenv("XUI_SKIP_HSTS") == "true" } +func GetPortOverride() (port int, configured bool, err error) { + value, ok := os.LookupEnv("XUI_PORT") + if !ok || strings.TrimSpace(value) == "" { + return 0, false, nil + } + + port, err = strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + return 0, true, fmt.Errorf("parse XUI_PORT: %w", err) + } + if port < 1 || port > 65535 { + return 0, true, fmt.Errorf("XUI_PORT must be between 1 and 65535") + } + + return port, true, nil +} + // GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER. func GetBinFolderPath() string { binFolderPath := os.Getenv("XUI_BIN_FOLDER") diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 000000000..494b7bf90 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,61 @@ +package config + +import ( + "os" + "testing" +) + +func TestGetPortOverride(t *testing.T) { + tests := []struct { + name string + value string + set bool + wantPort int + configured bool + wantErr bool + }{ + {name: "unset"}, + {name: "empty", value: "", set: true}, + {name: "whitespace", value: " ", set: true}, + {name: "minimum", value: "1", set: true, wantPort: 1, configured: true}, + {name: "default panel port", value: "2053", set: true, wantPort: 2053, configured: true}, + {name: "surrounding whitespace", value: " 8080 ", set: true, wantPort: 8080, configured: true}, + {name: "maximum", value: "65535", set: true, wantPort: 65535, configured: true}, + {name: "zero", value: "0", set: true, configured: true, wantErr: true}, + {name: "above maximum", value: "65536", set: true, configured: true, wantErr: true}, + {name: "negative", value: "-1", set: true, configured: true, wantErr: true}, + {name: "non-numeric", value: "abc", set: true, configured: true, wantErr: true}, + {name: "decimal", value: "8080.0", set: true, configured: true, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.set { + t.Setenv("XUI_PORT", tt.value) + } else { + original, existed := os.LookupEnv("XUI_PORT") + if err := os.Unsetenv("XUI_PORT"); err != nil { + t.Fatalf("unset XUI_PORT: %v", err) + } + t.Cleanup(func() { + if existed { + _ = os.Setenv("XUI_PORT", original) + } else { + _ = os.Unsetenv("XUI_PORT") + } + }) + } + + port, configured, err := GetPortOverride() + if port != tt.wantPort { + t.Errorf("port = %d, want %d", port, tt.wantPort) + } + if configured != tt.configured { + t.Errorf("configured = %t, want %t", configured, tt.configured) + } + if (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %t", err, tt.wantErr) + } + }) + } +} diff --git a/internal/web/web.go b/internal/web/web.go index c9107468d..8f14653f8 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -407,6 +407,14 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) { if err != nil { return err } + if envPort, configured, envErr := config.GetPortOverride(); configured { + if envErr != nil { + logger.Warning("Ignoring invalid XUI_PORT; using configured web port:", port, envErr) + } else { + port = envPort + logger.Info("Using XUI_PORT override for web panel port:", port) + } + } listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listener, err := net.Listen("tcp", listenAddr) if err != nil {