Compare commits

...

25 Commits

Author SHA1 Message Date
MHSanaei
d8c783a296 v2.8.8 2026-01-18 18:01:58 +01:00
MHSanaei
809f69729a Update minimum Xray version requirement
Raised the minimum required Xray version from 25.9.11 to 26.1.18 in GetXrayVersions. This ensures only newer versions are considered valid.
2026-01-18 17:50:00 +01:00
MHSanaei
93b7ce199f Add UDP mask support for Hysteria outbound
Introduces a 'congestion' option to Hysteria stream settings and updates the form to allow selection between BBR (Auto) and Brutal. Adds support for UDP masks, including model, serialization, and UI for adding/removing masks with type and password fields.
2026-01-18 17:38:05 +01:00
MHSanaei
2a76cec804 Add Hysteria2 outbound protocol support
Introduces support for the Hysteria2 protocol in outbound settings, including model, parsing, and form UI integration. Adds Hysteria2-specific stream and protocol settings, updates protocol selection, and enables configuration of Hysteria2 parameters in the outbound form.
2026-01-18 17:13:34 +01:00
MHSanaei
88eab032be Add TUN protocol for inbound
Introduces TUN protocol to inbound.js, including a new TunSettings class. Updates inbound form to support TUN protocol and adds a dedicated form template for TUN settings. Translation files are updated with TUN-related strings for all supported languages.
2026-01-18 16:47:01 +01:00
MHSanaei
20ec863f51 Xray Core v26.1.18 2026-01-18 16:06:19 +01:00
Nebulosa
2f4018bbe5 feat: improve BBR management with sysctl.d and backup support (#3658) 2026-01-18 15:47:02 +01:00
Vorontsov Amadey
f273708f6d Feature: Use of username and passwords consisting of several words (#3647) 2026-01-18 15:44:49 +01:00
Nebulosa
e6318d57e4 Add x-ui.service.arch file (#3650)
* Add a service file for Arch-based OSs

* Update release.yml with arch service file

* Update x-ui.service.arch
2026-01-18 15:41:07 +01:00
lolka1333
77fa976ee9 Enhance WebSocket client connection logic and improve event listener management (#3636)
- Updated WebSocketClient to allow connection during CONNECTING state.
- Introduced a flag for reconnection attempts.
- Improved event listener registration to prevent duplicate callbacks.
- Refactored online clients update logic in inbounds.html for better performance and clarity.
- Added CSS styles for subscription link boxes in subpage.html to enhance UI consistency and interactivity.

Co-authored-by: lolka1333 <test123@gmail.com>
2026-01-18 15:38:57 +01:00
MHSanaei
8098d2b1b1 Return nil if no error in GetXrayErr
Added a check to return nil immediately if p.GetErr() returns nil in GetXrayErr, preventing further error handling when no error is present.
2026-01-13 17:40:52 +01:00
VolgaIgor
a691eaea8d Fixed incorrect filtering for IDN top-level domains (#3666) 2026-01-12 02:53:43 +01:00
VolgaIgor
da447e5669 Added curl package to Dockerfile (#3665) 2026-01-11 20:18:54 +01:00
MHSanaei
f8c9aac97c Add port selection and checks for ACME HTTP-01 listener
Introduces user prompts to select the port for ACME HTTP-01 certificate validation (default 80), checks if the chosen port is available, and provides guidance for port forwarding. Adds is_port_in_use helper to all scripts and improves messaging for certificate issuance and error handling.
2026-01-11 15:28:43 +01:00
MHSanaei
e42c17f2b2 Default listen address to 0.0.0.0 in GenXrayInboundConfig
When the listen address is empty, it now defaults to 0.0.0.0 to ensure proper dual-stack IPv4/IPv6 binding, improving compatibility on systems with bindv6only=0.
2026-01-09 20:22:33 +01:00
Nebulosa
427b7b67d8 Refactor ca-certificate dependency (#3655) 2026-01-09 17:05:55 +01:00
Nebulosa
ccf08086ac refactor update geofiles fuctions (#3653) 2026-01-09 17:03:53 +01:00
MHSanaei
7b0a3929ff v2.8.7 2026-01-05 19:00:36 +01:00
MHSanaei
570ab8e5e0 Update OpenSSL installer to version 3.6.0
Replaced Win64OpenSSL_Light-3_5_3.exe with Win64OpenSSL_Light-3_6_0.exe in the windows_files/SSL directory to provide the latest OpenSSL version.
2026-01-05 18:49:30 +01:00
MHSanaei
1240e4c962 Update fasthttp to v1.69.0
Bump github.com/valyala/fasthttp from v1.68.0 to v1.69.0 in go.mod and go.sum to use the latest version.
2026-01-05 18:44:42 +01:00
MHSanaei
c117b8b272 mtu to 1250 2026-01-05 18:10:06 +01:00
Ilya Kryuchkov
6041d10e3d Refactor code and fix linter warnings (#3627)
* refactor: use any instead of empty interface

* refactor: code cleanup
2026-01-05 05:54:56 +01:00
lolka1333
4800f8fb70 feat: Real-time Outbound Traffic, UI Improvements & Fix (#3629)
* Refactor HTML and JavaScript for improved UI and functionality

- Cleaned up JavaScript methods in subscription.js for better readability.
- Updated inbounds.html to clarify traffic update handling and removed unnecessary comments.
- Enhanced xray.html by correcting casing in routingDomainStrategies.
- Added mobile touch scrolling styles in page.html for better tab navigation on small screens.
- Streamlined vless.html by removing redundant line breaks and improving form layout.
- Refined subscription subpage.html for better structure and user experience.
- Adjusted outbounds.html to improve button visibility and functionality.
- Updated xray_traffic_job.go to ensure accurate traffic updates and real-time UI refresh.

* Refactor client traffic handling in InboundService

- Updated addClientTraffic method to initialize onlineClients as an empty slice instead of nil.
- Improved clarity and consistency in handling empty onlineUsers scenario.

* Add WebSocket support for outbounds traffic updates

- Implemented WebSocket connection in xray.html to handle real-time updates for outbounds traffic.
- Enhanced xray_traffic_job.go to retrieve and broadcast outbounds traffic updates.
- Introduced MessageTypeOutbounds in hub.go for managing outbounds messages.
- Added BroadcastOutbounds function in notifier.go to facilitate broadcasting outbounds updates to connected clients.

---------

Co-authored-by: lolka1333 <test123@gmail.com>
2026-01-05 05:50:40 +01:00
Sanaei
a9770e1da2 ip cert (#3631) 2026-01-05 05:47:15 +01:00
MHSanaei
3f15d21f13 fix #3622 2026-01-03 22:31:31 +01:00
59 changed files with 1824 additions and 830 deletions

View File

@@ -18,6 +18,7 @@ on:
- 'go.mod' - 'go.mod'
- 'go.sum' - 'go.sum'
- 'x-ui.service.debian' - 'x-ui.service.debian'
- 'x-ui.service.arch'
- 'x-ui.service.rhel' - 'x-ui.service.rhel'
jobs: jobs:
@@ -80,6 +81,7 @@ jobs:
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/
cp x-ui.service.debian x-ui/ cp x-ui.service.debian x-ui/
cp x-ui.service.arch x-ui/
cp x-ui.service.rhel x-ui/ cp x-ui.service.rhel x-ui/
cp x-ui.sh x-ui/ cp x-ui.sh x-ui/
mv x-ui/xui-release x-ui/x-ui mv x-ui/xui-release x-ui/x-ui
@@ -87,7 +89,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.8/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.1.18/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
@@ -185,7 +187,7 @@ jobs:
cd x-ui\bin cd x-ui\bin
# Download Xray for Windows # Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/" $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.1.18/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip" Remove-Item "Xray-windows-64.zip"
@@ -223,4 +225,4 @@ jobs:
file: x-ui-windows-amd64.zip file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip
overwrite: true overwrite: true
prerelease: true prerelease: true

View File

@@ -27,7 +27,7 @@ case $1 in
esac esac
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip" curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.1.18/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"

View File

@@ -29,7 +29,8 @@ RUN apk add --no-cache --update \
ca-certificates \ ca-certificates \
tzdata \ tzdata \
fail2ban \ fail2ban \
bash bash \
curl
COPY --from=builder /app/build/ /app/ COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/

View File

@@ -1 +1 @@
2.8.6 2.8.8

View File

@@ -80,9 +80,12 @@ type HistoryOfSeeders struct {
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen listen := i.Listen
if listen != "" { // Default to 0.0.0.0 (all interfaces) when listen is empty
listen = fmt.Sprintf("\"%v\"", listen) // This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0
if listen == "" {
listen = "0.0.0.0"
} }
listen = fmt.Sprintf("\"%v\"", listen)
return &xray.InboundConfig{ return &xray.InboundConfig{
Listen: json_util.RawMessage(listen), Listen: json_util.RawMessage(listen),
Port: i.Port, Port: i.Port,

36
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/mhsanaei/3x-ui/v2 module github.com/mhsanaei/3x-ui/v2
go 1.25.5 go 1.25.6
require ( require (
github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/gzip v1.2.5
@@ -11,20 +11,20 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.4.0 github.com/mymmrac/telego v1.5.0
github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.12 github.com/shirou/gopsutil/v4 v4.25.12
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.68.0 github.com/valyala/fasthttp v1.69.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.251208.0 github.com/xtls/xray-core v1.260118.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.47.0
golang.org/x/sys v0.39.0 golang.org/x/sys v0.40.0
golang.org/x/text v0.32.0 golang.org/x/text v0.33.0
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
@@ -33,6 +33,7 @@ require (
require ( require (
github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
@@ -47,7 +48,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@@ -57,20 +58,20 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/miekg/dns v1.1.69 // indirect github.com/miekg/dns v1.1.70 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect github.com/refraction-networking/utls v1.8.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.14 // indirect github.com/sagernet/sing v0.7.14 // indirect
@@ -89,15 +90,16 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.23.0 // indirect golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.31.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.41.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

68
go.sum
View File

@@ -6,6 +6,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E=
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
@@ -57,8 +59,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -106,8 +108,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -122,15 +124,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I= github.com/mymmrac/telego v1.5.0 h1:VjBDZcSpEQim1Y3JX2WCsF/PJqOA2DKfZknXUvtKCnw=
github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o= github.com/mymmrac/telego v1.5.0/go.mod h1:MDYHIeT68tURdcwH4SNCQQ+0xBC3u6wOcH2hBpa4Ip0=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -147,10 +149,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -191,8 +193,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -203,8 +205,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
github.com/xtls/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes= github.com/xtls/xray-core v1.260118.0 h1:RJtgIbQ3ykFRcH1CKeoCgQ5WvhsMFu+lnvLF/fFHagE=
github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4= github.com/xtls/xray-core v1.260118.0/go.mod h1:A5k7TXE2KfAjT8dAq6Ir4mMP1q0OTh+8VMmUdqWMQpg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -231,12 +233,14 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -245,22 +249,22 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -278,7 +282,7 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=

View File

@@ -53,35 +53,52 @@ is_ip() {
is_ipv4 "$1" || is_ipv6 "$1" is_ipv4 "$1" || is_ipv6 "$1"
} }
is_domain() { is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# Port helpers
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
fi
return 1
} }
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install -y -q curl tar tzdata openssl socat apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
;; ;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update && dnf install -y -q curl tar tzdata openssl socat dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
;; ;;
centos) centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum install -y curl tar tzdata openssl socat yum -y update && yum install -y curl tar tzdata socat ca-certificates
else else
dnf -y update && dnf install -y -q curl tar tzdata openssl socat dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata openssl socat pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y curl tar timezone openssl socat zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates
;; ;;
alpine) alpine)
apk update && apk add curl tar tzdata openssl socat apk update && apk add curl tar tzdata socat ca-certificates
;; ;;
*) *)
apt-get update && apt-get install -y -q curl tar tzdata openssl socat apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
;; ;;
esac esac
} }
@@ -154,7 +171,9 @@ setup_ssl_certificate() {
# Enable auto-renew # Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 755 $certPath/* 2>/dev/null # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
# Set certificate for panel # Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem" local webCertFile="/root/cert/${domain}/fullchain.pem"
@@ -170,56 +189,155 @@ setup_ssl_certificate() {
fi fi
} }
# Fallback: generate a self-signed certificate (not publicly trusted) # Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
setup_self_signed_certificate() { # Requires acme.sh and port 80 open for HTTP-01 challenge
local name="$1" # domain or IP to place in SAN setup_ip_certificate() {
local certDir="/root/cert/selfsigned" local ipv4="$1"
local ipv6="$2" # optional
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
mkdir -p "$certDir" # Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
local sanExt="" install_acme
if is_ip "$name"; then if [ $? -ne 0 ]; then
sanExt="IP:${name}" echo -e "${red}Failed to install acme.sh${plain}"
else return 1
sanExt="DNS:${name}" fi
fi fi
# Use -addext if supported; fallback to config file if needed # Validate IP address
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ if [[ -z "$ipv4" ]]; then
-keyout "${certDir}/privkey.pem" \ echo -e "${red}IPv4 address is required${plain}"
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
# Fallback via temporary config file (for older OpenSSL versions)
local tmpCfg="${certDir}/openssl.cnf"
cat > "$tmpCfg" <<EOF
[req]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[req_distinguished_name]
[v3_req]
subjectAltName=${sanExt}
EOF
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "${certDir}/privkey.pem" \
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
rm -f "$tmpCfg"
fi
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Failed to generate self-signed certificate${plain}"
return 1 return 1
fi fi
chmod 755 ${certDir}/* 2>/dev/null if ! is_ipv4 "$ipv4"; then
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 echo -e "${red}Invalid IPv4 address: $ipv4${plain}"
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" return 1
fi
# Create certificate directory
local certDir="/root/cert/ip"
mkdir -p "$certDir"
# Build domain arguments
local domain_args="-d ${ipv4}"
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
domain_args="${domain_args} -d ${ipv6}"
echo -e "${green}Including IPv6 address: ${ipv6}${plain}"
fi
# Set reload command for auto-renewal (add || true so it doesn't fail during first install)
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
# Choose port for HTTP-01 listener (default 80, prompt override)
local WebPort=""
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
WebPort="${WebPort:-80}"
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
WebPort=80
fi
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
if [[ "${WebPort}" -ne 80 ]]; then
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
fi
# Ensure chosen port is available
while true; do
if is_port_in_use "${WebPort}"; then
echo -e "${yellow}Port ${WebPort} is in use.${plain}"
local alt_port=""
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
alt_port="${alt_port// /}"
if [[ -z "${alt_port}" ]]; then
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
return 1
fi
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
echo -e "${red}Invalid port provided.${plain}"
return 1
fi
WebPort="${alt_port}"
continue
else
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
break
fi
done
# Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
--server letsencrypt \
--certificate-profile shortlived \
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
return 1
fi
echo -e "${green}Certificate issued successfully, installing...${plain}"
# Install certificate
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
# but the cert files are still installed. We check for files instead of exit code.
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
--key-file "${certDir}/privkey.pem" \
--fullchain-file "${certDir}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
return 1
fi
echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
# Secure permissions: private key readable only by owner
chmod 600 ${certDir}/privkey.pem 2>/dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
# Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}"
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
if [ $? -ne 0 ]; then
echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}"
echo -e "${yellow}Certificate files are at:${plain}"
echo -e " Cert: ${certDir}/fullchain.pem"
echo -e " Key: ${certDir}/privkey.pem"
else
echo -e "${green}Certificate paths configured successfully${plain}"
fi
echo -e "${green}IP certificate installed and configured successfully!${plain}"
echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
echo -e "${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}"
return 0 return 0
} }
@@ -352,14 +470,18 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
chmod 755 $certPath/* # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
else else
echo -e "${green}Auto renew succeeded, certificate details:${plain}" echo -e "${green}Auto renew succeeded, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
chmod 755 $certPath/* # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
fi fi
# Restart panel # start panel
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
# Prompt user to set panel paths after successful certificate installation # Prompt user to set panel paths after successful certificate installation
@@ -387,7 +509,7 @@ ssl_cert_issue() {
return 0 return 0
} }
# Reusable interactive SSL setup (domain or self-signed) # Reusable interactive SSL setup (domain or IP)
# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage # Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
prompt_and_setup_ssl() { prompt_and_setup_ssl() {
local panel_port="$1" local panel_port="$1"
@@ -397,12 +519,13 @@ prompt_and_setup_ssl() {
local ssl_choice="" local ssl_choice=""
echo -e "${yellow}Choose SSL certificate setup method:${plain}" echo -e "${yellow}Choose SSL certificate setup method:${plain}"
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)" echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
read -rp "Choose an option (default 2): " ssl_choice echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (self-signed) if not 1 # Default to 2 (IP cert) if not 1
if [[ "$ssl_choice" != "1" ]]; then if [[ "$ssl_choice" != "1" ]]; then
ssl_choice="2" ssl_choice="2"
fi fi
@@ -410,7 +533,7 @@ prompt_and_setup_ssl() {
case "$ssl_choice" in case "$ssl_choice" in
1) 1)
# User chose Let's Encrypt domain option # User chose Let's Encrypt domain option
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
ssl_cert_issue ssl_cert_issue
# Extract the domain that was used from the certificate # Extract the domain that was used from the certificate
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
@@ -423,28 +546,30 @@ prompt_and_setup_ssl() {
fi fi
;; ;;
2) 2)
# User chose self-signed option # User chose Let's Encrypt IP certificate option
# Stop panel if running echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-service x-ui stop >/dev/null 2>&1 rc-service x-ui stop >/dev/null 2>&1
else else
systemctl stop x-ui >/dev/null 2>&1 systemctl stop x-ui >/dev/null 2>&1
fi fi
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
setup_self_signed_certificate "${server_ip}" setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
echo -e "${green}Self-signed SSL configured successfully${plain}" echo -e "${green}Let's Encrypt IP certificate configured successfully${plain}"
else else
echo -e "${red}Self-signed SSL setup failed${plain}" echo -e "${red}IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi fi
# Start panel after SSL is configured
if [[ $release == "alpine" ]]; then
rc-service x-ui start >/dev/null 2>&1
else
systemctl start x-ui >/dev/null 2>&1
fi
;; ;;
*) *)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}" echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
@@ -497,7 +622,7 @@ config_after_install() {
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}" echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}"
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo "" echo ""
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
@@ -527,7 +652,7 @@ config_after_install() {
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo "" echo ""
prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}"
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}"
@@ -552,7 +677,7 @@ config_after_install() {
echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}"
fi fi
# Existing install: if no cert configured, prompt user to set domain or self-signed # Existing install: if no cert configured, prompt user for SSL setup
# Properly detect empty cert by checking if cert: line exists and has content after it # Properly detect empty cert by checking if cert: line exists and has content after it
existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -z "$existing_cert" ]]; then if [[ -z "$existing_cert" ]]; then
@@ -560,7 +685,7 @@ config_after_install() {
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo "" echo ""
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
@@ -587,7 +712,7 @@ install_x-ui() {
fi fi
fi fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
exit 1 exit 1
@@ -604,7 +729,7 @@ install_x-ui() {
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1" echo -e "Beginning to install x-ui $1"
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz ${url} curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}" echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1 exit 1
@@ -693,6 +818,15 @@ install_x-ui() {
fi fi
fi fi
;; ;;
arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
*) *)
if [ -f "x-ui.service.rhel" ]; then if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}" echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
@@ -712,6 +846,9 @@ install_x-ui() {
ubuntu | debian | armbian) ubuntu | debian | armbian)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1 curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
;; ;;
arch | manjaro | parch)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
;;
*) *)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1 curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
;; ;;

View File

@@ -80,8 +80,8 @@ func runWebServer() {
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart --- // --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
service.StopBot() service.StopBot()
// -- // --
err := server.Stop() err := server.Stop()
if err != nil { if err != nil {
logger.Debug("Error stopping web server:", err) logger.Debug("Error stopping web server:", err)
@@ -113,7 +113,7 @@ func runWebServer() {
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
service.StopBot() service.StopBot()
// ------------------------------------------------------------ // ------------------------------------------------------------
server.Stop() server.Stop()
subServer.Stop() subServer.Stop()
log.Println("Shutting down servers.") log.Println("Shutting down servers.")

View File

@@ -4,6 +4,7 @@ import (
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"strings" "strings"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
@@ -197,9 +198,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
newOutbounds = append(newOutbounds, s.defaultOutbounds...) newOutbounds = append(newOutbounds, s.defaultOutbounds...)
newConfigJson := make(map[string]any) newConfigJson := make(map[string]any)
for key, value := range s.configJson { maps.Copy(newConfigJson, s.configJson)
newConfigJson[key] = value
}
newConfigJson["outbounds"] = newOutbounds newConfigJson["outbounds"] = newOutbounds
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string)) newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))

View File

@@ -484,8 +484,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
externalProxies, _ := stream["externalProxy"].([]any) externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 { if len(externalProxies) > 0 {
links := "" links := make([]string, 0, len(externalProxies))
for index, externalProxy := range externalProxies { for _, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any) ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string) newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string) dest, _ := ep["dest"].(string)
@@ -511,12 +511,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
if index > 0 { links = append(links, url.String())
links += "\n"
}
links += url.String()
} }
return links return strings.Join(links, "\n")
} }
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)

291
update.sh
View File

@@ -78,7 +78,24 @@ is_ip() {
is_ipv4 "$1" || is_ipv6 "$1" is_ipv4 "$1" || is_ipv6 "$1"
} }
is_domain() { is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# Port helpers
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
fi
return 1
} }
gen_random_string() { gen_random_string() {
@@ -91,29 +108,29 @@ install_base() {
echo -e "${green}Updating and install dependency packages...${plain}" echo -e "${green}Updating and install dependency packages...${plain}"
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1
;; ;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
;; ;;
centos) centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1
else else
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata openssl socat >/dev/null 2>&1 pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed | opensuse-leap)
zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone openssl socat >/dev/null 2>&1 zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1
;; ;;
alpine) alpine)
apk update >/dev/null 2>&1 && apk add curl tar tzdata openssl socat >/dev/null 2>&1 apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1
;; ;;
*) *)
apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata openssl socat >/dev/null 2>&1 apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1
;; ;;
esac esac
} }
@@ -180,7 +197,8 @@ setup_ssl_certificate() {
# Enable auto-renew # Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 755 $certPath/* 2>/dev/null chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
# Set certificate for panel # Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem" local webCertFile="/root/cert/${domain}/fullchain.pem"
@@ -196,57 +214,156 @@ setup_ssl_certificate() {
fi fi
} }
# Fallback: generate a self-signed certificate (not publicly trusted) # Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
setup_self_signed_certificate() { # Requires acme.sh and port 80 open for HTTP-01 challenge
local name="$1" # domain or IP to place in SAN setup_ip_certificate() {
local certDir="/root/cert/selfsigned" local ipv4="$1"
local ipv6="$2" # optional
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
mkdir -p "$certDir" # Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
local sanExt="" install_acme
if is_ip "$name"; then if [ $? -ne 0 ]; then
sanExt="IP:${name}" echo -e "${red}Failed to install acme.sh${plain}"
else return 1
sanExt="DNS:${name}" fi
fi fi
# Try -addext; fallback to config if not supported # Validate IP address
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ if [[ -z "$ipv4" ]]; then
-keyout "${certDir}/privkey.pem" \ echo -e "${red}IPv4 address is required${plain}"
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
local tmpCfg="${certDir}/openssl.cnf"
cat > "$tmpCfg" <<EOF
[req]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[req_distinguished_name]
[v3_req]
subjectAltName=${sanExt}
EOF
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "${certDir}/privkey.pem" \
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
rm -f "$tmpCfg"
fi
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Failed to generate self-signed certificate${plain}"
return 1 return 1
fi fi
chmod 755 ${certDir}/* 2>/dev/null if ! is_ipv4 "$ipv4"; then
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 echo -e "${red}Invalid IPv4 address: $ipv4${plain}"
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" return 1
fi
# Create certificate directory
local certDir="/root/cert/ip"
mkdir -p "$certDir"
# Build domain arguments
local domain_args="-d ${ipv4}"
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
domain_args="${domain_args} -d ${ipv6}"
echo -e "${green}Including IPv6 address: ${ipv6}${plain}"
fi
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
# Choose port for HTTP-01 listener (default 80, prompt override)
local WebPort=""
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
WebPort="${WebPort:-80}"
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
WebPort=80
fi
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
if [[ "${WebPort}" -ne 80 ]]; then
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
fi
# Ensure chosen port is available
while true; do
if is_port_in_use "${WebPort}"; then
echo -e "${yellow}Port ${WebPort} is currently in use.${plain}"
local alt_port=""
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
alt_port="${alt_port// /}"
if [[ -z "${alt_port}" ]]; then
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
return 1
fi
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
echo -e "${red}Invalid port provided.${plain}"
return 1
fi
WebPort="${alt_port}"
continue
else
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
break
fi
done
# Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
--server letsencrypt \
--certificate-profile shortlived \
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
return 1
fi
echo -e "${green}Certificate issued successfully, installing...${plain}"
# Install certificate
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
# but the cert files are still installed. We check for files instead of exit code.
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
--key-file "${certDir}/privkey.pem" \
--fullchain-file "${certDir}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
return 1
fi
echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 600 ${certDir}/privkey.pem 2>/dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
# Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}"
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
if [ $? -ne 0 ]; then
echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}"
echo -e "${yellow}You may need to set them manually in the panel settings.${plain}"
echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}"
echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}"
else
echo -e "${green}Certificate paths set successfully!${plain}"
fi
echo -e "${green}IP certificate installed and configured successfully!${plain}"
echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
echo -e "${yellow}Panel will automatically restart after each renewal.${plain}"
return 0 return 0
} }
# Comprehensive manual SSL certificate issuance via acme.sh # Comprehensive manual SSL certificate issuance via acme.sh
ssl_cert_issue() { ssl_cert_issue() {
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
@@ -376,11 +493,13 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
chmod 755 $certPath/* chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
else else
echo -e "${green}Auto renew succeeded, certificate details:${plain}" echo -e "${green}Auto renew succeeded, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
chmod 755 $certPath/* chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi fi
# Restart panel # Restart panel
@@ -410,7 +529,7 @@ ssl_cert_issue() {
return 0 return 0
} }
# Unified interactive SSL setup (domain or self-signed) # Unified interactive SSL setup (domain or IP)
# Sets global `SSL_HOST` to the chosen domain/IP # Sets global `SSL_HOST` to the chosen domain/IP
prompt_and_setup_ssl() { prompt_and_setup_ssl() {
local panel_port="$1" local panel_port="$1"
@@ -420,12 +539,13 @@ prompt_and_setup_ssl() {
local ssl_choice="" local ssl_choice=""
echo -e "${yellow}Choose SSL certificate setup method:${plain}" echo -e "${yellow}Choose SSL certificate setup method:${plain}"
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)" echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
read -rp "Choose an option (default 2): " ssl_choice echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (self-signed) if not 1 # Default to 2 (IP cert) if not 1
if [[ "$ssl_choice" != "1" ]]; then if [[ "$ssl_choice" != "1" ]]; then
ssl_choice="2" ssl_choice="2"
fi fi
@@ -433,7 +553,7 @@ prompt_and_setup_ssl() {
case "$ssl_choice" in case "$ssl_choice" in
1) 1)
# User chose Let's Encrypt domain option # User chose Let's Encrypt domain option
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
ssl_cert_issue ssl_cert_issue
# Extract the domain that was used from the certificate # Extract the domain that was used from the certificate
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
@@ -446,33 +566,37 @@ prompt_and_setup_ssl() {
fi fi
;; ;;
2) 2)
# User chose self-signed option # User chose Let's Encrypt IP certificate option
# Stop panel if running echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-service x-ui stop >/dev/null 2>&1 rc-service x-ui stop >/dev/null 2>&1
else else
systemctl stop x-ui >/dev/null 2>&1 systemctl stop x-ui >/dev/null 2>&1
fi fi
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
setup_self_signed_certificate "${server_ip}" setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
echo -e "${green}Self-signed SSL configured successfully${plain}" echo -e "${green}Let's Encrypt IP certificate configured successfully${plain}"
else else
echo -e "${red}Self-signed SSL setup failed${plain}" echo -e "${red}IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi fi
# Start panel after SSL is configured
# Restart panel after SSL is configured (restart applies new cert settings)
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-service x-ui start >/dev/null 2>&1 rc-service x-ui restart >/dev/null 2>&1
else else
systemctl start x-ui >/dev/null 2>&1 systemctl restart x-ui >/dev/null 2>&1
fi fi
;; ;;
0)
echo -e "${yellow}Skipping SSL setup${plain}"
SSL_HOST="${server_ip}"
;;
*) *)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}" echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
@@ -523,7 +647,7 @@ config_after_update() {
echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}"
echo -e "${red}═══════════════════════════════════════════${plain}" echo -e "${red}═══════════════════════════════════════════${plain}"
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo "" echo ""
if [[ -z "${server_ip}" ]]; then if [[ -z "${server_ip}" ]]; then
@@ -532,7 +656,7 @@ config_after_update() {
return return
fi fi
# Prompt and setup SSL (domain or self-signed) # Prompt and setup SSL (domain or IP)
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo "" echo ""
@@ -576,10 +700,10 @@ update_x-ui() {
fi fi
fi fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
fi fi
@@ -613,6 +737,7 @@ update_x-ui() {
rm ${xui_folder} -f >/dev/null 2>&1 rm ${xui_folder} -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
rm ${xui_folder}/x-ui -f >/dev/null 2>&1 rm ${xui_folder}/x-ui -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
@@ -695,6 +820,15 @@ update_x-ui() {
fi fi
fi fi
;; ;;
arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Installing arch-like systemd unit...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
*) *)
if [ -f "x-ui.service.rhel" ]; then if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Installing rhel-like systemd unit...${plain}" echo -e "${green}Installing rhel-like systemd unit...${plain}"
@@ -713,6 +847,9 @@ update_x-ui() {
ubuntu | debian | armbian) ubuntu | debian | armbian)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1 ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
;; ;;
arch | manjaro | parch)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
;;
*) *)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1 ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
;; ;;

View File

@@ -13,6 +13,5 @@ func HashPasswordAsBcrypt(password string) (string, error) {
// CheckPasswordHash verifies if the given password matches the bcrypt hash. // CheckPasswordHash verifies if the given password matches the bcrypt hash.
func CheckPasswordHash(hash, password string) bool { func CheckPasswordHash(hash, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
return err == nil
} }

View File

@@ -24,13 +24,22 @@ type Config struct {
// FetchVlessFlags returns map[email]enabled // FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) { func FetchVlessFlags(cfg Config) (map[string]bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error scheme := "ldap"
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) scheme = "ldaps"
} else {
conn, err = ldap.Dial("tcp", addr)
} }
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
var opts []ldap.DialOpt
if cfg.UseTLS {
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
InsecureSkipVerify: false,
}))
}
conn, err := ldap.DialURL(ldapURL, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -91,13 +100,22 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) { func AuthenticateUser(cfg Config, username, password string) (bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error scheme := "ldap"
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) scheme = "ldaps"
} else {
conn, err = ldap.Dial("tcp", addr)
} }
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
var opts []ldap.DialOpt
if cfg.UseTLS {
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
InsecureSkipVerify: false,
}))
}
conn, err := ldap.DialURL(ldapURL, opts...)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -18,10 +18,10 @@ var (
// init initializes the character sequences used for random string generation. // init initializes the character sequences used for random string generation.
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations. // It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
func init() { func init() {
for i := 0; i < 10; i++ { for i := range 10 {
numSeq[i] = rune('0' + i) numSeq[i] = rune('0' + i)
} }
for i := 0; i < 26; i++ { for i := range 26 {
lowerSeq[i] = rune('a' + i) lowerSeq[i] = rune('a' + i)
upperSeq[i] = rune('A' + i) upperSeq[i] = rune('A' + i)
} }
@@ -40,7 +40,7 @@ func init() {
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters). // Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
func Seq(n int) string { func Seq(n int) string {
runes := make([]rune, n) runes := make([]rune, n)
for i := 0; i < n; i++ { for i := range n {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq)))) idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
if err != nil { if err != nil {
panic("crypto/rand failed: " + err.Error()) panic("crypto/rand failed: " + err.Error())

View File

@@ -7,7 +7,7 @@ import "reflect"
func GetFields(t reflect.Type) []reflect.StructField { func GetFields(t reflect.Type) []reflect.StructField {
num := t.NumField() num := t.NumField()
fields := make([]reflect.StructField, 0, num) fields := make([]reflect.StructField, 0, num)
for i := 0; i < num; i++ { for i := range num {
fields = append(fields, t.Field(i)) fields = append(fields, t.Field(i))
} }
return fields return fields
@@ -17,7 +17,7 @@ func GetFields(t reflect.Type) []reflect.StructField {
func GetFieldValues(v reflect.Value) []reflect.Value { func GetFieldValues(v reflect.Value) []reflect.Value {
num := v.NumField() num := v.NumField()
fields := make([]reflect.Value, 0, num) fields := make([]reflect.Value, 0, num)
for i := 0; i < num; i++ { for i := range num {
fields = append(fields, v.Field(i)) fields = append(fields, v.Field(i))
} }
return fields return fields

View File

@@ -47,11 +47,11 @@ func CPUPercentRaw() (float64, error) {
var out [5]uint64 var out [5]uint64
switch len(raw) { switch len(raw) {
case 5 * 8: case 5 * 8:
for i := 0; i < 5; i++ { for i := range 5 {
out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8]) out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])
} }
case 5 * 4: case 5 * 4:
for i := 0; i < 5; i++ { for i := range 5 {
out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4])) out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))
} }
default: default:

View File

@@ -7,6 +7,7 @@ const Protocols = {
MIXED: 'mixed', MIXED: 'mixed',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
TUN: 'tun',
}; };
const SSMethods = { const SSMethods = {
@@ -317,7 +318,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
class KcpStreamSettings extends XrayCommonClass { class KcpStreamSettings extends XrayCommonClass {
constructor( constructor(
mtu = 1350, mtu = 1250,
tti = 50, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
@@ -1739,6 +1740,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return new Inbound.MixedSettings(protocol); case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
default: return null; default: return null;
} }
} }
@@ -1753,6 +1755,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json); case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
default: return null; default: return null;
} }
} }
@@ -2506,7 +2509,7 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
Inbound.WireguardSettings = class extends XrayCommonClass { Inbound.WireguardSettings = class extends XrayCommonClass {
constructor( constructor(
protocol, protocol,
mtu = 1420, mtu = 1250,
secretKey = Wireguard.generateKeypair().privateKey, secretKey = Wireguard.generateKeypair().privateKey,
peers = [new Inbound.WireguardSettings.Peer()], peers = [new Inbound.WireguardSettings.Peer()],
noKernelTun = false noKernelTun = false
@@ -2586,3 +2589,34 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
}; };
} }
}; };
Inbound.TunSettings = class extends Inbound.Settings {
constructor(
protocol,
name = 'xray0',
mtu = 1500,
userLevel = 0
) {
super(protocol);
this.name = name;
this.mtu = mtu;
this.userLevel = userLevel;
}
static fromJson(json = {}) {
return new Inbound.TunSettings(
Protocols.TUN,
json.name ?? 'xray0',
json.mtu ?? json.MTU ?? 1500,
json.userLevel ?? 0
);
}
toJson() {
return {
name: this.name || 'xray0',
mtu: this.mtu || 1500,
userLevel: this.userLevel || 0,
};
}
};

View File

@@ -8,7 +8,8 @@ const Protocols = {
Shadowsocks: "shadowsocks", Shadowsocks: "shadowsocks",
Socks: "socks", Socks: "socks",
HTTP: "http", HTTP: "http",
Wireguard: "wireguard" Wireguard: "wireguard",
Hysteria: "hysteria"
}; };
const SSMethods = { const SSMethods = {
@@ -164,7 +165,7 @@ class TcpStreamSettings extends CommonClass {
class KcpStreamSettings extends CommonClass { class KcpStreamSettings extends CommonClass {
constructor( constructor(
mtu = 1350, mtu = 1250,
tti = 50, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
@@ -424,6 +425,90 @@ class RealityStreamSettings extends CommonClass {
}; };
} }
}; };
class HysteriaStreamSettings extends CommonClass {
constructor(
version = 2,
auth = '',
congestion = '',
up = '0',
down = '0',
udphopPort = '',
udphopInterval = 30,
initStreamReceiveWindow = 8388608,
maxStreamReceiveWindow = 8388608,
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 0,
disablePathMTUDiscovery = false
) {
super();
this.version = version;
this.auth = auth;
this.congestion = congestion;
this.up = up;
this.down = down;
this.udphopPort = udphopPort;
this.udphopInterval = udphopInterval;
this.initStreamReceiveWindow = initStreamReceiveWindow;
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
this.maxIdleTimeout = maxIdleTimeout;
this.keepAlivePeriod = keepAlivePeriod;
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
}
static fromJson(json = {}) {
let udphopPort = '';
let udphopInterval = 30;
if (json.udphop) {
udphopPort = json.udphop.port || '';
udphopInterval = json.udphop.interval || 30;
}
return new HysteriaStreamSettings(
json.version,
json.auth,
json.congestion,
json.up,
json.down,
udphopPort,
udphopInterval,
json.initStreamReceiveWindow,
json.maxStreamReceiveWindow,
json.initConnectionReceiveWindow,
json.maxConnectionReceiveWindow,
json.maxIdleTimeout,
json.keepAlivePeriod,
json.disablePathMTUDiscovery
);
}
toJson() {
const result = {
version: this.version,
auth: this.auth,
congestion: this.congestion,
up: this.up,
down: this.down,
initStreamReceiveWindow: this.initStreamReceiveWindow,
maxStreamReceiveWindow: this.maxStreamReceiveWindow,
initConnectionReceiveWindow: this.initConnectionReceiveWindow,
maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
maxIdleTimeout: this.maxIdleTimeout,
keepAlivePeriod: this.keepAlivePeriod,
disablePathMTUDiscovery: this.disablePathMTUDiscovery
};
if (this.udphopPort) {
result.udphop = {
port: this.udphopPort,
interval: this.udphopInterval
};
}
return result;
}
};
class SockoptStreamSettings extends CommonClass { class SockoptStreamSettings extends CommonClass {
constructor( constructor(
dialerProxy = "", dialerProxy = "",
@@ -473,6 +558,30 @@ class SockoptStreamSettings extends CommonClass {
} }
} }
class UdpMask extends CommonClass {
constructor(type = 'salamander', password = '') {
super();
this.type = type;
this.password = password;
}
static fromJson(json = {}) {
return new UdpMask(
json.type,
json.settings?.password || ''
);
}
toJson() {
return {
type: this.type,
settings: {
password: this.password
}
};
}
}
class StreamSettings extends CommonClass { class StreamSettings extends CommonClass {
constructor( constructor(
network = 'tcp', network = 'tcp',
@@ -485,6 +594,8 @@ class StreamSettings extends CommonClass {
grpcSettings = new GrpcStreamSettings(), grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(), httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(), xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
udpmasks = [],
sockopt = undefined, sockopt = undefined,
) { ) {
super(); super();
@@ -498,9 +609,19 @@ class StreamSettings extends CommonClass {
this.grpc = grpcSettings; this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings; this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings; this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.udpmasks = udpmasks;
this.sockopt = sockopt; this.sockopt = sockopt;
} }
addUdpMask() {
this.udpmasks.push(new UdpMask());
}
delUdpMask(index) {
this.udpmasks.splice(index, 1);
}
get isTls() { get isTls() {
return this.security === 'tls'; return this.security === 'tls';
} }
@@ -518,6 +639,7 @@ class StreamSettings extends CommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
const udpmasks = json.udpmasks ? json.udpmasks.map(mask => UdpMask.fromJson(mask)) : [];
return new StreamSettings( return new StreamSettings(
json.network, json.network,
json.security, json.security,
@@ -529,6 +651,8 @@ class StreamSettings extends CommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings), GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings), HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
udpmasks,
SockoptStreamSettings.fromJson(json.sockopt), SockoptStreamSettings.fromJson(json.sockopt),
); );
} }
@@ -546,6 +670,8 @@ class StreamSettings extends CommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined, grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
udpmasks: this.udpmasks.length > 0 ? this.udpmasks.map(mask => mask.toJson()) : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
}; };
} }
@@ -609,7 +735,8 @@ class Outbound extends CommonClass {
} }
canEnableTls() { canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false; if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false;
if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network); return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
} }
@@ -634,7 +761,7 @@ class Outbound extends CommonClass {
} }
canEnableStream() { canEnableStream() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol); return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);
} }
canEnableMux() { canEnableMux() {
@@ -673,7 +800,8 @@ class Outbound extends CommonClass {
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.Socks, Protocols.Socks,
Protocols.HTTP Protocols.HTTP,
Protocols.Hysteria
].includes(this.protocol); ].includes(this.protocol);
} }
@@ -722,6 +850,9 @@ class Outbound extends CommonClass {
case Protocols.Trojan: case Protocols.Trojan:
case 'ss': case 'ss':
return this.fromParamLink(link); return this.fromParamLink(link);
case 'hysteria2':
case Protocols.Hysteria:
return this.fromHysteriaLink(link);
default: default:
return null; return null;
} }
@@ -842,6 +973,62 @@ class Outbound extends CommonClass {
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port; remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
return new Outbound(remark, protocol, settings, stream); return new Outbound(remark, protocol, settings, stream);
} }
static fromHysteriaLink(link) {
// Parse hysteria2://password@address:port[?param1=value1&param2=value2...][#remarks]
const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/;
const match = link.match(regex);
if (!match) return null;
let [, password, address, port, params, hash] = match;
port = parseInt(port);
// Parse URL parameters if present
let urlParams = new URLSearchParams(params);
// Create stream settings with hysteria network
let stream = new StreamSettings('hysteria', 'none');
// Set hysteria stream settings
stream.hysteria.auth = password;
stream.hysteria.congestion = urlParams.get('congestion') ?? '';
stream.hysteria.up = urlParams.get('up') ?? '0';
stream.hysteria.down = urlParams.get('down') ?? '0';
stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? '';
stream.hysteria.udphopInterval = parseInt(urlParams.get('udphopInterval') ?? '30');
// Optional QUIC parameters
if (urlParams.has('initStreamReceiveWindow')) {
stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow'));
}
if (urlParams.has('maxStreamReceiveWindow')) {
stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));
}
if (urlParams.has('initConnectionReceiveWindow')) {
stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));
}
if (urlParams.has('maxConnectionReceiveWindow')) {
stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));
}
if (urlParams.has('maxIdleTimeout')) {
stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));
}
if (urlParams.has('keepAlivePeriod')) {
stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));
}
if (urlParams.has('disablePathMTUDiscovery')) {
stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';
}
// Create settings
let settings = new Outbound.HysteriaSettings(address, port, 2);
// Extract remark from hash
let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;
return new Outbound(remark, Protocols.Hysteria, settings, stream);
}
} }
Outbound.Settings = class extends CommonClass { Outbound.Settings = class extends CommonClass {
@@ -862,6 +1049,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return new Outbound.SocksSettings(); case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings(); case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings(); case Protocols.Wireguard: return new Outbound.WireguardSettings();
case Protocols.Hysteria: return new Outbound.HysteriaSettings();
default: return null; default: return null;
} }
} }
@@ -878,6 +1066,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json); case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json); case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
default: return null; default: return null;
} }
} }
@@ -1233,7 +1422,7 @@ Outbound.HttpSettings = class extends CommonClass {
Outbound.WireguardSettings = class extends CommonClass { Outbound.WireguardSettings = class extends CommonClass {
constructor( constructor(
mtu = 1420, mtu = 1250,
secretKey = '', secretKey = '',
address = [''], address = [''],
workers = 2, workers = 2,
@@ -1324,4 +1513,30 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
keepAlive: this.keepAlive ?? undefined, keepAlive: this.keepAlive ?? undefined,
}; };
} }
};
Outbound.HysteriaSettings = class extends CommonClass {
constructor(address = '', port = 443, version = 2) {
super();
this.address = address;
this.port = port;
this.version = version;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
return new Outbound.HysteriaSettings(
json.address,
json.port,
json.version
);
}
toJson() {
return {
address: this.address,
port: this.port,
version: this.version
};
}
}; };

View File

@@ -138,14 +138,14 @@
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`; return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
}, },
v2raytunUrl() { v2raytunUrl() {
return this.app.subUrl; return this.app.subUrl;
}, },
npvtunUrl() { npvtunUrl() {
return this.app.subUrl; return this.app.subUrl;
}, },
happUrl() { happUrl() {
return `happ://add/${encodeURIComponent(this.app.subUrl)}`; return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
} }
}, },
methods: { methods: {
renderLink, renderLink,

View File

@@ -142,7 +142,7 @@ class RandomUtil {
let length = 32; let length = 32;
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) { if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
length = 16; length = 16;
} }
const array = new Uint8Array(length); const array = new Uint8Array(length);
@@ -154,28 +154,28 @@ class RandomUtil {
static randomBase32String(length = 16) { static randomBase32String(length = 16) {
const array = new Uint8Array(length); const array = new Uint8Array(length);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = ''; let result = '';
let bits = 0; let bits = 0;
let buffer = 0; let buffer = 0;
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
buffer = (buffer << 8) | array[i]; buffer = (buffer << 8) | array[i];
bits += 8; bits += 8;
while (bits >= 5) { while (bits >= 5) {
bits -= 5; bits -= 5;
result += base32Chars[(buffer >>> bits) & 0x1F]; result += base32Chars[(buffer >>> bits) & 0x1F];
} }
} }
if (bits > 0) { if (bits > 0) {
result += base32Chars[(buffer << (5 - bits)) & 0x1F]; result += base32Chars[(buffer << (5 - bits)) & 0x1F];
} }
return result; return result;
} }
} }
@@ -908,7 +908,10 @@ class IntlUtil {
const language = LanguageManager.getLanguage() const language = LanguageManager.getLanguage()
const now = new Date() const now = new Date()
const diff = Math.round((date - now) / (1000 * 60 * 60 * 24)) // Handle delayed start (negative expiryTime values)
const diff = date < 0
? Math.round(date / (1000 * 60 * 60 * 24))
: Math.round((date - now) / (1000 * 60 * 60 * 24))
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' }) const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
return formatter.format(diff, 'day'); return formatter.format(diff, 'day');

View File

@@ -14,10 +14,12 @@ class WebSocketClient {
} }
connect() { connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) { if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return; return;
} }
this.shouldReconnect = true;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Ensure basePath ends with '/' for proper URL construction // Ensure basePath ends with '/' for proper URL construction
let basePath = this.basePath || ''; let basePath = this.basePath || '';
@@ -97,7 +99,10 @@ class WebSocketClient {
if (!this.listeners.has(event)) { if (!this.listeners.has(event)) {
this.listeners.set(event, []); this.listeners.set(event, []);
} }
this.listeners.get(event).push(callback); const callbacks = this.listeners.get(event);
if (!callbacks.includes(callback)) {
callbacks.push(callback);
}
} }
off(event, callback) { off(event, callback) {

View File

@@ -210,10 +210,10 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
//getting tags for freedom and blackhole outbounds //getting tags for freedom and blackhole outbounds
config, err := a.settingService.GetDefaultXrayConfig() config, err := a.settingService.GetDefaultXrayConfig()
if err == nil && config != nil { if err == nil && config != nil {
if cfgMap, ok := config.(map[string]interface{}); ok { if cfgMap, ok := config.(map[string]any); ok {
if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok { if outbounds, ok := cfgMap["outbounds"].([]any); ok {
for _, outbound := range outbounds { for _, outbound := range outbounds {
if obMap, ok := outbound.(map[string]interface{}); ok { if obMap, ok := outbound.(map[string]any); ok {
switch obMap["protocol"] { switch obMap["protocol"] {
case "freedom": case "freedom":
if tag, ok := obMap["tag"].(string); ok { if tag, ok := obMap["tag"].(string); ok {

View File

@@ -17,7 +17,7 @@ var (
type WebServer interface { type WebServer interface {
GetCron() *cron.Cron // Get the cron scheduler GetCron() *cron.Cron // Get the cron scheduler
GetCtx() context.Context // Get the server context GetCtx() context.Context // Get the server context
GetWSHub() interface{} // Get the WebSocket hub (using interface{} to avoid circular dependency) GetWSHub() any // Get the WebSocket hub (using any to avoid circular dependency)
} }
// SubServer interface defines methods for accessing the subscription server instance. // SubServer interface defines methods for accessing the subscription server instance.

View File

@@ -24,6 +24,40 @@
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
} }
/* mobile touch scrolling for tabs */
@media (max-width: 576px) {
.ant-tabs-nav-container {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
overscroll-behavior-x: contain;
white-space: nowrap;
max-width: 100%;
padding: 0 !important; /* Remove padding for arrows */
}
.ant-tabs-nav-wrap {
overflow: visible !important;
padding: 0 !important;
}
.ant-tabs-nav-scroll {
overflow: visible !important;
box-shadow: none !important;
}
.ant-tabs-nav {
display: flex !important;
transform: none !important; /* Disable JS transform */
width: auto !important;
margin: 0 !important;
}
.ant-tabs-tab-prev,
.ant-tabs-tab-next {
display: none !important; /* Hide arrows */
}
.ant-tabs-nav-container::-webkit-scrollbar {
display: none;
}
}
</style> </style>
<title>{{ .host }} {{ i18n .title}}</title> <title>{{ .host }} {{ i18n .title}}</title>
{{ end }} {{ end }}

View File

@@ -1,6 +1,7 @@
{{define "form/inbound"}} {{define "form/inbound"}}
<!-- base --> <!-- base -->
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "enable" }}'> <a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch> <a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item> </a-form-item>
@@ -9,8 +10,10 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.protocol" :disabled="isEdit"
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -28,7 +31,8 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number> <a-input-number v-model.number="inbound.port" :min="1"
:max="65535"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -41,29 +45,42 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number> <a-input-number v-model.number="dbInbound.totalGB"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span> <span>{{ i18n "pages.inbounds.periodicTrafficResetDesc"
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> }}</span>
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> <br
v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<span
v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong> <strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
<span>[[ IntlUtil.formatDate(dbInbound.lastTrafficResetTime) ]]</span> <span>[[
IntlUtil.formatDate(dbInbound.lastTrafficResetTime)
]]</span>
</span> </span>
</template> </template>
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }} {{ i18n "pages.inbounds.periodicTrafficResetTitle" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-select v-model="dbInbound.trafficReset" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="dbInbound.trafficReset"
<a-select-option value="never">{{ i18n "pages.inbounds.periodicTrafficReset.never" }}</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="daily">{{ i18n "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option> <a-select-option value="never">{{ i18n
<a-select-option value="weekly">{{ i18n "pages.inbounds.periodicTrafficReset.weekly" }}</a-select-option> "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
<a-select-option value="monthly">{{ i18n "pages.inbounds.periodicTrafficReset.monthly" }}</a-select-option> <a-select-option value="daily">{{ i18n
"pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
<a-select-option value="weekly">{{ i18n
"pages.inbounds.periodicTrafficReset.weekly"
}}</a-select-option>
<a-select-option value="monthly">{{ i18n
"pages.inbounds.periodicTrafficReset.monthly"
}}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -71,16 +88,20 @@
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire"
}}</span>
</template> </template>
{{ i18n "pages.inbounds.expireDate" }} {{ i18n "pages.inbounds.expireDate" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker :style="{ width: '100%' }" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" <a-date-picker :style="{ width: '100%' }"
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime"></a-date-picker> v-model="dbInbound._expiryTime"></a-date-picker>
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}' <a-persian-datepicker v-else
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime"> value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
</a-persian-datepicker> </a-persian-datepicker>
</a-form-item> </a-form-item>
@@ -126,6 +147,11 @@
{{template "form/wireguard"}} {{template "form/wireguard"}}
</template> </template>
<!-- tun -->
<template v-if="inbound.protocol === Protocols.TUN">
{{template "form/tun"}}
</template>
<!-- stream settings --> <!-- stream settings -->
<template v-if="inbound.canEnableStream()"> <template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}} {{template "form/streamSettings"}}

View File

@@ -1,12 +1,16 @@
{{define "form/outbound"}} {{define "form/outbound"}}
<!-- base --> <!-- base -->
<a-tabs :active-key="outModal.activeKey" :style="{ padding: '0', backgroundColor: 'transparent' }" <a-tabs :active-key="outModal.activeKey"
:style="{ padding: '0', backgroundColor: 'transparent' }"
@change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }"> @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
<a-tab-pane key="1" tab="Form"> <a-tab-pane key="1" tab="Form">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.protocol"
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback <a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
@@ -21,8 +25,10 @@
<!-- freedom settings--> <!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom"> <template v-if="outbound.protocol === Protocols.Freedom">
<a-form-item label='Strategy'> <a-form-item label='Strategy'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.domainStrategy"
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[
s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Redirect'> <a-form-item label='Redirect'>
@@ -35,18 +41,22 @@
</a-form-item> </a-form-item>
<template v-if="Object.keys(outbound.settings.fragment).length >0"> <template v-if="Object.keys(outbound.settings.fragment).length >0">
<a-form-item label='Packets'> <a-form-item label='Packets'>
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.fragment.packets"
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Length'> <a-form-item label='Length'>
<a-input v-model.trim="outbound.settings.fragment.length"></a-input> <a-input v-model.trim="outbound.settings.fragment.length"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Interval'> <a-form-item label='Interval'>
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input> <a-input
v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Split'> <a-form-item label='Max Split'>
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input> <a-input
v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
</a-form-item> </a-form-item>
</template> </template>
@@ -60,11 +70,13 @@
<!-- Add Noise Button --> <!-- Add Noise Button -->
<template v-if="outbound.settings.noises.length > 0"> <template v-if="outbound.settings.noises.length > 0">
<a-form-item label="Noises"> <a-form-item label="Noises">
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addNoise()"></a-button> <a-button icon="plus" type="primary" size="small"
@click="outbound.settings.addNoise()"></a-button>
</a-form-item> </a-form-item>
<!-- Noise Configurations --> <!-- Noise Configurations -->
<a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false" <a-form v-for="(noise, index) in outbound.settings.noises"
:key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]] <a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
<a-icon v-if="outbound.settings.noises.length > 1" type="delete" <a-icon v-if="outbound.settings.noises.length > 1" type="delete"
@@ -72,8 +84,10 @@
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
<a-form-item label='Type'> <a-form-item label='Type'>
<a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="noise.type"
<a-select-option v-for="s in ['rand','base64','str', 'hex']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['rand','base64','str', 'hex']"
:value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Packet'> <a-form-item label='Packet'>
@@ -83,8 +97,10 @@
<a-input v-model.trim="noise.delay"></a-input> <a-input v-model.trim="noise.delay"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Apply To'> <a-form-item label='Apply To'>
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="noise.applyTo"
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[
s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -94,8 +110,10 @@
<!-- blackhole settings --> <!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole"> <template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'> <a-form-item label='Response Type'>
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.type"
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
@@ -103,16 +121,21 @@
<!-- dns settings --> <!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS"> <template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.network"
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='non-IP queries'> <a-form-item label='non-IP queries'>
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.nonIPQuery"
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[
s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types'> <a-form-item v-if="outbound.settings.nonIPQuery === 'skip'"
label='Block Types'>
<a-input v-model.number="outbound.settings.blockTypes"></a-input> <a-input v-model.number="outbound.settings.blockTypes"></a-input>
</a-form-item> </a-form-item>
</template> </template>
@@ -149,15 +172,19 @@
<a-input disabled v-model="outbound.settings.pubKey"></a-input> <a-input disabled v-model="outbound.settings.pubKey"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'> <a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.domainStrategy"
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]"
:value="wds">[[ wds ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='MTU'> <a-form-item label='MTU'>
<a-input-number v-model.number="outbound.settings.mtu" min="0"></a-input-number> <a-input-number v-model.number="outbound.settings.mtu"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Workers'> <a-form-item label='Workers'>
<a-input-number v-model.number="outbound.settings.workers" min="0"></a-input-number> <a-input-number v-model.number="outbound.settings.workers"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='No Kernel Tun'> <a-form-item label='No Kernel Tun'>
<a-switch v-model="outbound.settings.noKernelTun"></a-switch> <a-switch v-model="outbound.settings.noKernelTun"></a-switch>
@@ -173,11 +200,14 @@
<a-input v-model="outbound.settings.reserved"></a-input> <a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Peers"> <a-form-item label="Peers">
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button> <a-button icon="plus" type="primary" size="small"
@click="outbound.settings.addPeer()"></a-button>
</a-form-item> </a-form-item>
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" <a-form v-for="(peer, index) in outbound.settings.peers" :colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"> :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1" <a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon
v-if="outbound.settings.peers.length>1"
type="delete" @click="() => outbound.settings.delPeer(index)" type="delete" @click="() => outbound.settings.delPeer(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
@@ -193,17 +223,21 @@
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} {{ i18n "pages.xray.wireguard.allowedIPs" }}
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button> <a-button icon="plus" type="primary" size="small"
@click="peer.allowedIPs.push('')"></a-button>
</template> </template>
<template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }"> <template v-for="(aip, index) in peer.allowedIPs"
:style="{ marginBottom: '10px' }">
<a-input v-model.trim="peer.allowedIPs[index]"> <a-input v-model.trim="peer.allowedIPs[index]">
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" <a-button icon="minus" v-if="peer.allowedIPs.length>1"
slot="addonAfter" size="small"
@click="peer.allowedIPs.splice(index, 1)"></a-button> @click="peer.allowedIPs.splice(index, 1)"></a-button>
</a-input> </a-input>
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label='Keep Alive'> <a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number> <a-input-number v-model.number="peer.keepAlive"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
</template> </template>
@@ -214,12 +248,14 @@
<a-input v-model.trim="outbound.settings.address"></a-input> <a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number> <a-input-number v-model.number="outbound.settings.port" :min="1"
:max="65532"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
<!-- VLESS/VMess user settings --> <!-- VLESS/VMess user settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)"> <template
v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'> <a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input> <a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item> </a-form-item>
@@ -227,8 +263,10 @@
<!-- vmess settings --> <!-- vmess settings -->
<template v-if="outbound.protocol === Protocols.VMess"> <template v-if="outbound.protocol === Protocols.VMess">
<a-form-item label='Security'> <a-form-item label='Security'>
<a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.security"
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
@@ -241,35 +279,47 @@
</template> </template>
<template v-if="outbound.canEnableTlsFlow()"> <template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'> <a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.flow"
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option value selected>{{ i18n "none"
}}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
<!-- XTLS Vision Advanced Settings --> <!-- XTLS Vision Advanced Settings -->
<template v-if="outbound.canEnableVisionSeed()"> <template v-if="outbound.canEnableVisionSeed()">
<a-form-item label="Vision Pre-Connect"> <a-form-item label="Vision Pre-Connect">
<a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }" <a-input-number v-model.number="outbound.settings.testpre" :min="0"
:max="10" :style="{ width: '100%' }"
placeholder="0"></a-input-number> placeholder="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Vision Seed"> <a-form-item label="Vision Seed">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="6"> <a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999" <a-input-number v-model.number="outbound.settings.testseed[0]"
:style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number> :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900"
addon-before="[0]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999" <a-input-number v-model.number="outbound.settings.testseed[1]"
:style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number> :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="500"
addon-before="[1]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999" <a-input-number v-model.number="outbound.settings.testseed[2]"
:style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number> :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900"
addon-before="[2]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999" <a-input-number v-model.number="outbound.settings.testseed[3]"
:style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number> :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="256"
addon-before="[3]"></a-input-number>
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
@@ -289,7 +339,8 @@
</template> </template>
<!-- trojan/shadowsocks --> <!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)"> <template
v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.password"></a-input> <a-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item> </a-form-item>
@@ -298,8 +349,10 @@
<!-- shadowsocks --> <!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks"> <template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.method"
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods"
:value="method">[[ method_name
]]</a-select-option> ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -307,15 +360,25 @@
<a-switch v-model="outbound.settings.uot"></a-switch> <a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='UoTVersion'> <a-form-item label='UoTVersion'>
<a-input-number v-model.number="outbound.settings.UoTVersion" :min="1" :max="2"></a-input-number> <a-input-number v-model.number="outbound.settings.UoTVersion"
:min="1" :max="2"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- hysteria settings -->
<template v-if="outbound.protocol === Protocols.Hysteria">
<a-form-item label='Version'>
<a-input-number v-model.number="outbound.settings.version" :min="2"
:max="2" disabled></a-input-number>
</a-form-item>
</template>
<!-- stream settings --> <!-- stream settings -->
<template v-if="outbound.canEnableStream()"> <template v-if="outbound.canEnableStream()">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange" <a-select v-model="outbound.stream.network"
@change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP (RAW)</a-select-option> <a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option> <a-select-option value="kcp">mKCP</a-select-option>
@@ -323,6 +386,8 @@
<a-select-option value="grpc">gRPC</a-select-option> <a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option> <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option> <a-select-option value="xhttp">XHTTP</a-select-option>
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
value="hysteria">Hysteria2</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.network === 'tcp'"> <template v-if="outbound.stream.network === 'tcp'">
@@ -343,7 +408,8 @@
<!-- kcp --> <!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'"> <template v-if="outbound.stream.network === 'kcp'">
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.kcp.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option> <a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option> <a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option> <a-select-option value="utp">uTP</a-select-option>
@@ -357,25 +423,31 @@
<a-input v-model="outbound.stream.kcp.seed"></a-input> <a-input v-model="outbound.stream.kcp.seed"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='MTU'> <a-form-item label='MTU'>
<a-input-number v-model.number="outbound.stream.kcp.mtu" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.mtu"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='TTI (ms)'> <a-form-item label='TTI (ms)'>
<a-input-number v-model.number="outbound.stream.kcp.tti" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.tti"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Uplink (MB/s)'> <a-form-item label='Uplink (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.upCap" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.upCap"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Downlink (MB/s)'> <a-form-item label='Downlink (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.downCap" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.downCap"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Congestion'> <a-form-item label='Congestion'>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch> <a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='Read Buffer (MB)'> <a-form-item label='Read Buffer (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.readBuffer" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.readBuffer"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Write Buffer (MB)'> <a-form-item label='Write Buffer (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer" min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.writeBuffer"
min="0"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
@@ -388,7 +460,8 @@
<a-input v-model.trim="outbound.stream.ws.path"></a-input> <a-input v-model.trim="outbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Heartbeat Period'> <a-form-item label='Heartbeat Period'>
<a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
@@ -424,45 +497,144 @@
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input> <a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Mode'> <a-form-item label='Mode'>
<a-select v-model="outbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.xhttp.mode"
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="No gRPC Header" <a-form-item label="No gRPC Header"
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'"> v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch> <a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'"> <a-form-item label="Min Upload Interval (Ms)"
<a-input v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input> v-if="outbound.stream.xhttp.mode === 'packet-up'">
<a-input
v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections"> <a-form-item label="Max Concurrency"
<a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input> v-if="!outbound.stream.xhttp.xmux.maxConnections">
<a-input
v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency"> <a-form-item label="Max Connections"
<a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input> v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
<a-input
v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Reuse Times"> <a-form-item label="Max Reuse Times">
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input> <a-input
v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Request Times"> <a-form-item label="Max Request Times">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input> <a-input
v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Reusable Secs"> <a-form-item label="Max Reusable Secs">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input> <a-input
v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Keep Alive Period'> <a-form-item label='Keep Alive Period'>
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number> <a-input-number
v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
<!-- hysteria -->
<template v-if="outbound.stream.network === 'hysteria'">
<a-form-item label='Auth Password'>
<a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
</a-form-item>
<a-form-item label='Congestion'>
<a-select v-model="outbound.stream.hysteria.congestion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">BBR (Auto)</a-select-option>
<a-select-option value="brutal">Brutal</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Upload Speed'>
<a-input v-model.trim="outbound.stream.hysteria.up"
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
</a-form-item>
<a-form-item label='Download Speed'>
<a-input v-model.trim="outbound.stream.hysteria.down"
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
</a-form-item>
<a-form-item label='UDP Hop Port'>
<a-input v-model.trim="outbound.stream.hysteria.udphopPort"
placeholder="e.g., 1145-1919 or 11,13,15-17"></a-input>
</a-form-item>
<a-form-item label='UDP Hop Interval (s)'
v-if="outbound.stream.hysteria.udphopPort">
<a-input-number
v-model.number="outbound.stream.hysteria.udphopInterval"
:min="5"></a-input-number>
</a-form-item>
<a-form-item label='Init Stream Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Stream Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Init Connection Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Connection Receive'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
</a-form-item>
<a-form-item label='Max Idle Timeout (s)'>
<a-input-number
v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
:max="120"></a-input-number>
</a-form-item>
<a-form-item label='Keep Alive Period (s)'>
<a-input-number
v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
:max="60"></a-input-number>
</a-form-item>
<a-form-item label='Disable Path MTU'>
<a-switch
v-model="outbound.stream.hysteria.disablePathMTUDiscovery"></a-switch>
</a-form-item>
</template>
</template>
<!-- udpmasks settings -->
<template v-if="outbound.canEnableStream()">
<a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small" @click="outbound.stream.addUdpMask()"></a-button>
</a-form-item>
<template v-if="outbound.stream.udpmasks.length > 0">
<a-form v-for="(mask, index) in outbound.stream.udpmasks" :key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
<a-icon type="delete" @click="() => outbound.stream.delUdpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='Type'>
<a-select v-model="mask.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="salamander">Salamander</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Password'>
<a-input v-model.trim="mask.password" placeholder="Obfuscation password"></a-input>
</a-form-item>
</a-form>
</template>
</template> </template>
<!-- tls settings --> <!-- tls settings -->
<template v-if="outbound.canEnableTls()"> <template v-if="outbound.canEnableTls()">
<a-form-item label='{{ i18n "security" }}'> <a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="outbound.stream.security" button-style="solid"> <a-radio-group v-model="outbound.stream.security"
button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button> <a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button> <a-radio-button v-if="outbound.canEnableReality()"
value="reality">Reality</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.isTls"> <template v-if="outbound.stream.isTls">
@@ -470,15 +642,19 @@
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input> <a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.tls.fingerprint"
<a-select-option value=''>None</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option value>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ALPN"> <a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" <a-select mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn"> v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ECH Config List"> <a-form-item label="ECH Config List">
@@ -492,11 +668,14 @@
<!-- reality settings --> <!-- reality settings -->
<template v-if="outbound.stream.isReality"> <template v-if="outbound.stream.isReality">
<a-form-item label="SNI"> <a-form-item label="SNI">
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input> <a-input
v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.reality.fingerprint"
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Short ID"> <a-form-item label="Short ID">
@@ -506,10 +685,12 @@
<a-input v-model.trim="outbound.stream.reality.spiderX"></a-input> <a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Public Key"> <a-form-item label="Public Key">
<a-textarea v-model.trim="outbound.stream.reality.publicKey"></a-textarea> <a-textarea
v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label="mldsa65 Verify"> <a-form-item label="mldsa65 Verify">
<a-textarea v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea> <a-textarea
v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
@@ -520,18 +701,23 @@
</a-form-item> </a-form-item>
<template v-if="outbound.stream.sockoptSwitch"> <template v-if="outbound.stream.sockoptSwitch">
<a-form-item label="Dialer Proxy"> <a-form-item label="Dialer Proxy">
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.sockopt.dialerProxy"
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ['', ...outModal.tags]"
:value="tag">[[ tag ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Address Port Strategy'> <a-form-item label='Address Port Strategy'>
<a-select v-model="outbound.stream.sockopt.addressPortStrategy" <a-select v-model="outbound.stream.sockopt.addressPortStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in Address_Port_Strategy" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in Address_Port_Strategy"
:value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Keep Alive Interval"> <a-form-item label="Keep Alive Interval">
<a-input-number v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number> <a-input-number
v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Fast Open"> <a-form-item label="TCP Fast Open">
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch> <a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
@@ -543,11 +729,15 @@
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch> <a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Trusted X-Forwarded-For"> <a-form-item label="Trusted X-Forwarded-For">
<a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }" <a-select mode="tags"
v-model="outbound.stream.sockopt.trustedXForwardedFor"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option> <a-select-option
value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option> <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option> <a-select-option
value="True-Client-IP">True-Client-IP</a-select-option>
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option> <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -560,14 +750,18 @@
</a-form-item> </a-form-item>
<template v-if="outbound.mux.enabled"> <template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency"> <a-form-item label="Concurrency">
<a-input-number v-model.number="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number> <a-input-number v-model.number="outbound.mux.concurrency" :min="-1"
:max="1024"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="xudp Concurrency"> <a-form-item label="xudp Concurrency">
<a-input-number v-model.number="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number> <a-input-number v-model.number="outbound.mux.xudpConcurrency"
:min="-1" :max="1024"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="xudp UDP 443"> <a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.mux.xudpProxyUDP443"
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']"
:value="c">[[ c ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
@@ -576,11 +770,13 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true"> <a-tab-pane key="2" tab="JSON" force-render="true">
<a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }"> <a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }">
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link" <a-input addon-before='{{ i18n "pages.xray.outbound.link" }}'
v-model.trim="outModal.link"
placeholder="vmess:// vless:// trojan:// ss://"> placeholder="vmess:// vless:// trojan:// ss://">
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon> <a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
</a-input> </a-input>
<textarea :style="{ position: 'absolute', left: '-800px' }" id="outboundJson"></textarea> <textarea :style="{ position: 'absolute', left: '-800px' }"
id="outboundJson"></textarea>
</a-space> </a-space>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>

View File

@@ -0,0 +1,44 @@
{{define "form/tun"}}
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.nameDesc" }}</span>
</template>
Interface Name
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.name"
placeholder="xray0"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.mtuDesc" }}</span>
</template>
MTU
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="inbound.settings.mtu" :min="1"
:max="9000" placeholder="1500"></a-input-number>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.tun.userLevelDesc" }}</span>
</template>
{{ i18n "pages.xray.tun.userLevel" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="inbound.settings.userLevel" :min="0"
placeholder="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,6 +1,5 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-collapse activeKey="0" <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
@@ -22,115 +21,103 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality"> <template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:wrapper-col="{ md: {span:14} }"> <a-form-item label="Authentication">
<a-form-item label="Authentication"> <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="X25519, not Post-Quantum">X25519 (not
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
Post-Quantum)</a-select-option> <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
(Post-Quantum)</a-select-option> </a-select>
</a-select> </a-form-item>
</a-form-item> <a-form-item label="decryption">
<a-form-item label="decryption"> <a-input v-model.trim="inbound.settings.decryption"></a-input>
<a-input v-model.trim="inbound.settings.decryption"></a-input> </a-form-item>
</a-form-item> <a-form-item label="encryption">
<a-form-item label="encryption"> <a-input v-model="inbound.settings.encryption"></a-input>
<a-input v-model="inbound.settings.encryption"></a-input> </a-form-item>
</a-form-item> <a-form-item label=" ">
<a-form-item label=" "> <a-space>
<a-space> <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
keys</a-button> <a-button danger @click="clearVlessEnc">Clear</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button> </a-space>
</a-space> </a-form-item>
</a-form-item> </a-form>
</a-form> <a-divider :style="{ margin: '5px 0' }"></a-divider>
<a-divider v-if="inbound.settings.selectedAuth" </template>
:style="{ margin: '5px 0' }"></a-divider> <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
</template> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> <a-form-item label="Fallbacks">
<a-form :colon="false" :label-col="{ md: {span:8} }" <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
:wrapper-col="{ md: {span:14} }"> </a-form-item>
<a-form-item label="Fallbacks"> </a-form>
<a-button icon="plus" type="primary" size="small"
@click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:wrapper-col="{ md: {span:14} }"> <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon @click="() => inbound.settings.delFallback(index)"
type="delete" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
@click="() => inbound.settings.delFallback(index)" </a-divider>
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> <a-form-item label='SNI'>
</a-divider> <a-input v-model="fallback.name"></a-input>
<a-form-item label='SNI'> </a-form-item>
<a-input v-model="fallback.name"></a-input> <a-form-item label='ALPN'>
</a-form-item> <a-input v-model="fallback.alpn"></a-input>
<a-form-item label='ALPN'> </a-form-item>
<a-input v-model="fallback.alpn"></a-input> <a-form-item label='Path'>
</a-form-item> <a-input v-model="fallback.path"></a-input>
<a-form-item label='Path'> </a-form-item>
<a-input v-model="fallback.path"></a-input> <a-form-item label='Dest'>
</a-form-item> <a-input v-model="fallback.dest"></a-input>
<a-form-item label='Dest'> </a-form-item>
<a-input v-model="fallback.dest"></a-input> <a-form-item label='xVer'>
</a-form-item> <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
<a-form-item label='xVer'> </a-form-item>
<a-input-number v-model.number="fallback.xver" :min="0" </a-form>
:max="2"></a-input-number> <a-divider :style="{ margin: '5px 0' }"></a-divider>
</a-form-item> </template>
</a-form> <template v-if="inbound.canEnableVisionSeed()">
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
</template> <a-form-item label="Vision Seed">
<template v-if="inbound.canEnableVisionSeed()"> <a-row :gutter="8">
<a-form :colon="false" :label-col="{ md: {span:8} }" <a-col :span="6">
:wrapper-col="{ md: {span:14} }"> <a-input-number
<a-form-item label="Vision Seed"> :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
<a-row :gutter="8"> @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }"
<a-col :span="6"> placeholder="900" addon-before="[0]"></a-input-number>
<a-input-number </a-col>
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" <a-col :span="6">
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" <a-input-number
:style="{ width: '100%' }" :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
placeholder="900" addon-before="[0]"></a-input-number> @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }"
</a-col> placeholder="500" addon-before="[1]"></a-input-number>
<a-col :span="6"> </a-col>
<a-input-number <a-col :span="6">
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" <a-input-number
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
:style="{ width: '100%' }" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="500" addon-before="[1]"></a-input-number> placeholder="900" addon-before="[2]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }"
:style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
placeholder="900" addon-before="[2]"></a-input-number> </a-col>
</a-col> </a-row>
<a-col :span="6"> <a-space :size="8" :style="{ marginTop: '8px' }">
<a-input-number <a-button type="primary" @click="setRandomTestseed">
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" Rand
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" </a-button>
:style="{ width: '100%' }" <a-button @click="resetTestseed">
placeholder="256" addon-before="[3]"></a-input-number> Reset
</a-col> </a-button>
</a-row> </a-space>
<a-space :size="8" :style="{ marginTop: '8px' }"> </a-form-item>
<a-button type="primary" @click="setRandomTestseed"> </a-form>
Rand <a-divider :style="{ margin: '5px 0' }"></a-divider>
</a-button> </template>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
{{end}} {{end}}

View File

@@ -1602,41 +1602,42 @@
if (payload && Array.isArray(payload)) { if (payload && Array.isArray(payload)) {
// Use setInbounds to properly convert to DBInbound objects with methods // Use setInbounds to properly convert to DBInbound objects with methods
this.setInbounds(payload); this.setInbounds(payload);
this.searchInbounds(this.searchKey);
} }
}); });
// Listen for traffic updates // Listen for traffic updates
window.wsClient.on('traffic', (payload) => { window.wsClient.on('traffic', (payload) => {
if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) { // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event
// Update client traffic statistics // because clientTraffics contains delta/incremental values, not total accumulated values.
payload.clientTraffics.forEach(clientTraffic => { // Total traffic is updated via the 'inbounds' event which contains accumulated values from database.
const dbInbound = this.dbInbounds.find(ib => {
if (!ib) return false;
const clients = this.getInboundClients(ib);
return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
});
if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
if (stats) {
stats.up = clientTraffic.up || stats.up;
stats.down = clientTraffic.down || stats.down;
stats.total = clientTraffic.total || stats.total;
}
}
});
}
// Update online clients list in real-time // Update online clients list in real-time
if (payload && Array.isArray(payload.onlineClients)) { if (payload && Array.isArray(payload.onlineClients)) {
this.onlineClients = payload.onlineClients; const nextOnlineClients = payload.onlineClients;
// Recalculate client counts to update online status let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;
this.dbInbounds.forEach(dbInbound => { if (!onlineChanged) {
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id); const prevSet = new Set(this.onlineClients);
if (inbound && this.clientCount[dbInbound.id]) { for (const email of nextOnlineClients) {
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound); if (!prevSet.has(email)) {
onlineChanged = true;
break;
}
} }
}); }
this.onlineClients = nextOnlineClients;
if (onlineChanged) {
// Recalculate client counts to update online status
this.dbInbounds.forEach(dbInbound => {
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
if (inbound && this.clientCount[dbInbound.id]) {
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
}
});
if (this.enableFilter) {
this.filterInbounds();
}
}
} }
// Update last online map in real-time // Update last online map in real-time
@@ -1645,8 +1646,6 @@
} }
}); });
// Notifications disabled - white notifications are not needed
// Fallback to polling if WebSocket fails // Fallback to polling if WebSocket fails
window.wsClient.on('error', () => { window.wsClient.on('error', () => {
console.warn('WebSocket connection failed, falling back to polling'); console.warn('WebSocket connection failed, falling back to polling');

View File

@@ -5,6 +5,43 @@
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script> <script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<style>
.subscription-page .subscription-link-box {
cursor: pointer;
border-radius: 12px;
padding: 25px 20px 15px 20px;
margin-top: -12px;
word-break: break-all;
font-size: 13px;
line-height: 1.5;
text-align: left;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.dark.subscription-page .subscription-link-box {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
.dark.subscription-page .subscription-link-box:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
}
.light.subscription-page .subscription-link-box {
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.85);
}
.light.subscription-page .subscription-link-box:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.14);
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
@@ -20,28 +57,20 @@
</a-space> </a-space>
</template> </template>
<template #extra> <template #extra>
<a-popover <a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
:overlay-class-name="themeSwitcher.currentTheme"
title='{{ i18n "menu.settings" }}'
placement="bottomRight" trigger="click"> placement="bottomRight" trigger="click">
<template #content> <template #content>
<a-space direction="vertical" :size="10"> <a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login> <a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" <span>{{ i18n "pages.settings.language"
}}</span> }}</span>
<a-select ref="selectLang" class="w-100" <a-select ref="selectLang" class="w-100" v-model="lang"
v-model="lang"
@change="LanguageManager.setLanguage(lang)" @change="LanguageManager.setLanguage(lang)"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" <a-select-option :value="l.value" label="English"
label="English" v-for="l in LanguageManager.supportedLanguages" :key="l.value">
v-for="l in LanguageManager.supportedLanguages" <span role="img" :aria-label="l.name" v-text="l.icon"></span>
:key="l.value"> &nbsp;&nbsp;<span v-text="l.name"></span>
<span role="img"
:aria-label="l.name"
v-text="l.icon"></span>
&nbsp;&nbsp;<span
v-text="l.name"></span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-space> </a-space>
@@ -53,42 +82,31 @@
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item> <a-form-item>
<a-space direction="vertical" align="center"> <a-space direction="vertical" align="center">
<a-row type="flex" :gutter="[8,8]" <a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%">
justify="center" style="width:100%"> <a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24" style="text-align:center;">
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24"
style="text-align:center;">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" <a-tag color="purple" class="qr-tag">
class="qr-tag">
<span>{{ i18n <span>{{ i18n
"pages.settings.subSettings"}}</span> "pages.settings.subSettings"}}</span>
</a-tag> </a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner <tr-qr-bg-inner class="qr-bg-sub-inner">
class="qr-bg-sub-inner"> <canvas id="qrcode" class="qr-cv" title='{{ i18n "copy" }}'
<canvas id="qrcode"
class="qr-cv"
title='{{ i18n "copy" }}'
@click="copy(app.subUrl)"></canvas> @click="copy(app.subUrl)"></canvas>
</tr-qr-bg-inner> </tr-qr-bg-inner>
</tr-qr-bg> </tr-qr-bg>
</tr-qr-box> </tr-qr-box>
</a-col> </a-col>
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12" <a-col v-if="app.subJsonUrl" :xs="24" :sm="12" style="text-align:center;">
style="text-align:center;">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" <a-tag color="purple" class="qr-tag">
class="qr-tag">
<span>{{ i18n <span>{{ i18n
"pages.settings.subSettings"}} "pages.settings.subSettings"}}
Json</span> Json</span>
</a-tag> </a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner <tr-qr-bg-inner class="qr-bg-sub-inner">
class="qr-bg-sub-inner"> <canvas id="qrcode-subjson" class="qr-cv" title='{{ i18n "copy" }}'
<canvas id="qrcode-subjson"
class="qr-cv"
title='{{ i18n "copy" }}'
@click="copy(app.subJsonUrl)"></canvas> @click="copy(app.subJsonUrl)"></canvas>
</tr-qr-bg-inner> </tr-qr-bg-inner>
</tr-qr-bg> </tr-qr-bg>
@@ -100,45 +118,36 @@
<a-form-item> <a-form-item>
<a-descriptions bordered :column="1" size="small"> <a-descriptions bordered :column="1" size="small">
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.subId" }}'>[[
label='{{ i18n "subscription.subId" }}'>[[
app.sId app.sId
]]</a-descriptions-item> ]]</a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.status" }}'>
label='{{ i18n "subscription.status" }}'>
<template v-if="isUnlimited"> <template v-if="isUnlimited">
<a-tag color="purple">{{ i18n <a-tag color="purple">{{ i18n
"subscription.unlimited" }}</a-tag> "subscription.unlimited" }}</a-tag>
</template> </template>
<template v-else> <template v-else>
<a-tag <a-tag :color="isActive ? 'green' : 'red'">[[
:color="isActive ? 'green' : 'red'">[[
isActive ? '{{ i18n isActive ? '{{ i18n
"subscription.active" }}' : '{{ i18n "subscription.active" }}' : '{{ i18n
"subscription.inactive" }}' "subscription.inactive" }}'
]]</a-tag> ]]</a-tag>
</template> </template>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.downloaded" }}'>[[
label='{{ i18n "subscription.downloaded" }}'>[[
app.download app.download
]]</a-descriptions-item> ]]</a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.uploaded" }}'>[[
label='{{ i18n "subscription.uploaded" }}'>[[
app.upload app.upload
]]</a-descriptions-item> ]]</a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "usage" }}'>[[ app.used
label='{{ i18n "usage" }}'>[[ app.used
]]</a-descriptions-item> ]]</a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.totalQuota" }}'>[[
label='{{ i18n "subscription.totalQuota" }}'>[[
app.total app.total
]]</a-descriptions-item> ]]</a-descriptions-item>
<a-descriptions-item v-if="app.totalByte > 0" <a-descriptions-item v-if="app.totalByte > 0" label='{{ i18n "remained" }}'>[[
label='{{ i18n "remained" }}'>[[
app.remained ]]</a-descriptions-item> app.remained ]]</a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "lastOnline" }}'>
label='{{ i18n "lastOnline" }}'>
<template v-if="app.lastOnlineMs > 0"> <template v-if="app.lastOnlineMs > 0">
[[ IntlUtil.formatDate(app.lastOnlineMs) ]] [[ IntlUtil.formatDate(app.lastOnlineMs) ]]
</template> </template>
@@ -146,8 +155,7 @@
<span>-</span> <span>-</span>
</template> </template>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item label='{{ i18n "subscription.expiry" }}'>
label='{{ i18n "subscription.expiry" }}'>
<template v-if="app.expireMs === 0"> <template v-if="app.expireMs === 0">
{{ i18n "subscription.noExpiry" }} {{ i18n "subscription.noExpiry" }}
</template> </template>
@@ -160,32 +168,33 @@
</a-form> </a-form>
<br /> <br />
<a-list bordered> <div v-for="(link, idx) in links" :key="link"
<a-list-item v-for="(link, idx) in links" :key="link"> style="position: relative; margin-bottom: 20px; text-align: center;">
<div style="width:100%; text-align:center;"> <div class="qr-box" style="display: inline-block; width: 100%; max-width: 100%;">
<a-button type="primary" :block="isMobile" <a-tag color="purple"
@click="copy(link)">[[ linkName(link, idx) style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
]]</a-button> <span>[[ linkName(link, idx) ]]</span>
</a-tag>
<div @click="copy(link)" class="subscription-link-box">
[[ link ]]
</div> </div>
</a-list-item> </div>
</a-list> </div>
</div>
<br /> <br />
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item> <a-form-item>
<a-row type="flex" justify="center" :gutter="[8,8]" <a-row type="flex" justify="center" :gutter="[8,8]" style="width:100%">
style="width:100%"> <a-col :xs="24" :sm="12" style="text-align:center;">
<a-col :xs="24" :sm="12"
style="text-align:center;">
<!-- Android dropdown --> <!-- Android dropdown -->
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-button icon="android" :block="isMobile" <a-button icon="android" :block="isMobile"
:style="{ marginTop: isMobile ? '6px' : 0 }" :style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary">
size="large" type="primary">
Android <a-icon type="down" /> Android <a-icon type="down" />
</a-button> </a-button>
<a-menu slot="overlay" <a-menu slot="overlay" :class="themeSwitcher.currentTheme">
:class="themeSwitcher.currentTheme">
<a-menu-item key="android-v2box" <a-menu-item key="android-v2box"
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item> @click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
<a-menu-item key="android-v2rayng" <a-menu-item key="android-v2rayng"
@@ -194,39 +203,32 @@
@click="copy(app.subUrl)">Sing-box</a-menu-item> @click="copy(app.subUrl)">Sing-box</a-menu-item>
<a-menu-item key="android-v2raytun" <a-menu-item key="android-v2raytun"
@click="copy(app.subUrl)">V2RayTun</a-menu-item> @click="copy(app.subUrl)">V2RayTun</a-menu-item>
<a-menu-item key="android-npvtunnel" <a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV
@click="copy(app.subUrl)">NPV
Tunnel</a-menu-item> Tunnel</a-menu-item>
<a-menu-item key="android-happ" <a-menu-item key="android-happ"
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item> @click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
<a-col :xs="24" :sm="12" <a-col :xs="24" :sm="12" style="text-align:center;">
style="text-align:center;">
<!-- iOS dropdown --> <!-- iOS dropdown -->
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-button icon="apple" :block="isMobile" <a-button icon="apple" :block="isMobile"
:style="{ marginTop: isMobile ? '6px' : 0 }" :style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary">
size="large" type="primary">
iOS <a-icon type="down" /> iOS <a-icon type="down" />
</a-button> </a-button>
<a-menu slot="overlay" <a-menu slot="overlay" :class="themeSwitcher.currentTheme">
:class="themeSwitcher.currentTheme">
<a-menu-item key="ios-shadowrocket" <a-menu-item key="ios-shadowrocket"
@click="open(shadowrocketUrl)">Shadowrocket</a-menu-item> @click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
<a-menu-item key="ios-v2box" <a-menu-item key="ios-v2box" @click="open(v2boxUrl)">V2Box</a-menu-item>
@click="open(v2boxUrl)">V2Box</a-menu-item>
<a-menu-item key="ios-streisand" <a-menu-item key="ios-streisand"
@click="open(streisandUrl)">Streisand</a-menu-item> @click="open(streisandUrl)">Streisand</a-menu-item>
<a-menu-item key="ios-v2raytun" <a-menu-item key="ios-v2raytun"
@click="copy(v2raytunUrl)">V2RayTun</a-menu-item> @click="copy(v2raytunUrl)">V2RayTun</a-menu-item>
<a-menu-item key="ios-npvtunnel" <a-menu-item key="ios-npvtunnel" @click="copy(npvtunUrl)">NPV
@click="copy(npvtunUrl)">NPV
Tunnel Tunnel
</a-menu-item> </a-menu-item>
<a-menu-item key="ios-happ" <a-menu-item key="ios-happ" @click="open(happUrl)">Happ</a-menu-item>
@click="open(happUrl)">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
@@ -240,17 +242,12 @@
</a-layout> </a-layout>
<!-- Bootstrap data for external JS --> <!-- Bootstrap data for external JS -->
<template id="subscription-data" data-sid="{{ .sId }}" <template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}"
data-download="{{ .download }}" data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
data-upload="{{ .upload }}" data-used="{{ .used }}" data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
data-total="{{ .total }}" data-remained="{{ .remained }}"
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
data-downloadbyte="{{ .downloadByte }}"
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
data-datepicker="{{ .datepicker }}"></template> data-datepicker="{{ .datepicker }}"></template>
<textarea id="subscription-links" <textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
style="display:none">{{ range .result }}{{ . }}
{{ end }}</textarea> {{ end }}</textarea>
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}

View File

@@ -3,8 +3,8 @@
<a-row> <a-row>
<a-col :xs="12" :sm="12" :lg="12"> <a-col :xs="12" :sm="12" :lg="12">
<a-space direction="horizontal" size="small"> <a-space direction="horizontal" size="small">
<a-button type="primary" icon="plus" @click="addOutbound()"> <a-button type="primary" icon="plus" @click="addOutbound">
{{ i18n "pages.xray.outbound.addOutbound" }} <span v-if="!isMobile">{{ i18n "pages.xray.outbound.addOutbound" }}</span>
</a-button> </a-button>
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button> <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
</a-space> </a-space>

View File

@@ -269,7 +269,7 @@
tag: "direct", tag: "direct",
protocol: "freedom" protocol: "freedom"
}, },
routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"], routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
log: { log: {
loglevel: ["none", "debug", "info", "warning", "error"], loglevel: ["none", "debug", "info", "warning", "error"],
access: ["none", "./access.log"], access: ["none", "./access.log"],
@@ -968,6 +968,17 @@
await this.getXraySetting(); await this.getXraySetting();
await this.getXrayResult(); await this.getXrayResult();
await this.getOutboundsTraffic(); await this.getOutboundsTraffic();
if (window.wsClient) {
window.wsClient.connect();
window.wsClient.on('outbounds', (payload) => {
if (payload) {
this.outboundsTraffic = payload;
this.$forceUpdate();
}
});
}
while (true) { while (true) {
await PromiseUtil.sleep(800); await PromiseUtil.sleep(800);
this.saveBtnDisable = this.oldXraySetting === this.xraySetting; this.saveBtnDisable = this.oldXraySetting === this.xraySetting;

View File

@@ -45,7 +45,7 @@ func (j *ClearLogsJob) Run() {
} }
// Clear log files and copy to previous logs // Clear log files and copy to previous logs
for i := 0; i < len(logFiles); i++ { for i := range len(logFiles) {
if i > 0 { if i > 0 {
// Copy to previous logs // Copy to previous logs
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)

View File

@@ -58,7 +58,19 @@ func (j *XrayTrafficJob) Run() {
lastOnlineMap = make(map[string]int64) lastOnlineMap = make(map[string]int64)
} }
// Broadcast traffic update via WebSocket // Fetch updated inbounds from database with accumulated traffic values
// This ensures frontend receives the actual total traffic, not just delta values
updatedInbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("get all inbounds for websocket failed:", err)
}
updatedOutbounds, err := j.outboundService.GetOutboundsTraffic()
if err != nil {
logger.Warning("get all outbounds for websocket failed:", err)
}
// Broadcast traffic update via WebSocket with accumulated values from database
trafficUpdate := map[string]interface{}{ trafficUpdate := map[string]interface{}{
"traffics": traffics, "traffics": traffics,
"clientTraffics": clientTraffics, "clientTraffics": clientTraffics,
@@ -66,6 +78,16 @@ func (j *XrayTrafficJob) Run() {
"lastOnlineMap": lastOnlineMap, "lastOnlineMap": lastOnlineMap,
} }
websocket.BroadcastTraffic(trafficUpdate) websocket.BroadcastTraffic(trafficUpdate)
// Broadcast full inbounds update for real-time UI refresh
if updatedInbounds != nil {
websocket.BroadcastInbounds(updatedInbounds)
}
if updatedOutbounds != nil {
websocket.BroadcastOutbounds(updatedOutbounds)
}
} }
func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) { func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {

View File

@@ -1010,12 +1010,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if len(traffics) == 0 { if len(traffics) == 0 {
// Empty onlineUsers // Empty onlineUsers
if p != nil { if p != nil {
p.SetOnlineClients(nil) p.SetOnlineClients(make([]string, 0))
} }
return nil return nil
} }
var onlineClients []string onlineClients := make([]string, 0)
emails := make([]string, 0, len(traffics)) emails := make([]string, 0, len(traffics))
for _, traffic := range traffics { for _, traffic := range traffics {

View File

@@ -567,7 +567,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) { if major > 26 || (major == 26 && minor > 1) || (major == 26 && minor == 1 && patch >= 18) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@@ -1205,7 +1205,7 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil return keyPair, nil
} }
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) { func (s *ServerService) GetNewEchCert(sni string) (any, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer var out bytes.Buffer
@@ -1223,7 +1223,7 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
configList := lines[1] configList := lines[1]
serverKeys := lines[3] serverKeys := lines[3]
return map[string]interface{}{ return map[string]any{
"echServerKeys": serverKeys, "echServerKeys": serverKeys,
"echConfigList": configList, "echConfigList": configList,
}, nil }, nil

View File

@@ -40,6 +40,9 @@ func (s *XrayService) GetXrayErr() error {
} }
err := p.GetErr() err := p.GetErr()
if err == nil {
return nil
}
if runtime.GOOS == "windows" && err.Error() == "exit status 1" { if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed // exit status 1 on Windows means that Xray process was killed

View File

@@ -531,6 +531,12 @@
"psk" = "المفتاح المشترك" "psk" = "المفتاح المشترك"
"domainStrategy" = "استراتيجية الدومين" "domainStrategy" = "استراتيجية الدومين"
[pages.xray.tun]
"nameDesc" = "اسم واجهة TUN. القيمة الافتراضية هي 'xray0'"
"mtuDesc" = "وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500"
"userLevel" = "مستوى المستخدم"
"userLevelDesc" = "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "فعل DNS" "enable" = "فعل DNS"
"enableDesc" = "فعل سيرفر DNS المدمج" "enableDesc" = "فعل سيرفر DNS المدمج"

View File

@@ -531,6 +531,12 @@
"psk" = "PreShared Key" "psk" = "PreShared Key"
"domainStrategy" = "Domain Strategy" "domainStrategy" = "Domain Strategy"
[pages.xray.tun]
"nameDesc" = "The name of the TUN interface. Default is 'xray0'"
"mtuDesc" = "Maximum Transmission Unit. The maximum size of data packets. Default is 1500"
"userLevel" = "User Level"
"userLevelDesc" = "All connections made through this inbound will use this user level. Default is 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Enable DNS" "enable" = "Enable DNS"
"enableDesc" = "Enable built-in DNS server" "enableDesc" = "Enable built-in DNS server"

View File

@@ -531,6 +531,12 @@
"psk" = "Clave precompartida" "psk" = "Clave precompartida"
"domainStrategy" = "Estrategia de dominio" "domainStrategy" = "Estrategia de dominio"
[pages.xray.tun]
"nameDesc" = "El nombre de la interfaz TUN. El valor predeterminado es 'xray0'"
"mtuDesc" = "Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500"
"userLevel" = "Nivel de Usuario"
"userLevelDesc" = "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Habilitar DNS" "enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS incorporado" "enableDesc" = "Habilitar servidor DNS incorporado"

View File

@@ -531,6 +531,12 @@
"psk" = "کلید مشترک" "psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه" "domainStrategy" = "استراتژی حل دامنه"
[pages.xray.tun]
"nameDesc" = "نام رابط TUN. مقدار پیش‌فرض 'xray0' است"
"mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است"
"userLevel" = "سطح کاربر"
"userLevelDesc" = "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است"
[pages.xray.dns] [pages.xray.dns]
"enable" = "فعال کردن حل دامنه" "enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید" "enableDesc" = "سرور حل دامنه داخلی را فعال کنید"

View File

@@ -531,6 +531,12 @@
"psk" = "Kunci Pra-Bagi" "psk" = "Kunci Pra-Bagi"
"domainStrategy" = "Strategi Domain" "domainStrategy" = "Strategi Domain"
[pages.xray.tun]
"nameDesc" = "Nama antarmuka TUN. Standar adalah 'xray0'"
"mtuDesc" = "Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500"
"userLevel" = "Level Pengguna"
"userLevelDesc" = "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Aktifkan DNS" "enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan" "enableDesc" = "Aktifkan server DNS bawaan"

View File

@@ -531,6 +531,12 @@
"psk" = "共有キー" "psk" = "共有キー"
"domainStrategy" = "ドメイン戦略" "domainStrategy" = "ドメイン戦略"
[pages.xray.tun]
"nameDesc" = "TUN インターフェースの名前。デフォルトは 'xray0' です"
"mtuDesc" = "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です"
"userLevel" = "ユーザーレベル"
"userLevelDesc" = "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
[pages.xray.dns] [pages.xray.dns]
"enable" = "DNSを有効にする" "enable" = "DNSを有効にする"
"enableDesc" = "組み込みDNSサーバーを有効にする" "enableDesc" = "組み込みDNSサーバーを有効にする"

View File

@@ -531,6 +531,12 @@
"psk" = "Chave Pré-Compartilhada" "psk" = "Chave Pré-Compartilhada"
"domainStrategy" = "Estratégia de Domínio" "domainStrategy" = "Estratégia de Domínio"
[pages.xray.tun]
"nameDesc" = "O nome da interface TUN. O padrão é 'xray0'"
"mtuDesc" = "Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500"
"userLevel" = "Nível do Usuário"
"userLevelDesc" = "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Ativar DNS" "enable" = "Ativar DNS"
"enableDesc" = "Ativar o servidor DNS integrado" "enableDesc" = "Ativar o servidor DNS integrado"

View File

@@ -531,6 +531,12 @@
"psk" = "Общий ключ" "psk" = "Общий ключ"
"domainStrategy" = "Стратегия домена" "domainStrategy" = "Стратегия домена"
[pages.xray.tun]
"nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xray0'"
"mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500"
"userLevel" = "Уровень пользователя"
"userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Включить DNS" "enable" = "Включить DNS"
"enableDesc" = "Включить встроенный DNS-сервер" "enableDesc" = "Включить встроенный DNS-сервер"

View File

@@ -531,6 +531,12 @@
"psk" = "Ön Paylaşılan Anahtar" "psk" = "Ön Paylaşılan Anahtar"
"domainStrategy" = "Alan Adı Stratejisi" "domainStrategy" = "Alan Adı Stratejisi"
[pages.xray.tun]
"nameDesc" = "TUN arabiriminin adı. Varsayılan değer 'xray0'dir"
"mtuDesc" = "Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür"
"userLevel" = "Kullanıcı Seviyesi"
"userLevelDesc" = "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır"
[pages.xray.dns] [pages.xray.dns]
"enable" = "DNS'yi Etkinleştir" "enable" = "DNS'yi Etkinleştir"
"enableDesc" = "Dahili DNS sunucusunu etkinleştir" "enableDesc" = "Dahili DNS sunucusunu etkinleştir"

View File

@@ -531,6 +531,12 @@
"psk" = "Спільний ключ" "psk" = "Спільний ключ"
"domainStrategy" = "Стратегія домену" "domainStrategy" = "Стратегія домену"
[pages.xray.tun]
"nameDesc" = "Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'"
"mtuDesc" = "Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500"
"userLevel" = "Рівень користувача"
"userLevelDesc" = "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Увімкнути DNS" "enable" = "Увімкнути DNS"
"enableDesc" = "Увімкнути вбудований DNS-сервер" "enableDesc" = "Увімкнути вбудований DNS-сервер"

View File

@@ -531,6 +531,12 @@
"psk" = "Khóa chia sẻ" "psk" = "Khóa chia sẻ"
"domainStrategy" = "Chiến lược tên miền" "domainStrategy" = "Chiến lược tên miền"
[pages.xray.tun]
"nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xray0'"
"mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500"
"userLevel" = "Mức Người Dùng"
"userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Kích hoạt DNS" "enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp" "enableDesc" = "Kích hoạt máy chủ DNS tích hợp"

View File

@@ -531,6 +531,12 @@
"psk" = "共享密钥" "psk" = "共享密钥"
"domainStrategy" = "域策略" "domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 接口的名称。默认值为 'xray0'"
"mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500"
"userLevel" = "用户级别"
"userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "启用 DNS" "enable" = "启用 DNS"
"enableDesc" = "启用内置 DNS 服务器" "enableDesc" = "启用内置 DNS 服务器"

View File

@@ -531,6 +531,12 @@
"psk" = "共享金鑰" "psk" = "共享金鑰"
"domainStrategy" = "域策略" "domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 介面的名稱。預設值為 'xray0'"
"mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500"
"userLevel" = "用戶級別"
"userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "啟用 DNS" "enable" = "啟用 DNS"
"enableDesc" = "啟用內建 DNS 伺服器" "enableDesc" = "啟用內建 DNS 伺服器"

View File

@@ -487,6 +487,6 @@ func (s *Server) GetCron() *cron.Cron {
} }
// GetWSHub returns the WebSocket hub instance. // GetWSHub returns the WebSocket hub instance.
func (s *Server) GetWSHub() interface{} { func (s *Server) GetWSHub() any {
return s.wsHub return s.wsHub
} }

View File

@@ -20,12 +20,13 @@ const (
MessageTypeInbounds MessageType = "inbounds" // Inbounds list update MessageTypeInbounds MessageType = "inbounds" // Inbounds list update
MessageTypeNotification MessageType = "notification" // System notification MessageTypeNotification MessageType = "notification" // System notification
MessageTypeXrayState MessageType = "xray_state" // Xray state change MessageTypeXrayState MessageType = "xray_state" // Xray state change
MessageTypeOutbounds MessageType = "outbounds" // Outbounds list update
) )
// Message represents a WebSocket message // Message represents a WebSocket message
type Message struct { type Message struct {
Type MessageType `json:"type"` Type MessageType `json:"type"`
Payload interface{} `json:"payload"` Payload any `json:"payload"`
Time int64 `json:"time"` Time int64 `json:"time"`
} }
@@ -249,7 +250,7 @@ func (h *Hub) broadcastParallel(clients []*Client, message []byte) {
} }
// Broadcast sends a message to all connected clients // Broadcast sends a message to all connected clients
func (h *Hub) Broadcast(messageType MessageType, payload interface{}) { func (h *Hub) Broadcast(messageType MessageType, payload any) {
if h == nil { if h == nil {
return return
} }
@@ -288,7 +289,7 @@ func (h *Hub) Broadcast(messageType MessageType, payload interface{}) {
} }
// BroadcastToTopic sends a message only to clients subscribed to the specific topic // BroadcastToTopic sends a message only to clients subscribed to the specific topic
func (h *Hub) BroadcastToTopic(messageType MessageType, payload interface{}) { func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {
if h == nil { if h == nil {
return return
} }

View File

@@ -25,7 +25,7 @@ func GetHub() *Hub {
} }
// BroadcastStatus broadcasts server status update to all connected clients // BroadcastStatus broadcasts server status update to all connected clients
func BroadcastStatus(status interface{}) { func BroadcastStatus(status any) {
hub := GetHub() hub := GetHub()
if hub != nil { if hub != nil {
hub.Broadcast(MessageTypeStatus, status) hub.Broadcast(MessageTypeStatus, status)
@@ -33,7 +33,7 @@ func BroadcastStatus(status interface{}) {
} }
// BroadcastTraffic broadcasts traffic statistics update to all connected clients // BroadcastTraffic broadcasts traffic statistics update to all connected clients
func BroadcastTraffic(traffic interface{}) { func BroadcastTraffic(traffic any) {
hub := GetHub() hub := GetHub()
if hub != nil { if hub != nil {
hub.Broadcast(MessageTypeTraffic, traffic) hub.Broadcast(MessageTypeTraffic, traffic)
@@ -41,13 +41,21 @@ func BroadcastTraffic(traffic interface{}) {
} }
// BroadcastInbounds broadcasts inbounds list update to all connected clients // BroadcastInbounds broadcasts inbounds list update to all connected clients
func BroadcastInbounds(inbounds interface{}) { func BroadcastInbounds(inbounds any) {
hub := GetHub() hub := GetHub()
if hub != nil { if hub != nil {
hub.Broadcast(MessageTypeInbounds, inbounds) hub.Broadcast(MessageTypeInbounds, inbounds)
} }
} }
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
func BroadcastOutbounds(outbounds interface{}) {
hub := GetHub()
if hub != nil {
hub.Broadcast(MessageTypeOutbounds, outbounds)
}
}
// BroadcastNotification broadcasts a system notification to all connected clients // BroadcastNotification broadcasts a system notification to all connected clients
func BroadcastNotification(title, message, level string) { func BroadcastNotification(title, message, level string) {
hub := GetHub() hub := GetHub()

Binary file not shown.

16
x-ui.service.arch Normal file
View File

@@ -0,0 +1,16 @@
[Unit]
Description=x-ui Service
After=network.target
Wants=network.target
[Service]
EnvironmentFile=-/etc/conf.d/x-ui
Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple
WorkingDirectory=/usr/lib/x-ui/
ExecStart=/usr/lib/x-ui/x-ui
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target

385
x-ui.sh
View File

@@ -19,6 +19,23 @@ function LOGI() {
echo -e "${green}[INF] $* ${plain}" echo -e "${green}[INF] $* ${plain}"
} }
# Port helpers: detect listener and owning process (best effort)
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
fi
return 1
}
# Simple helpers for domain/IP validation # Simple helpers for domain/IP validation
is_ipv4() { is_ipv4() {
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
@@ -30,7 +47,7 @@ is_ip() {
is_ipv4 "$1" || is_ipv6 "$1" is_ipv4 "$1" || is_ipv6 "$1"
} }
is_domain() { is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
} }
# check root # check root
@@ -212,9 +229,9 @@ reset_user() {
read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm
if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then
${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 ${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor false >/dev/null 2>&1
else else
${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 ${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor true >/dev/null 2>&1
echo -e "Two factor authentication has been disabled." echo -e "Two factor authentication has been disabled."
fi fi
@@ -230,57 +247,6 @@ gen_random_string() {
echo "$random_string" echo "$random_string"
} }
# Generate and configure a self-signed SSL certificate
setup_self_signed_certificate() {
local name="$1" # domain or IP to place in SAN
local certDir="/root/cert/selfsigned"
LOGI "Generating a self-signed certificate (not publicly trusted)..."
mkdir -p "$certDir"
local sanExt=""
if [[ "$name" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$name" =~ : ]]; then
sanExt="IP:${name}"
else
sanExt="DNS:${name}"
fi
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "${certDir}/privkey.pem" \
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
local tmpCfg="${certDir}/openssl.cnf"
cat > "$tmpCfg" <<EOF
[req]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[req_distinguished_name]
[v3_req]
subjectAltName=${sanExt}
EOF
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "${certDir}/privkey.pem" \
-out "${certDir}/fullchain.pem" \
-subj "/CN=${name}" \
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
rm -f "$tmpCfg"
fi
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
LOGE "Failed to generate self-signed certificate"
return 1
fi
chmod 755 ${certDir}/* >/dev/null 2>&1
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
LOGI "Self-signed certificate configured. Browsers will show a warning."
return 0
}
reset_webbasepath() { reset_webbasepath() {
echo -e "${yellow}Resetting Web Base Path${plain}" echo -e "${yellow}Resetting Web Base Path${plain}"
@@ -340,16 +306,19 @@ check_config() {
fi fi
else else
echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}"
read -rp "Generate a self-signed SSL certificate now? [y/N]: " gen_self echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}"
if [[ "$gen_self" == "y" || "$gen_self" == "Y" ]]; then read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl
if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then
stop >/dev/null 2>&1 stop >/dev/null 2>&1
setup_self_signed_certificate "${server_ip}" ssl_cert_issue_for_ip
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
restart >/dev/null 2>&1
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
# ssl_cert_issue_for_ip already restarts the panel, but ensure it's running
start >/dev/null 2>&1
else else
LOGE "Self-signed SSL setup failed." LOGE "IP certificate setup failed."
echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}"
start >/dev/null 2>&1
fi fi
else else
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
@@ -561,20 +530,27 @@ bbr_menu() {
disable_bbr() { disable_bbr() {
if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]] || [[ ! $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
echo -e "${yellow}BBR is not currently enabled.${plain}" echo -e "${yellow}BBR is not currently enabled.${plain}"
before_show_menu before_show_menu
fi fi
# Replace BBR with CUBIC configurations if [ -f "/etc/sysctl.d/99-bbr-x-ui.conf" ]; then
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#')
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf sysctl -w net.core.default_qdisc="${old_settings%:*}"
sysctl -w net.ipv4.tcp_congestion_control="${old_settings#*:}"
rm /etc/sysctl.d/99-bbr-x-ui.conf
sysctl --system
else
# Replace BBR with CUBIC configurations
if [ -f "/etc/sysctl.conf" ]; then
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
sysctl -p
fi
fi
# Apply changes if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]]; then
sysctl -p
# Verify that BBR is replaced with CUBIC
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
else else
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
@@ -582,50 +558,34 @@ disable_bbr() {
} }
enable_bbr() { enable_bbr() {
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]] && [[ $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
echo -e "${green}BBR is already enabled!${plain}" echo -e "${green}BBR is already enabled!${plain}"
before_show_menu before_show_menu
fi fi
# Check the OS and install necessary packages
case "${release}" in
ubuntu | debian | armbian)
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update && dnf -y install ca-certificates
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum -y install ca-certificates
else
dnf -y update && dnf -y install ca-certificates
fi
;;
arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y ca-certificates
;;
alpine)
apk add ca-certificates
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
;;
esac
# Enable BBR # Enable BBR
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf if [ -d "/etc/sysctl.d/" ]; then
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf {
echo "#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)"
# Apply changes echo "net.core.default_qdisc = fq"
sysctl -p echo "net.ipv4.tcp_congestion_control = bbr"
} > "/etc/sysctl.d/99-bbr-x-ui.conf"
if [ -f "/etc/sysctl.conf" ]; then
# Backup old settings from sysctl.conf, if any
sed -i 's/^net.core.default_qdisc/# &/' /etc/sysctl.conf
sed -i 's/^net.ipv4.tcp_congestion_control/# &/' /etc/sysctl.conf
fi
sysctl --system
else
sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf
sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
sysctl -p
fi
# Verify that BBR is enabled # Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]]; then
echo -e "${green}BBR has been enabled successfully.${plain}" echo -e "${green}BBR has been enabled successfully.${plain}"
else else
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
@@ -951,24 +911,23 @@ delete_ports() {
} }
update_all_geofiles() { update_all_geofiles() {
update_main_geofiles update_geofiles "main"
update_ir_geofiles update_geofiles "IR"
update_ru_geofiles update_geofiles "RU"
} }
update_main_geofiles() { update_geofiles() {
curl -fLRo geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat case "${1}" in
curl -fLRo geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";;
} "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;;
"RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";;
update_ir_geofiles() { esac
curl -fLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat for dat in "${dat_files[@]}"; do
curl -fLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat # Remove suffix for remote filename (e.g., geoip_IR -> geoip)
} remote_file="${dat%%_*}"
curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \
update_ru_geofiles() { https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
curl -fLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat done
curl -fLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
} }
update_geo() { update_geo() {
@@ -979,24 +938,22 @@ update_geo() {
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
cd ${xui_folder}/bin
case "$choice" in case "$choice" in
0) 0)
show_menu show_menu
;; ;;
1) 1)
update_main_geofiles update_geofiles "main"
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
restart restart
;; ;;
2) 2)
update_ir_geofiles update_geofiles "IR"
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
restart restart
;; ;;
3) 3)
update_ru_geofiles update_geofiles "RU"
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart restart
;; ;;
@@ -1036,12 +993,12 @@ install_acme() {
} }
ssl_cert_issue_main() { ssl_cert_issue_main() {
echo -e "${green}\t1.${plain} Get SSL" echo -e "${green}\t1.${plain} Get SSL (Domain)"
echo -e "${green}\t2.${plain} Revoke" echo -e "${green}\t2.${plain} Revoke"
echo -e "${green}\t3.${plain} Force Renew" echo -e "${green}\t3.${plain} Force Renew"
echo -e "${green}\t4.${plain} Show Existing Domains" echo -e "${green}\t4.${plain} Show Existing Domains"
echo -e "${green}\t5.${plain} Set Cert paths for the panel" echo -e "${green}\t5.${plain} Set Cert paths for the panel"
echo -e "${green}\t6.${plain} Auto SSL for Server IP" echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
@@ -1136,9 +1093,10 @@ ssl_cert_issue_main() {
ssl_cert_issue_main ssl_cert_issue_main
;; ;;
6) 6)
echo -e "${yellow}Automatic SSL Certificate for Server IP${plain}" echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}"
echo -e "This will automatically obtain and configure an SSL certificate for your server's IP address." echo -e "This will obtain a certificate for your server's IP using the shortlived profile."
echo -e "${yellow}Note: Let's Encrypt supports IP certificates. Make sure port 80 is open.${plain}" echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
confirm "Do you want to proceed?" "y" confirm "Do you want to proceed?" "y"
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
ssl_cert_issue_for_ip ssl_cert_issue_for_ip
@@ -1155,6 +1113,7 @@ ssl_cert_issue_main() {
ssl_cert_issue_for_ip() { ssl_cert_issue_for_ip() {
LOGI "Starting automatic SSL certificate generation for server IP..." LOGI "Starting automatic SSL certificate generation for server IP..."
LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)"
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
@@ -1172,6 +1131,11 @@ ssl_cert_issue_for_ip() {
LOGI "Server IP detected: ${server_ip}" LOGI "Server IP detected: ${server_ip}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# check for acme.sh first # check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
LOGI "acme.sh not found, installing..." LOGI "acme.sh not found, installing..."
@@ -1211,66 +1175,114 @@ ssl_cert_issue_for_ip() {
;; ;;
esac esac
# check if certificate already exists for this IP # Create certificate directory
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') certPath="/root/cert/ip"
if [ "${currentCert}" == "${server_ip}" ]; then mkdir -p "$certPath"
LOGI "Certificate already exists for IP: ${server_ip}"
certPath="/root/cert/${server_ip}" # Build domain arguments
else local domain_args="-d ${server_ip}"
# create directory for certificate if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then
certPath="/root/cert/${server_ip}" domain_args="${domain_args} -d ${ipv6_addr}"
if [ ! -d "$certPath" ]; then LOGI "Including IPv6 address: ${ipv6_addr}"
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
# Use port 80 for certificate issuance
local WebPort=80
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
LOGI "Make sure port ${WebPort} is open and not in use..."
# issue the certificate for IP
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue -d ${server_ip} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
LOGE "Failed to issue certificate for IP: ${server_ip}"
LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet"
rm -rf ~/.acme.sh/${server_ip}
return 1
else
LOGI "Certificate issued successfully for IP: ${server_ip}"
fi
# install the certificate
~/.acme.sh/acme.sh --installcert -d ${server_ip} \
--key-file /root/cert/${server_ip}/privkey.pem \
--fullchain-file /root/cert/${server_ip}/fullchain.pem \
--reloadcmd "x-ui restart"
if [ $? -ne 0 ]; then
LOGE "Failed to install certificate"
rm -rf ~/.acme.sh/${server_ip}
return 1
else
LOGI "Certificate installed successfully"
fi
# enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 755 $certPath/*
fi fi
# Choose port for HTTP-01 listener (default 80, allow override)
local WebPort=""
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
WebPort="${WebPort:-80}"
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
LOGE "Invalid port provided. Falling back to 80."
WebPort=80
fi
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
if [[ "${WebPort}" -ne 80 ]]; then
LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation."
fi
while true; do
if is_port_in_use "${WebPort}"; then
LOGI "Port ${WebPort} is currently in use."
local alt_port=""
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
alt_port="${alt_port// /}"
if [[ -z "${alt_port}" ]]; then
LOGE "Port ${WebPort} is busy; cannot proceed with issuance."
return 1
fi
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
LOGE "Invalid port provided."
return 1
fi
WebPort="${alt_port}"
continue
else
LOGI "Port ${WebPort} is free and ready for standalone validation."
break
fi
done
# Reload command - restarts panel after renewal
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
# issue the certificate for IP with shortlived profile
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
--server letsencrypt \
--certificate-profile shortlived \
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
LOGE "Failed to issue certificate for IP: ${server_ip}"
LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${server_ip} 2>/dev/null
[[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null
rm -rf ${certPath} 2>/dev/null
return 1
else
LOGI "Certificate issued successfully for IP: ${server_ip}"
fi
# Install the certificate
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
# but the cert files are still installed. We check for files instead of exit code.
~/.acme.sh/acme.sh --installcert -d ${server_ip} \
--key-file "${certPath}/privkey.pem" \
--fullchain-file "${certPath}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then
LOGE "Certificate files not found after installation"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${server_ip} 2>/dev/null
[[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null
rm -rf ${certPath} 2>/dev/null
return 1
fi
LOGI "Certificate files installed successfully"
# enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
# Set certificate paths for the panel # Set certificate paths for the panel
local webCertFile="/root/cert/${server_ip}/fullchain.pem" local webCertFile="${certPath}/fullchain.pem"
local webKeyFile="/root/cert/${server_ip}/privkey.pem" local webKeyFile="${certPath}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Certificate configured for panel" LOGI "Certificate configured for panel"
LOGI " - Certificate File: $webCertFile" LOGI " - Certificate File: $webCertFile"
LOGI " - Private Key File: $webKeyFile" LOGI " - Private Key File: $webKeyFile"
LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)"
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
LOGI "Panel will restart to apply SSL certificate..." LOGI "Panel will restart to apply SSL certificate..."
restart restart
@@ -1433,12 +1445,14 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
LOGE "Auto renew failed, certificate details:" LOGE "Auto renew failed, certificate details:"
ls -lah cert/* ls -lah cert/*
chmod 755 $certPath/* chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
exit 1 exit 1
else else
LOGI "Auto renew succeeded, certificate details:" LOGI "Auto renew succeeded, certificate details:"
ls -lah cert/* ls -lah cert/*
chmod 755 $certPath/* chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi fi
# Prompt user to set panel paths after successful certificate installation # Prompt user to set panel paths after successful certificate installation
@@ -1578,7 +1592,8 @@ ssl_cert_issue_CF() {
else else
LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:"
ls -lah ${certPath}/* ls -lah ${certPath}/*
chmod 755 ${certPath}/* chmod 600 ${certPath}/privkey.pem
chmod 644 ${certPath}/fullchain.pem
fi fi
# Prompt user to set panel paths after successful certificate installation # Prompt user to set panel paths after successful certificate installation

View File

@@ -116,7 +116,7 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
} }
// Add testseed if provided // Add testseed if provided
if testseedVal, ok := user["testseed"]; ok { if testseedVal, ok := user["testseed"]; ok {
if testseedArr, ok := testseedVal.([]interface{}); ok && len(testseedArr) >= 4 { if testseedArr, ok := testseedVal.([]any); ok && len(testseedArr) >= 4 {
testseed := make([]uint32, len(testseedArr)) testseed := make([]uint32, len(testseedArr))
for i, v := range testseedArr { for i, v := range testseedArr {
if num, ok := v.(float64); ok { if num, ok := v.(float64); ok {